stack/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts
2025-08-24 11:36:01 -07:00

2474 lines
88 KiB
TypeScript

import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto";
import { describe } from "vitest";
import { STACK_BACKEND_BASE_URL, it } from "../../../../helpers";
import { Auth, InternalProjectKeys, Project, Team, Webhook, backendContext, bumpEmailAddress, createMailbox, niceBackendFetch } from "../../../backend-helpers";
describe("without project access", () => {
backendContext.set({
projectKeys: "no-project",
});
it("should not be able to read own user", async ({ expect }) => {
await backendContext.with({
projectKeys: InternalProjectKeys,
}, async () => {
await Auth.Otp.signIn();
});
const response = await niceBackendFetch("/api/v1/users/me");
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "ACCESS_TYPE_REQUIRED",
"error": deindent\`
You must specify an access level for this Stack project. Make sure project API keys are provided (eg. x-stack-publishable-client-key) and you set the x-stack-access-type header to 'client', 'server', or 'admin'.
For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/overview#authentication
\`,
},
"headers": Headers {
"x-stack-known-error": "ACCESS_TYPE_REQUIRED",
<some fields may have been hidden>,
},
}
`);
});
it("should not be able to list users", async ({ expect }) => {
await Project.createAndSwitch();
const response = await niceBackendFetch("/api/v1/users");
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "ACCESS_TYPE_REQUIRED",
"error": deindent\`
You must specify an access level for this Stack project. Make sure project API keys are provided (eg. x-stack-publishable-client-key) and you set the x-stack-access-type header to 'client', 'server', or 'admin'.
For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/overview#authentication
\`,
},
"headers": Headers {
"x-stack-known-error": "ACCESS_TYPE_REQUIRED",
<some fields may have been hidden>,
},
}
`);
});
});
describe("with client access", () => {
it("should not be able to read own user if not signed in", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "CANNOT_GET_OWN_USER_WITHOUT_USER",
"error": "You have specified 'me' as a userId, but did not provide authentication for a user.",
},
"headers": Headers {
"x-stack-known-error": "CANNOT_GET_OWN_USER_WITHOUT_USER",
<some fields may have been hidden>,
},
}
`);
});
it.todo("should not be able to read own user if access token uses an incorrect signature", async ({ expect }) => {
// TODO we should hardcode an access token generated with a different signature here
backendContext.set({ userAuth: { accessToken: "replace this with an access token that uses a different signature" } });
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 401,
"body": {
"code": "UNPARSABLE_ACCESS_TOKEN",
"error": "Access token is not parsable.",
},
"headers": Headers {
"x-stack-known-error": "UNPARSABLE_ACCESS_TOKEN",
<some fields may have been hidden>,
},
}
`);
});
it.todo("should not be able to read own user if access token is expired", async ({ expect }) => {
// TODO instead of hardcoding an access token here, we should generate one that is short-lived and wait for it to expire
// this test will fail in some environments because the signature is incorrect
backendContext.set({ userAuth: { ...backendContext.value.userAuth, accessToken: "eyJhbGciOiJFUzI1NiIsImtpZCI6IkVYVkNzT01NRkpBMiJ9.eyJzdWIiOiIzM2U3YzA0My1kMmQxLTQxODctYWNkMy1mOTFiNWVkNjRiNDYiLCJpc3MiOiJodHRwczovL2FjY2Vzcy10b2tlbi5qd3Qtc2lnbmF0dXJlLnN0YWNrLWF1dGguY29tIiwiaWF0IjoxNzM4Mzc0OTU4LCJhdWQiOiJpbnRlcm5hbCIsImV4cCI6MTczODM3NDk4OH0.8USE-ELS4IYjFbzA5yNppNKKQGhdNQ0cUUBW7DMG8xHSfqEGw0Bm19u5uUZV6j0tGZypxRbIftgGaVdBRAOCig" } });
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 401,
"body": {
"code": "ACCESS_TOKEN_EXPIRED",
"details": { "expired_at_millis": 1738374988000 },
"error": "Access token has expired. Please refresh it and try again. (The access token expired at 2025-02-01T01:56:28.000Z.)",
},
"headers": Headers {
"x-stack-known-error": "ACCESS_TOKEN_EXPIRED",
<some fields may have been hidden>,
},
}
`);
});
it("should be able to read own user if signed in", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
},
"selected_team_id": "<stripped UUID>",
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to read own user if signed in even without refresh token", async ({ expect }) => {
await Auth.Otp.signIn();
backendContext.set({ userAuth: { ...backendContext.value.userAuth, refreshToken: undefined } });
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
},
"selected_team_id": "<stripped UUID>",
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should not be able to read own user without access token even if refresh token is given", async ({ expect }) => {
await Auth.Otp.signIn();
backendContext.set({ userAuth: { ...backendContext.value.userAuth, accessToken: undefined } });
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "CANNOT_GET_OWN_USER_WITHOUT_USER",
"error": "You have specified 'me' as a userId, but did not provide authentication for a user.",
},
"headers": Headers {
"x-stack-known-error": "CANNOT_GET_OWN_USER_WITHOUT_USER",
<some fields may have been hidden>,
},
}
`);
});
it("should return access token invalid error when reading own user with invalid access token", async ({ expect }) => {
await Auth.Otp.signIn();
backendContext.set({ userAuth: { ...backendContext.value.userAuth, accessToken: "12341234" } });
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 401,
"body": {
"code": "UNPARSABLE_ACCESS_TOKEN",
"error": "Access token is not parsable.",
},
"headers": Headers {
"x-stack-known-error": "UNPARSABLE_ACCESS_TOKEN",
<some fields may have been hidden>,
},
}
`);
});
it("should be able to update own user", async ({ expect }) => {
await Auth.Otp.signIn();
const response1 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
display_name: "John Doe",
},
});
expect(response1).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "John Doe",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
},
"selected_team_id": "<stripped UUID>",
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const response2 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
client_metadata: { key: "value" },
},
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": { "key": "value" },
"client_read_only_metadata": null,
"display_name": "John Doe",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
},
"selected_team_id": "<stripped UUID>",
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it.todo("should be able to set own profile image URL with an image HTTP URL, and the new profile image URL should be a different HTTP URL on our storage service");
it.todo("should be able to set own profile image URL with a base64 data URL, and the new profile image URL should be a different HTTP URL on our storage service");
it.todo("should not be able to set own profile image URL with a file: protocol URL");
it.todo("should not be able to set own profile image URL to a localhost/non-public URL");
it("should not be able to set own server_metadata", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
display_name: "Johnny Doe",
server_metadata: { "key": "value" },
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "SCHEMA_ERROR",
"details": {
"message": deindent\`
Request validation failed on PATCH /api/v1/users/me:
- body contains unknown properties: server_metadata
\`,
},
"error": deindent\`
Request validation failed on PATCH /api/v1/users/me:
- body contains unknown properties: server_metadata
\`,
},
"headers": Headers {
"x-stack-known-error": "SCHEMA_ERROR",
<some fields may have been hidden>,
},
}
`);
});
it("should not be able to delete own user if project is not configured to allow it", async ({ expect }) => {
await Project.createAndSwitch({
config: {
client_user_deletion_enabled: false,
magic_link_enabled: true,
},
});
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "DELETE",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": "Client user deletion is not enabled for this project",
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to delete own user if project is configured to allow it", async ({ expect }) => {
await Project.createAndSwitch({
config: {
client_user_deletion_enabled: true,
magic_link_enabled: true,
},
});
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "DELETE",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": { "success": true },
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should not be able to create a user", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "client",
method: "POST",
body: {},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 401,
"body": {
"code": "INSUFFICIENT_ACCESS_TYPE",
"details": {
"actual_access_type": "client",
"allowed_access_types": [
"server",
"admin",
],
},
"error": "The x-stack-access-type header must be 'server' or 'admin', but was 'client'.",
},
"headers": Headers {
"x-stack-known-error": "INSUFFICIENT_ACCESS_TYPE",
<some fields may have been hidden>,
},
}
`);
});
it("should set own display name to null when set to the empty string", async ({ expect }) => {
await Auth.Otp.signIn();
const response1 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
display_name: "John Doe",
},
});
expect(response1).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "John Doe",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
},
"selected_team_id": "<stripped UUID>",
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const response2 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
display_name: "",
},
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
},
"selected_team_id": "<stripped UUID>",
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to update totp_secret_base64 to valid base64", async ({ expect }) => {
await Auth.Otp.signIn();
const secret = generateSecureRandomString(32);
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
totp_secret_base64: "ZXhhbXBsZSB2YWx1ZQ==",
},
});
expect(response.status).toEqual(200);
});
it("should not be able to update totp_secret_base64 to invalid base64", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
totp_secret_base64: "not-valid-base64",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "SCHEMA_ERROR",
"details": {
"message": deindent\`
Request validation failed on PATCH /api/v1/users/me:
- body.totp_secret_base64 is not valid base64
\`,
},
"error": deindent\`
Request validation failed on PATCH /api/v1/users/me:
- body.totp_secret_base64 is not valid base64
\`,
},
"headers": Headers {
"x-stack-known-error": "SCHEMA_ERROR",
<some fields may have been hidden>,
},
}
`);
});
it("should not be able to list users", async ({ expect }) => {
await Project.createAndSwitch();
const response = await niceBackendFetch("/api/v1/users", {
accessType: "client",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 401,
"body": {
"code": "INSUFFICIENT_ACCESS_TYPE",
"details": {
"actual_access_type": "client",
"allowed_access_types": [
"server",
"admin",
],
},
"error": "The x-stack-access-type header must be 'server' or 'admin', but was 'client'.",
},
"headers": Headers {
"x-stack-known-error": "INSUFFICIENT_ACCESS_TYPE",
<some fields may have been hidden>,
},
}
`);
});
it("should not be able to read a user", async ({ expect }) => {
await Auth.Otp.signIn();
backendContext.set({
userAuth: null,
});
const response = await niceBackendFetch("/api/v1/users/123", {
accessType: "client",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 401,
"body": {
"code": "INSUFFICIENT_ACCESS_TYPE",
"details": {
"actual_access_type": "client",
"allowed_access_types": [
"server",
"admin",
],
},
"error": "The x-stack-access-type header must be 'server' or 'admin', but was 'client'.",
},
"headers": Headers {
"x-stack-known-error": "INSUFFICIENT_ACCESS_TYPE",
<some fields may have been hidden>,
},
}
`);
});
it("should be able to update own client metadata", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
client_metadata: { key: "value" },
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": { "key": "value" },
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
},
"selected_team_id": "<stripped UUID>",
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should not be able to update own client read-only metadata", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
client_read_only_metadata: { key: "value" },
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "SCHEMA_ERROR",
"details": {
"message": deindent\`
Request validation failed on PATCH /api/v1/users/me:
- body contains unknown properties: client_read_only_metadata
\`,
},
"error": deindent\`
Request validation failed on PATCH /api/v1/users/me:
- body contains unknown properties: client_read_only_metadata
\`,
},
"headers": Headers {
"x-stack-known-error": "SCHEMA_ERROR",
<some fields may have been hidden>,
},
}
`);
});
it("should not be able to update profile image url", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
profile_image_url: "http://localhost:8101/open-graph-image.png",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": "Invalid profile image URL",
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to update profile image url with base64", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
profile_image_url: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",
},
});
expect(response.body.profile_image_url).toMatchInlineSnapshot(`"http://localhost:8121/stack-storage/user-profile-images/<stripped UUID>.gif"`);
});
it("should not be able to update profile image url with invalid base64", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
profile_image_url: "data:image/not-valid;base64,test",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": "Invalid profile image URL",
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to update selected team", async ({ expect }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
await Auth.Otp.signIn();
const { teamId: team1Id } = await Team.createWithCurrentAsCreator({});
const { teamId: team2Id } = await Team.createWithCurrentAsCreator({});
const response1 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
});
expect(response1.body.selected_team_id).toEqual(null);
const response2 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
selected_team_id: team1Id,
},
});
expect(response2.body.selected_team_id).toEqual(team1Id);
const response3 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
selected_team_id: team2Id,
},
});
expect(response3.body.selected_team_id).toEqual(team2Id);
const response4 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
selected_team_id: null,
},
});
expect(response4.body.selected_team_id).toEqual(null);
});
});
describe("with server access", () => {
it("should be able to read own user", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to update own user", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
display_name: "John Doe",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "John Doe",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to delete own user", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "DELETE",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": { "success": true },
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to list users", async ({ expect }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"is_paginated": true,
"items": [
{
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
],
"pagination": { "next_cursor": null },
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("lists users with pagination", async ({ expect }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
for (let i = 0; i < 5; i++) {
await bumpEmailAddress();
await Auth.Otp.signIn();
}
const allResponse = await niceBackendFetch("/api/v1/users", {
accessType: "server",
});
const response1 = await niceBackendFetch("/api/v1/users?limit=2", {
accessType: "server",
});
expect(response1.body.pagination.next_cursor).toBeDefined();
const response2 = await niceBackendFetch(`/api/v1/users?limit=3&cursor=${response1.body.pagination.next_cursor}`, {
accessType: "server",
});
expect(response2.body.pagination.next_cursor).toBeDefined();
// check if response 1 + response 2 = allResponse
expect(response1.body.items.length + response2.body.items.length).toEqual(allResponse.body.items.length);
const allUserIds = new Set(allResponse.body.items.map((user: any) => user.id));
const concatenatedUserIds = new Set([...response1.body.items.map((user: any) => user.id), ...response2.body.items.map((user: any) => user.id)]);
expect(concatenatedUserIds).toEqual(allUserIds);
});
it("should be able to read a user", async ({ expect }) => {
await Auth.Otp.signIn();
const signedInResponse = (await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
}));
const userId = signedInResponse.body.id;
backendContext.set({
userAuth: null,
});
const response = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
expect(response.body.primary_email).toEqual(backendContext.value.mailbox.emailAddress);
});
it("should be able to create a user", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": null,
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to create a user with email auth enabled", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
display_name: "John Dough",
server_metadata: "test",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "John Dough",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": "test",
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to create a user with an email that doesn't match the strict email schema", async ({ expect }) => {
// This test is to ensure that we don't break existing users who have an email that doesn't match the strict email
// schema.
// The frontend no longer allows those emails, but some users may still have them in their accounts and we should
// continue to support them.
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: "invalid_email@gmai"
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "invalid_email@gmai",
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to create a user with a password and sign in with it", async ({ expect }) => {
const password = generateSecureRandomString();
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
password,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": true,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const signInResponse = await Auth.Password.signInWithEmail({ password });
expect(signInResponse.signInResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"access_token": <stripped field 'access_token'>,
"refresh_token": <stripped field 'refresh_token'>,
"user_id": "<stripped UUID>",
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to create a user with a password hash and sign in with it", async ({ expect }) => {
const password = "hello-world";
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
password_hash: "$2a$13$TVyY/gpw9Db/w1fBeJkCgeNg2Rae2JfNqrPnSAKtj.ufAO5cVF13.",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": true,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const signInResponse = await Auth.Password.signInWithEmail({ password });
expect(signInResponse.signInResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"access_token": <stripped field 'access_token'>,
"refresh_token": <stripped field 'refresh_token'>,
"user_id": "<stripped UUID>",
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to create an anonymous user", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
is_anonymous: true,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": true,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": null,
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to make an anonymous user non-anonymous", async ({ expect }) => {
await Auth.Anonymous.signUp();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
is_anonymous: false,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": null,
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "Personal Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should not be able to make a non-anonymous user anonymous", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {},
});
const userId = response.body.id;
const response2 = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
method: "PATCH",
body: {
is_anonymous: true,
},
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "SCHEMA_ERROR",
"details": {
"message": deindent\`
Request validation failed on PATCH /api/v1/users/<stripped UUID>:
- body.is_anonymous must be one of the following values: false
\`,
},
"error": deindent\`
Request validation failed on PATCH /api/v1/users/<stripped UUID>:
- body.is_anonymous must be one of the following values: false
\`,
},
"headers": Headers {
"x-stack-known-error": "SCHEMA_ERROR",
<some fields may have been hidden>,
},
}
`);
});
it("should not be able to create a user when both password and password hash are provided", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
password: "hello-world",
password_hash: "$2a$13$TVyY/gpw9Db/w1fBeJkCgeNg2Rae2JfNqrPnSAKtj.ufAO5cVF13.",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": "Cannot set both password and password_hash at the same time.",
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should not be able to create a user with a password hash that has too many rounds", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
password_hash: "$2a$17$VIhIOofSMqGdGlL4wzE//e.77dAQGqNtF/1dT7bqCrVtQuInWy2qi",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": "Invalid password hash. Make sure it's a supported algorithm in Modular Crypt Format.",
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should not be able to create a user without primary email but with email auth enabled", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email_auth_enabled: true,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": "primary_email_auth_enabled cannot be true without primary_email",
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should not be able to create a user with email auth enabled if the email already exists with email auth enabled", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const response2 = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
},
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 409,
"body": {
"code": "USER_EMAIL_ALREADY_EXISTS",
"details": {
"email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"would_work_if_email_was_verified": false,
},
"error": "A user with email \\"default-mailbox--<stripped UUID>@stack-generated.example.com\\" already exists.",
},
"headers": Headers {
"x-stack-known-error": "USER_EMAIL_ALREADY_EXISTS",
<some fields may have been hidden>,
},
}
`);
});
it("should be able to create a user with email auth enabled if the email already exists but without email auth enabled", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const password = generateSecureRandomString();
const response2 = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
password: password,
},
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": true,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const signInResponse = await Auth.Password.signInWithEmail({ password });
expect(signInResponse.signInResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"access_token": <stripped field 'access_token'>,
"refresh_token": <stripped field 'refresh_token'>,
"user_id": "<stripped UUID>",
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to create a user with email auth disabled even if the email already exists with email auth enabled", async ({ expect }) => {
const password = generateSecureRandomString();
const response2 = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
password: password,
},
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": true,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const response = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 201,
"body": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const signInResponse = await Auth.Password.signInWithEmail({ password });
expect(signInResponse.signInResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"access_token": <stripped field 'access_token'>,
"refresh_token": <stripped field 'refresh_token'>,
"user_id": "<stripped UUID>",
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to update a user", async ({ expect }) => {
await Auth.Otp.signIn();
const signedInResponse = (await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
}));
const userId = signedInResponse.body.id;
backendContext.set({
userAuth: null,
});
const response1 = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
method: "PATCH",
body: {
display_name: "John Doe",
server_metadata: { key: "value" },
},
});
expect(response1).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "John Doe",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": { "key": "value" },
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const response2 = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "John Doe",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": { "key": "value" },
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to update own user", async ({ expect }) => {
await Auth.Otp.signIn();
const response1 = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
display_name: "John Doe",
server_metadata: { key: "value" },
},
});
expect(response1).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "John Doe",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": { "key": "value" },
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to update a user's password, signing them out, and sign in with it again", async ({ expect }) => {
const password = "this-is-some-password";
await Auth.Otp.signIn();
const response1 = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
password,
},
});
expect(response1).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": true,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
backendContext.set({
userAuth: {
...backendContext.value.userAuth,
accessToken: undefined,
},
});
await Auth.expectToBeSignedOut();
await Auth.Password.signInWithEmail({ password });
await Auth.expectToBeSignedIn();
});
it("should be able to delete a user", async ({ expect }) => {
await Auth.Otp.signIn();
const signedInResponse = (await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
}));
const userId = signedInResponse.body.id;
backendContext.set({
userAuth: null,
});
const response1 = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
method: "DELETE",
});
expect(response1).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": { "success": true },
"headers": Headers { <some fields may have been hidden> },
}
`);
const response2 = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 404,
"body": {
"code": "USER_NOT_FOUND",
"error": "User not found.",
},
"headers": Headers {
"x-stack-known-error": "USER_NOT_FOUND",
<some fields may have been hidden>,
},
}
`);
});
it("should be able to update all metadata fields", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
client_metadata: { key: "client value" },
client_read_only_metadata: { key: "client read only value" },
server_metadata: { key: "server value" },
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": { "key": "client value" },
"client_read_only_metadata": { "key": "client read only value" },
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": { "key": "server value" },
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to update profile image url", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
profile_image_url: "http://localhost:8101/open-graph-image.png",
},
});
expect(response.body.profile_image_url).toEqual("http://localhost:8101/open-graph-image.png");
});
it("should be able to update primary email", async ({ expect }) => {
await Auth.Otp.signIn();
const mailbox = createMailbox();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
primary_email: mailbox.emailAddress,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "mailbox-1--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": true,
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should be able to update primary email and sign-in with the new email", async ({ expect }) => {
await Auth.Password.signUpWithEmail({ password: "password123" });
const mailbox = createMailbox();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
primary_email: mailbox.emailAddress,
},
});
expect(response.body.primary_email).toEqual(mailbox.emailAddress);
backendContext.set({
mailbox,
});
await Auth.Password.signInWithEmail({ password: "password123" });
expect(response.body.primary_email).toEqual(mailbox.emailAddress);
});
it("should be able to remove primary email", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
primary_email: null,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": null,
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "default-mailbox--<stripped UUID>@stack-generated.example.com's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should not be able to update primary email to an email already in use for auth by someone else", async ({ expect }) => {
await Auth.Otp.signIn();
const primaryEmail = backendContext.value.mailbox.emailAddress;
await Auth.signOut();
await bumpEmailAddress();
await Auth.Password.signUpWithEmail({ password: "password123" });
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
primary_email: primaryEmail,
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 409,
"body": {
"code": "USER_EMAIL_ALREADY_EXISTS",
"details": {
"email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"would_work_if_email_was_verified": false,
},
"error": "A user with email \\"default-mailbox--<stripped UUID>@stack-generated.example.com\\" already exists.",
},
"headers": Headers {
"x-stack-known-error": "USER_EMAIL_ALREADY_EXISTS",
<some fields may have been hidden>,
},
}
`);
});
it("should not be able to set profile image url to empty string", async ({ expect }) => {
await Auth.Otp.signIn();
const response = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
profile_image_url: "",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "SCHEMA_ERROR",
"details": {
"message": deindent\`
Request validation failed on PATCH /api/v1/users/me:
- body.profile_image_url is not a valid URL
\`,
},
"error": deindent\`
Request validation failed on PATCH /api/v1/users/me:
- body.profile_image_url is not a valid URL
\`,
},
"headers": Headers {
"x-stack-known-error": "SCHEMA_ERROR",
<some fields may have been hidden>,
},
}
`);
});
it("should not be able to sign up with an email already in use for auth", async ({ expect }) => {
await Auth.Password.signUpWithEmail({ password: "password123" });
const response = await niceBackendFetch("/api/v1/auth/password/sign-up", {
method: "POST",
accessType: "client",
body: {
email: backendContext.value.mailbox.emailAddress,
password: "password123",
verification_callback_url: "http://localhost:12345/some-callback-url",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 409,
"body": {
"code": "USER_EMAIL_ALREADY_EXISTS",
"details": {
"email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"would_work_if_email_was_verified": false,
},
"error": "A user with email \\"default-mailbox--<stripped UUID>@stack-generated.example.com\\" already exists.",
},
"headers": Headers {
"x-stack-known-error": "USER_EMAIL_ALREADY_EXISTS",
<some fields may have been hidden>,
},
}
`);
});
it("should be able to sign up with an email already in use for auth in a different project", async ({ expect }) => {
await Auth.Password.signUpWithEmail({ password: "password123" });
await Project.createAndSwitch({});
const response = await niceBackendFetch("/api/v1/auth/password/sign-up", {
method: "POST",
accessType: "client",
body: {
email: backendContext.value.mailbox.emailAddress,
password: "password123",
verification_callback_url: "http://localhost:12345/some-callback-url",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"access_token": <stripped field 'access_token'>,
"refresh_token": <stripped field 'refresh_token'>,
"user_id": "<stripped UUID>",
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should trigger user webhook when a user is created", async ({ expect }) => {
const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint();
const createUserResponse = await niceBackendFetch(new URL("/api/v1/users", STACK_BACKEND_BASE_URL), {
method: "POST",
accessType: "server",
body: {
primary_email: "test@example.com",
},
});
expect(createUserResponse.status).toBe(201);
const attemptResponse = await Webhook.findWebhookAttempt(projectId, endpointId, svixToken, event => true);
expect(attemptResponse).toMatchInlineSnapshot(`
{
"channels": null,
"eventId": null,
"eventType": "user.created",
"id": "<stripped svix message id>",
"payload": {
"data": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "test@example.com",
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"type": "user.created",
},
"timestamp": <stripped field 'timestamp'>,
}
`);
});
it("should trigger user webhook when a user is updated", async ({ expect }) => {
const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint();
const createUserResponse = await niceBackendFetch("/api/v1/users", {
method: "POST",
accessType: "server",
body: {
primary_email: "test@example.com",
},
});
expect(createUserResponse.status).toBe(201);
const userId = createUserResponse.body.id;
const updateUserResponse = await niceBackendFetch(`/api/v1/users/${userId}`, {
method: "PATCH",
accessType: "server",
body: {
display_name: "Test User"
}
});
expect(updateUserResponse.status).toBe(200);
const userUpdatedEvent = await Webhook.findWebhookAttempt(projectId, endpointId, svixToken, event => event.eventType === "user.updated");
expect(userUpdatedEvent).toMatchInlineSnapshot(`
{
"channels": null,
"eventId": null,
"eventType": "user.updated",
"id": "<stripped svix message id>",
"payload": {
"data": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "Test User",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "test@example.com",
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": null,
"selected_team_id": null,
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"type": "user.updated",
},
"timestamp": <stripped field 'timestamp'>,
}
`);
});
it("should trigger user webhook when a user is deleted", async ({ expect }) => {
const { projectId, svixToken, endpointId } = await Webhook.createProjectWithEndpoint();
const createUserResponse = await niceBackendFetch(new URL("/api/v1/users", STACK_BACKEND_BASE_URL), {
method: "POST",
accessType: "server",
body: {
primary_email: "test@example.com",
},
});
expect(createUserResponse.status).toBe(201);
const userId = createUserResponse.body.id;
const deleteUserResponse = await niceBackendFetch(new URL(`/api/v1/users/${userId}`, STACK_BACKEND_BASE_URL), {
method: "DELETE",
accessType: "server",
});
expect(deleteUserResponse.status).toBe(200);
const userDeletedEvent = await Webhook.findWebhookAttempt(projectId, endpointId, svixToken, event => event.eventType === "user.deleted");
expect(userDeletedEvent).toMatchInlineSnapshot(`
{
"channels": null,
"eventId": null,
"eventType": "user.deleted",
"id": "<stripped svix message id>",
"payload": {
"data": {
"id": "<stripped UUID>",
"teams": [],
},
"type": "user.deleted",
},
"timestamp": <stripped field 'timestamp'>,
}
`);
});
it("should be able to properly disable primary_email_auth_enabled", async ({ expect }) => {
// Test for the fix where primary_email_auth_enabled couldn't be disabled properly
// due to an OR operator issue that always kept it true
// Create a user with email auth enabled
const response1 = await niceBackendFetch("/api/v1/users", {
accessType: "server",
method: "POST",
body: {
primary_email: backendContext.value.mailbox.emailAddress,
primary_email_auth_enabled: true,
display_name: "Test User",
},
});
expect(response1.status).toEqual(201);
expect(response1.body.primary_email_auth_enabled).toEqual(true);
const userId = response1.body.id;
// Update the user to disable email auth
const response2 = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
method: "PATCH",
body: {
primary_email_auth_enabled: false,
},
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": false,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "Test User",
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
"oauth_providers": [],
"otp_auth_enabled": false,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_auth_enabled": false,
"primary_email_verified": false,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"created_at_millis": <stripped field 'created_at_millis'>,
"display_name": "Test User's Team",
"id": "<stripped UUID>",
"profile_image_url": null,
"server_metadata": null,
},
"selected_team_id": "<stripped UUID>",
"server_metadata": null,
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
// Verify the change persisted by reading the user again
const response3 = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
});
expect(response3.status).toEqual(200);
expect(response3.body.primary_email_auth_enabled).toEqual(false);
expect(response3.body.auth_with_email).toEqual(false);
// Test that we can re-enable it
const response4 = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
method: "PATCH",
body: {
primary_email_auth_enabled: true,
},
});
expect(response4.status).toEqual(200);
expect(response4.body.primary_email_auth_enabled).toEqual(true);
expect(response4.body.auth_with_email).toEqual(false); // Still false because no password/otp is set
// Verify re-enabling persisted
const response5 = await niceBackendFetch("/api/v1/users/" + userId, {
accessType: "server",
});
expect(response5.status).toEqual(200);
expect(response5.body.primary_email_auth_enabled).toEqual(true);
});
it("should be able to disable primary_email_auth_enabled on current user", async ({ expect }) => {
// Test the same functionality when updating the current user via /me endpoint
await Auth.Otp.signIn();
// First verify the user has email auth enabled (from OTP sign in)
const initialResponse = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
});
expect(initialResponse.status).toEqual(200);
expect(initialResponse.body.primary_email_auth_enabled).toEqual(true);
// Disable email auth on current user
const response1 = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
primary_email_auth_enabled: false,
},
});
expect(response1.status).toEqual(200);
expect(response1.body.primary_email_auth_enabled).toEqual(false);
expect(response1.body.auth_with_email).toEqual(true); // May still be true due to existing auth methods
// Verify the change persisted by reading the user again
const response2 = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
});
expect(response2.status).toEqual(200);
expect(response2.body.primary_email_auth_enabled).toEqual(false);
// Re-enable email auth
const response3 = await niceBackendFetch("/api/v1/users/me", {
accessType: "server",
method: "PATCH",
body: {
primary_email_auth_enabled: true,
},
});
expect(response3.status).toEqual(200);
expect(response3.body.primary_email_auth_enabled).toEqual(true);
});
});
it.todo("creating a new user with an OAuth provider ID that does not exist should fail");
it.todo("creating a new user with password enabled when password sign in is disabled in the config should fail");