mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-27 21:01:03 +08:00
2474 lines
88 KiB
TypeScript
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");
|