diff --git a/packages/console/src/pages/OrganizationDetails/Members/AddMembersToOrganization.tsx b/packages/console/src/pages/OrganizationDetails/Members/AddMembersToOrganization.tsx index d11c4bc09..b80877f56 100644 --- a/packages/console/src/pages/OrganizationDetails/Members/AddMembersToOrganization.tsx +++ b/packages/console/src/pages/OrganizationDetails/Members/AddMembersToOrganization.tsx @@ -50,7 +50,7 @@ function AddMembersToOrganization({ organization, isOpen, onClose }: Props) { await api.post(`api/organizations/${organization.id}/users/roles`, { json: { userIds: data.users.map(({ id }) => id), - roleIds: data.scopes.map(({ value }) => value), + organizationRoleIds: data.scopes.map(({ value }) => value), }, }); } diff --git a/packages/core/src/middleware/koa-auth/index.ts b/packages/core/src/middleware/koa-auth/index.ts index 38fffc659..1cb894570 100644 --- a/packages/core/src/middleware/koa-auth/index.ts +++ b/packages/core/src/middleware/koa-auth/index.ts @@ -58,7 +58,10 @@ export const verifyBearerTokenFromRequest = async ( const userId = request.headers['development-user-id']?.toString() ?? developmentUserId; if ((!isProduction || isIntegrationTest) && userId) { - consoleLog.warn(`Found dev user ID ${userId}, skip token validation.`); + // This log is distracting in integration tests. + if (!isIntegrationTest) { + consoleLog.warn(`Found dev user ID ${userId}, skip token validation.`); + } return { sub: userId, diff --git a/packages/core/src/routes/organization/index.ts b/packages/core/src/routes/organization/index.ts index 48db4af8a..eb711c419 100644 --- a/packages/core/src/routes/organization/index.ts +++ b/packages/core/src/routes/organization/index.ts @@ -67,16 +67,16 @@ export default function organizationRoutes(...args: Rout params: z.object({ id: z.string().min(1) }), body: z.object({ userIds: z.string().min(1).array().nonempty(), - roleIds: z.string().min(1).array().nonempty(), + organizationRoleIds: z.string().min(1).array().nonempty(), }), status: [201, 422], }), async (ctx, next) => { const { id } = ctx.guard.params; - const { userIds, roleIds } = ctx.guard.body; + const { userIds, organizationRoleIds } = ctx.guard.body; await organizations.relations.rolesUsers.insert( - ...roleIds.flatMap<[string, string, string]>((roleId) => + ...organizationRoleIds.flatMap<[string, string, string]>((roleId) => userIds.map<[string, string, string]>((userId) => [id, roleId, userId]) ) ); diff --git a/packages/integration-tests/src/api/organization.ts b/packages/integration-tests/src/api/organization.ts index 410672707..dd7adfd19 100644 --- a/packages/integration-tests/src/api/organization.ts +++ b/packages/integration-tests/src/api/organization.ts @@ -1,8 +1,19 @@ -import { type Role, type Organization, type OrganizationWithRoles } from '@logto/schemas'; +import { + type Role, + type Organization, + type OrganizationWithRoles, + type UserWithOrganizationRoles, +} from '@logto/schemas'; import { authedAdminApi } from './api.js'; import { ApiFactory } from './factory.js'; +type Query = { + q?: string; + page?: number; + page_size?: number; +}; + export class OrganizationApi extends ApiFactory< Organization, { name: string; description?: string } @@ -15,8 +26,12 @@ export class OrganizationApi extends ApiFactory< await authedAdminApi.post(`${this.path}/${id}/users`, { json: { userIds } }); } - async getUsers(id: string): Promise { - return authedAdminApi.get(`${this.path}/${id}/users`).json(); + async getUsers( + id: string, + query?: Query + ): Promise<[rows: UserWithOrganizationRoles[], totalCount: number]> { + const got = await authedAdminApi.get(`${this.path}/${id}/users`, { searchParams: query }); + return [JSON.parse(got.body), Number(got.headers['total-number'] ?? 0)]; } async deleteUser(id: string, userId: string): Promise { @@ -29,6 +44,12 @@ export class OrganizationApi extends ApiFactory< }); } + async addUsersRoles(id: string, userIds: string[], organizationRoleIds: string[]): Promise { + await authedAdminApi.post(`${this.path}/${id}/users/roles`, { + json: { userIds, organizationRoleIds }, + }); + } + async getUserRoles(id: string, userId: string): Promise { return authedAdminApi.get(`${this.path}/${id}/users/${userId}/roles`).json(); } diff --git a/packages/integration-tests/src/helpers/organization.ts b/packages/integration-tests/src/helpers/organization.ts index 8db245219..1ff7b6d30 100644 --- a/packages/integration-tests/src/helpers/organization.ts +++ b/packages/integration-tests/src/helpers/organization.ts @@ -14,7 +14,11 @@ import { OrganizationApi } from '#src/api/organization.js'; * delete them. */ export class OrganizationRoleApiTest extends OrganizationRoleApi { - protected roles: OrganizationRole[] = []; + #roles: OrganizationRole[] = []; + + get roles(): OrganizationRole[] { + return this.#roles; + } override async create(data: CreateOrganizationRolePostData): Promise { const created = await super.create(data); @@ -29,7 +33,7 @@ export class OrganizationRoleApiTest extends OrganizationRoleApi { async cleanUp(): Promise { // Use `trySafe` to avoid error when role is deleted by other tests. await Promise.all(this.roles.map(async (role) => trySafe(this.delete(role.id)))); - this.roles = []; + this.#roles = []; } } @@ -38,7 +42,11 @@ export class OrganizationRoleApiTest extends OrganizationRoleApi { * delete them. */ export class OrganizationScopeApiTest extends OrganizationScopeApi { - protected scopes: OrganizationScope[] = []; + #scopes: OrganizationScope[] = []; + + get scopes(): OrganizationScope[] { + return this.#scopes; + } override async create(data: { name: string; description?: string }): Promise { const created = await super.create(data); @@ -53,7 +61,7 @@ export class OrganizationScopeApiTest extends OrganizationScopeApi { async cleanUp(): Promise { // Use `trySafe` to avoid error when scope is deleted by other tests. await Promise.all(this.scopes.map(async (scope) => trySafe(this.delete(scope.id)))); - this.scopes = []; + this.#scopes = []; } } @@ -69,7 +77,11 @@ export class OrganizationApiTest extends OrganizationApi { roleApi = new OrganizationRoleApiTest(); scopeApi = new OrganizationScopeApiTest(); - protected organizations: Organization[] = []; + #organizations: Organization[] = []; + + get organizations(): Organization[] { + return this.#organizations; + } override async create(data: { name: string; description?: string }): Promise { const created = await super.create(data); @@ -89,7 +101,7 @@ export class OrganizationApiTest extends OrganizationApi { // Use `trySafe` to avoid error when organization is deleted by other tests. this.organizations.map(async (organization) => trySafe(this.delete(organization.id))) ); - this.organizations = []; + this.#organizations = []; } } /* eslint-enable @silverhand/fp/no-mutating-methods */ diff --git a/packages/integration-tests/src/helpers/user.ts b/packages/integration-tests/src/helpers/user.ts index c06ff7e29..86514d4bc 100644 --- a/packages/integration-tests/src/helpers/user.ts +++ b/packages/integration-tests/src/helpers/user.ts @@ -51,7 +51,11 @@ export const generateNewUser = async (options: }; export class UserApiTest { - protected users: User[] = []; + #users: User[] = []; + + get users(): User[] { + return this.#users; + } async create(data: CreateUserPayload): Promise { const user = await createUser(data); @@ -67,6 +71,6 @@ export class UserApiTest { async cleanUp(): Promise { // Use `trySafe` to avoid error when user is deleted by other tests. await Promise.all(this.users.map(async (user) => trySafe(deleteUser(user.id)))); - this.users = []; + this.#users = []; } } diff --git a/packages/integration-tests/src/tests/api/admin-user.search.test.ts b/packages/integration-tests/src/tests/api/admin-user.search.test.ts index 3f51986df..ae7ec3fdc 100644 --- a/packages/integration-tests/src/tests/api/admin-user.search.test.ts +++ b/packages/integration-tests/src/tests/api/admin-user.search.test.ts @@ -1,9 +1,12 @@ import type { IncomingHttpHeaders } from 'node:http'; -import type { User } from '@logto/schemas'; +import type { Role, User } from '@logto/schemas'; -import { authedAdminApi, deleteUser } from '#src/api/index.js'; +import { assignRolesToUser, authedAdminApi, createUser, deleteUser } from '#src/api/index.js'; +import { createRole, deleteRole } from '#src/api/role.js'; import { createUserByAdmin, expectRejects } from '#src/helpers/index.js'; +import { OrganizationApiTest } from '#src/helpers/organization.js'; +import { UserApiTest } from '#src/helpers/user.js'; const getUsers = async ( init: string[][] | Record | URLSearchParams @@ -235,3 +238,134 @@ describe('admin console user search params', () => { ]); }); }); + +describe('admin console user search params - excludeRoleId', () => { + const users: User[] = []; + const roles: Role[] = []; + const userPrefix = `search_exclude_role_`; + const rolePrefix = `role_`; + + beforeAll(async () => { + // Create users with different roles + // eslint-disable-next-line @silverhand/fp/no-mutating-methods + users.push( + ...(await Promise.all([ + createUser({ username: userPrefix + '1' }), + createUser({ username: userPrefix + '2' }), + createUser({ username: userPrefix + '3' }), + ])) + ); + // eslint-disable-next-line @silverhand/fp/no-mutating-methods + roles.push( + ...(await Promise.all([ + createRole({ name: rolePrefix + '1' }), + createRole({ name: rolePrefix + '2' }), + createRole({ name: rolePrefix + '3' }), + ])) + ); + + // Assign roles to users + await Promise.all([ + assignRolesToUser(users[0]!.id, [roles[0]!.id, roles[1]!.id]), + assignRolesToUser(users[1]!.id, [roles[1]!.id, roles[2]!.id]), + assignRolesToUser(users[2]!.id, [roles[2]!.id]), + ]); + }); + + afterAll(async () => { + await Promise.all(users.map(async ({ id }) => deleteUser(id))); + await Promise.all(roles.map(async ({ id }) => deleteRole(id))); + }); + + it('should be able to exclude users with a specific role (1)', async () => { + const { headers, json } = await getUsers([ + ['search.username', userPrefix + '%'], + ['excludeRoleId', roles[0]!.id], + ]); + + expect(headers['total-number']).toEqual('2'); + expect(json).toHaveLength(2); + expect(json).toContainEqual(expect.objectContaining({ id: users[1]!.id })); + expect(json).toContainEqual(expect.objectContaining({ id: users[2]!.id })); + }); + + it('should be able to exclude users with a specific role (2)', async () => { + const { headers, json } = await getUsers([ + ['search.username', userPrefix + '%'], + ['excludeRoleId', roles[1]!.id], + ]); + + expect(headers['total-number']).toEqual('1'); + expect(json).toHaveLength(1); + expect(json).toContainEqual(expect.objectContaining({ id: users[2]!.id })); + }); +}); + +describe('admin console user search params - excludeOrganizationId', () => { + const organizationApi = new OrganizationApiTest(); + const userApi = new UserApiTest(); + const organizationPrefix = `search_exclude_organization_`; + const userPrefix = `search_exclude_organization_`; + + beforeAll(async () => { + await Promise.all([ + organizationApi.create({ name: organizationPrefix + '1' }), + organizationApi.create({ name: organizationPrefix + '2' }), + organizationApi.create({ name: organizationPrefix + '3' }), + ]); + + await Promise.all([ + userApi.create({ username: userPrefix + '1' }), + userApi.create({ username: userPrefix + '2' }), + userApi.create({ username: userPrefix + '3' }), + ]); + + const { organizations } = organizationApi; + const { users } = userApi; + + await Promise.all([ + organizationApi.addUsers(organizations[0]!.id, [users[0]!.id, users[1]!.id]), + organizationApi.addUsers(organizations[1]!.id, [users[1]!.id, users[2]!.id]), + organizationApi.addUsers(organizations[2]!.id, [users[2]!.id]), + ]); + }); + + afterAll(async () => { + await organizationApi.cleanUp(); + await userApi.cleanUp(); + }); + + it('should be able to exclude users with a specific organization (1)', async () => { + const { headers, json } = await getUsers([ + ['search.username', userPrefix + '%'], + ['excludeOrganizationId', organizationApi.organizations[0]!.id], + ]); + + expect(headers['total-number']).toEqual('1'); + expect(json).toHaveLength(1); + expect(json).toContainEqual(expect.objectContaining({ id: userApi.users[2]!.id })); + }); + + it('should be able to exclude users with a specific organization (2)', async () => { + const { headers, json } = await getUsers([ + ['search.username', userPrefix + '%'], + ['excludeOrganizationId', organizationApi.organizations[1]!.id], + ]); + + expect(headers['total-number']).toEqual('1'); + expect(json).toHaveLength(1); + expect(json).toContainEqual(expect.objectContaining({ id: userApi.users[0]!.id })); + }); + + it('should be able to exclude users with a specific organization (3)', async () => { + const { headers, json } = await getUsers([ + ['search.username', userPrefix + '%'], + ['excludeOrganizationId', organizationApi.organizations[2]!.id], + ]); + + expect(headers['total-number']).toEqual('2'); + expect(json).toHaveLength(2); + expect(json).toContainEqual(expect.objectContaining({ id: userApi.users[0]!.id })); + expect(json).toContainEqual(expect.objectContaining({ id: userApi.users[1]!.id })); + }); +}); diff --git a/packages/integration-tests/src/tests/api/organization-user.test.ts b/packages/integration-tests/src/tests/api/organization-user.test.ts new file mode 100644 index 000000000..962209526 --- /dev/null +++ b/packages/integration-tests/src/tests/api/organization-user.test.ts @@ -0,0 +1,231 @@ +import assert from 'node:assert'; + +import { HTTPError } from 'got'; + +import { OrganizationApiTest } from '#src/helpers/organization.js'; +import { UserApiTest } from '#src/helpers/user.js'; +import { generateTestName } from '#src/utils.js'; + +describe('organization user APIs', () => { + describe('organization get users', () => { + const organizationApi = new OrganizationApiTest(); + const userApi = new UserApiTest(); + + beforeAll(async () => { + const organization = await organizationApi.create({ name: 'test' }); + const createdUsers = await Promise.all( + Array.from({ length: 30 }).map(async () => userApi.create({ username: generateTestName() })) + ); + await organizationApi.addUsers( + organization.id, + createdUsers.map((user) => user.id) + ); + }); + + afterAll(async () => { + await Promise.all([organizationApi.cleanUp(), userApi.cleanUp()]); + }); + + it('should be able to get organization users with pagination', async () => { + const organizationId = organizationApi.organizations[0]!.id; + const [users1, total1] = await organizationApi.getUsers(organizationId, { + page: 1, + page_size: 20, + }); + const [users2, total2] = await organizationApi.getUsers(organizationId, { + page: 2, + page_size: 10, + }); + expect(users2.length).toBeGreaterThanOrEqual(10); + expect(users2[0]?.id).not.toBeFalsy(); + expect(users2[0]?.id).toBe(users1[10]?.id); + expect(total1).toBe(30); + expect(total2).toBe(30); + }); + + it('should be able to get organization users with search keyword', async () => { + const organizationId = organizationApi.organizations[0]!.id; + const username = generateTestName(); + const createdUser = await userApi.create({ username }); + + await organizationApi.addUsers(organizationId, [createdUser.id]); + const [users] = await organizationApi.getUsers(organizationId, { + q: username, + }); + expect(users).toHaveLength(1); + expect(users[0]).toMatchObject(createdUser); + }); + + it('should be able to get organization users with their roles', async () => { + const organizationId = organizationApi.organizations[0]!.id; + const user = userApi.users[0]!; + + const roles = await Promise.all([ + organizationApi.roleApi.create({ name: generateTestName() }), + organizationApi.roleApi.create({ name: generateTestName() }), + ]); + const roleIds = roles.map(({ id }) => id); + await organizationApi.addUserRoles(organizationId, user.id, roleIds); + + const [usersWithRoles] = await organizationApi.getUsers(organizationId, { + q: user.username!, + }); + expect(usersWithRoles).toHaveLength(1); + expect(usersWithRoles[0]).toMatchObject(user); + expect(usersWithRoles[0]!.organizationRoles).toHaveLength(2); + expect(usersWithRoles[0]!.organizationRoles).toContainEqual( + expect.objectContaining({ id: roles[0].id }) + ); + expect(usersWithRoles[0]!.organizationRoles).toContainEqual( + expect.objectContaining({ id: roles[1].id }) + ); + }); + }); + + describe('organization - user relations', () => { + const organizationApi = new OrganizationApiTest(); + const userApi = new UserApiTest(); + + afterEach(async () => { + await Promise.all([organizationApi.cleanUp(), userApi.cleanUp()]); + }); + + it('should fail when try to add empty user list', async () => { + const organization = await organizationApi.create({ name: 'test' }); + const response = await organizationApi + .addUsers(organization.id, []) + .catch((error: unknown) => error); + expect(response instanceof HTTPError && response.response.statusCode).toBe(400); + }); + + it('should fail when try to add user to an organization that does not exist', async () => { + const response = await organizationApi.addUsers('0', ['0']).catch((error: unknown) => error); + assert(response instanceof HTTPError); + expect(response.response.statusCode).toBe(422); + expect(JSON.parse(String(response.response.body))).toMatchObject( + expect.objectContaining({ code: 'entity.relation_foreign_key_not_found' }) + ); + }); + + it('should be able to delete organization user', async () => { + const organization = await organizationApi.create({ name: 'test' }); + const user = await userApi.create({ username: generateTestName() }); + + await organizationApi.addUsers(organization.id, [user.id]); + await organizationApi.deleteUser(organization.id, user.id); + const users = await organizationApi.getUsers(organization.id); + expect(users).not.toContainEqual(user); + }); + + it('should fail when try to delete user from an organization that does not exist', async () => { + const response = await organizationApi.deleteUser('0', '0').catch((error: unknown) => error); + assert(response instanceof HTTPError); + expect(response.response.statusCode).toBe(404); + }); + }); + + describe('organization - user - organization role relation routes', () => { + const organizationApi = new OrganizationApiTest(); + const { roleApi } = organizationApi; + const userApi = new UserApiTest(); + + afterEach(async () => { + await Promise.all([organizationApi.cleanUp(), userApi.cleanUp()]); + }); + + it("should be able to add and get user's organization roles", async () => { + const organization = await organizationApi.create({ name: 'test' }); + const user = await userApi.create({ username: generateTestName() }); + const [role1, role2] = await Promise.all([ + roleApi.create({ name: generateTestName() }), + roleApi.create({ name: generateTestName() }), + ]); + + const response = await organizationApi + .addUserRoles(organization.id, user.id, [role1.id, role2.id]) + .catch((error: unknown) => error); + + assert(response instanceof HTTPError); + expect(response.response.statusCode).toBe(422); + expect(JSON.parse(String(response.response.body))).toMatchObject( + expect.objectContaining({ code: 'organization.require_membership' }) + ); + + await organizationApi.addUsers(organization.id, [user.id]); + await organizationApi.addUserRoles(organization.id, user.id, [role1.id, role2.id]); + const roles = await organizationApi.getUserRoles(organization.id, user.id); + expect(roles).toContainEqual(expect.objectContaining({ id: role1.id })); + expect(roles).toContainEqual(expect.objectContaining({ id: role2.id })); + }); + + it('should be able to get all organizations with roles for a user', async () => { + const [organization1, organization2] = await Promise.all([ + organizationApi.create({ name: 'test' }), + organizationApi.create({ name: 'test' }), + ]); + const user = await userApi.create({ username: generateTestName() }); + const [role1, role2] = await Promise.all([ + roleApi.create({ name: generateTestName() }), + roleApi.create({ name: generateTestName() }), + ]); + + await organizationApi.addUsers(organization1.id, [user.id]); + await organizationApi.addUserRoles(organization1.id, user.id, [role1.id]); + await organizationApi.addUsers(organization2.id, [user.id]); + await organizationApi.addUserRoles(organization2.id, user.id, [role1.id, role2.id]); + + const organizations = await organizationApi.getUserOrganizations(user.id); + + // Check organization 1 and ensure it only has role 1 + const organization1WithRoles = organizations.find((org) => org.id === organization1.id); + assert(organization1WithRoles); + expect(organization1WithRoles.id).toBe(organization1.id); + expect(organization1WithRoles.organizationRoles).toContainEqual( + expect.objectContaining({ id: role1.id }) + ); + expect(organization1WithRoles.organizationRoles).not.toContainEqual( + expect.objectContaining({ id: role2.id }) + ); + + // Check organization 2 and ensure it has both role 1 and role 2 + const organization2WithRoles = organizations.find((org) => org.id === organization2.id); + assert(organization2WithRoles); + expect(organization2WithRoles.id).toBe(organization2.id); + expect(organization2WithRoles.organizationRoles).toContainEqual( + expect.objectContaining({ id: role1.id }) + ); + expect(organization2WithRoles.organizationRoles).toContainEqual( + expect.objectContaining({ id: role2.id }) + ); + }); + + it('should be able to assign multiple roles to multiple users', async () => { + const organization = await organizationApi.create({ name: 'test' }); + const [user1, user2] = await Promise.all([ + userApi.create({ username: generateTestName() }), + userApi.create({ username: generateTestName() }), + ]); + const [role1, role2] = await Promise.all([ + roleApi.create({ name: generateTestName() }), + roleApi.create({ name: generateTestName() }), + ]); + + await organizationApi.addUsers(organization.id, [user1.id, user2.id]); + await organizationApi.addUsersRoles( + organization.id, + [user1.id, user2.id], + [role1.id, role2.id] + ); + + const [user1Roles, user2Roles] = await Promise.all([ + organizationApi.getUserRoles(organization.id, user1.id), + organizationApi.getUserRoles(organization.id, user2.id), + ]); + + expect(user1Roles).toContainEqual(expect.objectContaining({ id: role1.id })); + expect(user1Roles).toContainEqual(expect.objectContaining({ id: role2.id })); + expect(user2Roles).toContainEqual(expect.objectContaining({ id: role1.id })); + expect(user2Roles).toContainEqual(expect.objectContaining({ id: role2.id })); + }); + }); +}); diff --git a/packages/integration-tests/src/tests/api/organization.test.ts b/packages/integration-tests/src/tests/api/organization.test.ts index 10c95a5cd..8fc468bb8 100644 --- a/packages/integration-tests/src/tests/api/organization.test.ts +++ b/packages/integration-tests/src/tests/api/organization.test.ts @@ -1,10 +1,7 @@ -import assert from 'node:assert'; - import { generateStandardId } from '@logto/shared'; import { HTTPError } from 'got'; import { OrganizationApiTest } from '#src/helpers/organization.js'; -import { UserApiTest } from '#src/helpers/user.js'; const randomId = () => generateStandardId(4); @@ -90,135 +87,4 @@ describe('organization APIs', () => { expect(response instanceof HTTPError && response.response.statusCode).toBe(404); }); }); - - describe('organization - user relations', () => { - const organizationApi = new OrganizationApiTest(); - const userApi = new UserApiTest(); - - afterEach(async () => { - await Promise.all([organizationApi.cleanUp(), userApi.cleanUp()]); - }); - - it('should be able to add and get organization users', async () => { - const organization = await organizationApi.create({ name: 'test' }); - const [user1, user2] = await Promise.all([ - userApi.create({ username: 'test' + randomId() }), - userApi.create({ username: 'test' + randomId() }), - ]); - - await organizationApi.addUsers(organization.id, [user1.id, user2.id]); - const users = await organizationApi.getUsers(organization.id); - expect(users).toContainEqual(expect.objectContaining({ id: user1.id })); - expect(users).toContainEqual(expect.objectContaining({ id: user2.id })); - }); - - it('should fail when try to add empty user list', async () => { - const organization = await organizationApi.create({ name: 'test' }); - const response = await organizationApi - .addUsers(organization.id, []) - .catch((error: unknown) => error); - expect(response instanceof HTTPError && response.response.statusCode).toBe(400); - }); - - it('should fail when try to add user to an organization that does not exist', async () => { - const response = await organizationApi.addUsers('0', ['0']).catch((error: unknown) => error); - assert(response instanceof HTTPError); - expect(response.response.statusCode).toBe(422); - expect(JSON.parse(String(response.response.body))).toMatchObject( - expect.objectContaining({ code: 'entity.relation_foreign_key_not_found' }) - ); - }); - - it('should be able to delete organization user', async () => { - const organization = await organizationApi.create({ name: 'test' }); - const user = await userApi.create({ username: 'test' + randomId() }); - - await organizationApi.addUsers(organization.id, [user.id]); - await organizationApi.deleteUser(organization.id, user.id); - const users = await organizationApi.getUsers(organization.id); - expect(users).not.toContainEqual(user); - }); - - it('should fail when try to delete user from an organization that does not exist', async () => { - const response = await organizationApi.deleteUser('0', '0').catch((error: unknown) => error); - assert(response instanceof HTTPError); - expect(response.response.statusCode).toBe(404); - }); - }); - - describe('organization - user - organization role relation routes', () => { - const organizationApi = new OrganizationApiTest(); - const { roleApi } = organizationApi; - const userApi = new UserApiTest(); - - afterEach(async () => { - await Promise.all([organizationApi.cleanUp(), userApi.cleanUp()]); - }); - - it("should be able to add and get user's organization roles", async () => { - const organization = await organizationApi.create({ name: 'test' }); - const user = await userApi.create({ username: 'test' + randomId() }); - const [role1, role2] = await Promise.all([ - roleApi.create({ name: 'test' + randomId() }), - roleApi.create({ name: 'test' + randomId() }), - ]); - - const response = await organizationApi - .addUserRoles(organization.id, user.id, [role1.id, role2.id]) - .catch((error: unknown) => error); - - assert(response instanceof HTTPError); - expect(response.response.statusCode).toBe(422); - expect(JSON.parse(String(response.response.body))).toMatchObject( - expect.objectContaining({ code: 'organization.require_membership' }) - ); - - await organizationApi.addUsers(organization.id, [user.id]); - await organizationApi.addUserRoles(organization.id, user.id, [role1.id, role2.id]); - const roles = await organizationApi.getUserRoles(organization.id, user.id); - expect(roles).toContainEqual(expect.objectContaining({ id: role1.id })); - expect(roles).toContainEqual(expect.objectContaining({ id: role2.id })); - }); - - it('should be able to get all organizations with roles for a user', async () => { - const [organization1, organization2] = await Promise.all([ - organizationApi.create({ name: 'test' }), - organizationApi.create({ name: 'test' }), - ]); - const user = await userApi.create({ username: 'test' + randomId() }); - const [role1, role2] = await Promise.all([ - roleApi.create({ name: 'test' + randomId() }), - roleApi.create({ name: 'test' + randomId() }), - ]); - - await organizationApi.addUsers(organization1.id, [user.id]); - await organizationApi.addUserRoles(organization1.id, user.id, [role1.id]); - await organizationApi.addUsers(organization2.id, [user.id]); - await organizationApi.addUserRoles(organization2.id, user.id, [role1.id, role2.id]); - - const organizations = await organizationApi.getUserOrganizations(user.id); - - // Check organization 1 and ensure it only has role 1 - const organization1WithRoles = organizations.find((org) => org.id === organization1.id); - assert(organization1WithRoles); - expect(organization1WithRoles.id).toBe(organization1.id); - expect(organization1WithRoles.organizationRoles).toContainEqual( - expect.objectContaining({ id: role1.id }) - ); - expect(organization1WithRoles.organizationRoles).not.toContainEqual( - expect.objectContaining({ id: role2.id }) - ); - - // Check organization 2 and ensure it has both role 1 and role 2 - const organization2WithRoles = organizations.find((org) => org.id === organization2.id); - assert(organization2WithRoles); - expect(organization2WithRoles.id).toBe(organization2.id); - expect(organization2WithRoles.organizationRoles).toContainEqual( - expect.objectContaining({ id: role1.id }) - ); - expect(organization2WithRoles.organizationRoles).toContainEqual( - expect.objectContaining({ id: role2.id }) - ); - }); - }); }); diff --git a/packages/integration-tests/src/utils.ts b/packages/integration-tests/src/utils.ts index eec26c36f..7405d8db0 100644 --- a/packages/integration-tests/src/utils.ts +++ b/packages/integration-tests/src/utils.ts @@ -1,6 +1,7 @@ import crypto from 'node:crypto'; import path from 'node:path'; +import { generateStandardId } from '@logto/shared'; import { assert } from '@silverhand/essentials'; import { type Page } from 'puppeteer'; @@ -105,3 +106,13 @@ export const cls = (className: C) => `[class*=_${className}]` * @see {@link cls} */ export const dcls = (className: C) => `div${cls(className)}` as const; + +/** + * Generate a random test name that starts with `test_` and followed by 4 random characters. + * + * @example + * ```ts + * generateTestName() // => 'test_abc1' + * ``` + */ +export const generateTestName = () => `test_${generateStandardId(4)}`;