From 128ee0c9bb97a8b1695a386d661c480754e9f0cf Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sat, 8 Jun 2024 14:48:57 +0800 Subject: [PATCH] chore: add tests --- .../integration-tests/src/api/organization.ts | 6 +- .../integration-tests/src/client/index.ts | 8 ++ .../src/helpers/organization.ts | 8 +- .../tests/api/oidc/get-access-token.test.ts | 2 +- .../tests/api/oidc/organization-token.test.ts | 115 ++++++++++++++++++ 5 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 packages/integration-tests/src/tests/api/oidc/organization-token.test.ts diff --git a/packages/integration-tests/src/api/organization.ts b/packages/integration-tests/src/api/organization.ts index 660406c5a..9fc1682fd 100644 --- a/packages/integration-tests/src/api/organization.ts +++ b/packages/integration-tests/src/api/organization.ts @@ -6,6 +6,7 @@ import { type OrganizationWithFeatured, type OrganizationScope, type OrganizationEmailDomain, + type CreateOrganization, } from '@logto/schemas'; import { authedAdminApi } from './api.js'; @@ -17,10 +18,7 @@ type Query = { page_size?: number; }; -export class OrganizationApi extends ApiFactory< - Organization, - { name: string; description?: string } -> { +export class OrganizationApi extends ApiFactory> { constructor() { super('organizations'); } diff --git a/packages/integration-tests/src/client/index.ts b/packages/integration-tests/src/client/index.ts index d10a9bdd7..6c0569708 100644 --- a/packages/integration-tests/src/client/index.ts +++ b/packages/integration-tests/src/client/index.ts @@ -152,6 +152,14 @@ export default class MockClient { return this.logto.getAccessTokenClaims(resource); } + public async getOrganizationTokenClaims(organizationId: string) { + return this.logto.getOrganizationTokenClaims(organizationId); + } + + public async clearAccessToken() { + return this.logto.clearAccessToken(); + } + public async getRefreshToken(): Promise> { return this.logto.getRefreshToken(); } diff --git a/packages/integration-tests/src/helpers/organization.ts b/packages/integration-tests/src/helpers/organization.ts index d45ef5d44..c98fdea08 100644 --- a/packages/integration-tests/src/helpers/organization.ts +++ b/packages/integration-tests/src/helpers/organization.ts @@ -4,7 +4,7 @@ import { type Organization, type OrganizationRoleWithScopes, type OrganizationInvitationEntity, - type JsonObject, + type CreateOrganization, } from '@logto/schemas'; import { trySafe } from '@silverhand/essentials'; @@ -123,11 +123,7 @@ export class OrganizationApiTest extends OrganizationApi { return this.#organizations; } - override async create(data: { - name: string; - description?: string; - customData?: JsonObject; - }): Promise { + override async create(data: Omit): Promise { const created = await super.create(data); this.organizations.push(created); return created; diff --git a/packages/integration-tests/src/tests/api/oidc/get-access-token.test.ts b/packages/integration-tests/src/tests/api/oidc/get-access-token.test.ts index 5857de7ca..5129ae95a 100644 --- a/packages/integration-tests/src/tests/api/oidc/get-access-token.test.ts +++ b/packages/integration-tests/src/tests/api/oidc/get-access-token.test.ts @@ -152,7 +152,7 @@ describe('get access token', () => { ).resolves.toBeTruthy(); }); - it('can sign in and get multiple Access Tokens by the same Refresh Token within refreshTokenReuseInterval', async () => { + it('can sign in and get multiple access tokens by the same refresh token within `refreshTokenReuseInterval`', async () => { const client = new MockClient({ resources: [testApiResourceInfo.indicator] }); await client.initSession(); diff --git a/packages/integration-tests/src/tests/api/oidc/organization-token.test.ts b/packages/integration-tests/src/tests/api/oidc/organization-token.test.ts new file mode 100644 index 000000000..6bf9a433e --- /dev/null +++ b/packages/integration-tests/src/tests/api/oidc/organization-token.test.ts @@ -0,0 +1,115 @@ +import { UserScope, buildOrganizationUrn } from '@logto/core-kit'; +import { InteractionEvent, MfaFactor } from '@logto/schemas'; + +import { createUserMfaVerification, deleteUser } from '#src/api/admin-user.js'; +import { putInteraction } from '#src/api/index.js'; +import MockClient from '#src/client/index.js'; +import { processSession } from '#src/helpers/client.js'; +import { createUserByAdmin } from '#src/helpers/index.js'; +import { OrganizationApiTest } from '#src/helpers/organization.js'; +import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { generatePassword, generateUsername, randomString } from '#src/utils.js'; + +describe('get access token for organization', () => { + const username = generateUsername(); + const password = generatePassword(); + const scopeName = `read:${randomString()}`; + const scopeName2 = `read:other:${randomString()}`; + const client = new MockClient({ + scopes: [scopeName, scopeName2, UserScope.Organizations], + }); + + /* eslint-disable @silverhand/fp/no-let */ + let testApiScopeId: string; + let testApiScopeId2: string; + let testUserId: string; + let testOrganizationId: string; + let testOrganizationId2: string; + /* eslint-enable @silverhand/fp/no-let */ + + const organizationApi = new OrganizationApiTest(); + + /* eslint-disable @silverhand/fp/no-mutation */ + beforeAll(async () => { + const user = await createUserByAdmin({ username, password }); + testUserId = user.id; + + const organization = await organizationApi.create({ name: 'org1' }); + testOrganizationId = organization.id; + await organizationApi.addUsers(testOrganizationId, [user.id]); + + const scope = await organizationApi.scopeApi.create({ name: scopeName }); + testApiScopeId = scope.id; + const scope2 = await organizationApi.scopeApi.create({ name: scopeName2 }); + testApiScopeId2 = scope2.id; + + const role = await organizationApi.roleApi.create({ name: `role1:${randomString()}` }); + await organizationApi.roleApi.addScopes(role.id, [scope.id]); + await organizationApi.addUserRoles(testOrganizationId, user.id, [role.id]); + + const organization2 = await organizationApi.create({ name: 'org2' }); + testOrganizationId2 = organization2.id; + await organizationApi.addUsers(testOrganizationId2, [user.id]); + const role2 = await organizationApi.roleApi.create({ name: `role2:${randomString()}` }); + await organizationApi.roleApi.addScopes(role2.id, [scope2.id]); + await organizationApi.addUserRoles(testOrganizationId2, user.id, [role2.id]); + + await enableAllPasswordSignInMethods(); + + // Prepare client + await client.initSession(); + await client.successSend(putInteraction, { + event: InteractionEvent.SignIn, + identifier: { username, password }, + }); + const { redirectTo } = await client.submitInteraction(); + await processSession(client, redirectTo); + }); + /* eslint-enable @silverhand/fp/no-mutation */ + + afterAll(async () => { + await Promise.all([organizationApi.cleanUp(), deleteUser(testUserId)]); + }); + + it('should be able to get access token for organization with correct scopes', async () => { + await expect(client.getOrganizationTokenClaims(testOrganizationId)).resolves.toMatchObject({ + aud: buildOrganizationUrn(testOrganizationId), + scope: scopeName, + }); + await expect(client.getOrganizationTokenClaims(testOrganizationId2)).resolves.toMatchObject({ + aud: buildOrganizationUrn(testOrganizationId2), + scope: scopeName2, + }); + }); + + it('should be able to dynamically get access token according to the status quo', async () => { + const newOrganization = await organizationApi.create({ name: 'foo' }); + + await organizationApi.addUsers(newOrganization.id, [testUserId]); + await expect(client.getOrganizationTokenClaims(newOrganization.id)).resolves.toMatchObject({ + aud: buildOrganizationUrn(newOrganization.id), + }); + + await organizationApi.deleteUser(newOrganization.id, testUserId); + await client.clearAccessToken(); + await expect( + client.getOrganizationTokenClaims(newOrganization.id) + ).rejects.toMatchInlineSnapshot('[Error: Access denied.]'); + }); + + it('should throw when organization requires mfa but user has not configured', async () => { + await organizationApi.update(testOrganizationId, { isMfaRequired: true }); + await client.clearAccessToken(); + + await expect( + client.getOrganizationTokenClaims(testOrganizationId) + ).rejects.toMatchInlineSnapshot('[Error: Access denied.]'); + }); + + it('should be able to get access token for organization when user has mfa configured', async () => { + await createUserMfaVerification(testUserId, MfaFactor.TOTP); + await expect(client.getOrganizationTokenClaims(testOrganizationId)).resolves.toMatchObject({ + aud: buildOrganizationUrn(testOrganizationId), + }); + }); +});