0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

Merge pull request #4738 from logto-io/gao-refactor-integration-tests

refactor(test): manage test resource lifecycle
This commit is contained in:
Gao Sun 2023-10-24 23:43:50 -05:00 committed by GitHub
commit 7c4f0cd56f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 188 additions and 90 deletions

View file

@ -2,7 +2,7 @@ import type { Identities, Role, User } from '@logto/schemas';
import { authedAdminApi } from './api.js';
type CreateUserPayload = Partial<{
export type CreateUserPayload = Partial<{
primaryEmail: string;
primaryPhone: string;
username: string;

View file

@ -7,9 +7,15 @@ import {
import { authedAdminApi } from './api.js';
import { ApiFactory } from './factory.js';
class OrganizationRoleApi extends ApiFactory<
export type CreateOrganizationRolePostData = {
name: string;
description?: string;
organizationScopeIds?: string[];
};
export class OrganizationRoleApi extends ApiFactory<
OrganizationRole,
{ name: string; description?: string; organizationScopeIds?: string[] }
CreateOrganizationRolePostData
> {
constructor() {
super('organization-roles');
@ -41,5 +47,3 @@ class OrganizationRoleApi extends ApiFactory<
await authedAdminApi.delete(`${this.path}/${id}/scopes/${scopeId}`);
}
}
export const roleApi = new OrganizationRoleApi();

View file

@ -2,7 +2,7 @@ import { type OrganizationScope } from '@logto/schemas';
import { ApiFactory } from './factory.js';
class OrganizationScopeApi extends ApiFactory<
export class OrganizationScopeApi extends ApiFactory<
OrganizationScope,
{ name: string; description?: string }
> {
@ -10,6 +10,3 @@ class OrganizationScopeApi extends ApiFactory<
super('organization-scopes');
}
}
/** API methods for operating organization template scopes. */
export const scopeApi = new OrganizationScopeApi();

View file

@ -3,7 +3,10 @@ import { type Role, type Organization, type OrganizationWithRoles } from '@logto
import { authedAdminApi } from './api.js';
import { ApiFactory } from './factory.js';
class OrganizationApi extends ApiFactory<Organization, { name: string; description?: string }> {
export class OrganizationApi extends ApiFactory<
Organization,
{ name: string; description?: string }
> {
constructor() {
super('organizations');
}
@ -38,6 +41,3 @@ class OrganizationApi extends ApiFactory<Organization, { name: string; descripti
return authedAdminApi.get(`users/${userId}/organizations`).json<OrganizationWithRoles[]>();
}
}
/** API methods for operating organizations. */
export const organizationApi = new OrganizationApi();

View file

@ -0,0 +1,95 @@
import { type OrganizationScope, type OrganizationRole, type Organization } from '@logto/schemas';
import { trySafe } from '@silverhand/essentials';
import {
type CreateOrganizationRolePostData,
OrganizationRoleApi,
} from '#src/api/organization-role.js';
import { OrganizationScopeApi } from '#src/api/organization-scope.js';
import { OrganizationApi } from '#src/api/organization.js';
/* eslint-disable @silverhand/fp/no-mutating-methods */
/**
* A help class that records the created organization roles, and provides a `cleanUp` method to
* delete them.
*/
export class OrganizationRoleApiTest extends OrganizationRoleApi {
protected roles: OrganizationRole[] = [];
override async create(data: CreateOrganizationRolePostData): Promise<OrganizationRole> {
const created = await super.create(data);
this.roles.push(created);
return created;
}
/**
* Delete all created roles. This method will ignore errors when deleting roles to avoid error
* when they are deleted by other tests.
*/
async cleanUp(): Promise<void> {
// 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 = [];
}
}
/**
* A help class that records the created organization scopes, and provides a `cleanUp` method to
* delete them.
*/
export class OrganizationScopeApiTest extends OrganizationScopeApi {
protected scopes: OrganizationScope[] = [];
override async create(data: { name: string; description?: string }): Promise<OrganizationScope> {
const created = await super.create(data);
this.scopes.push(created);
return created;
}
/**
* Delete all created scopes. This method will ignore errors when deleting scopes to avoid error
* when they are deleted by other tests.
*/
async cleanUp(): Promise<void> {
// 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 = [];
}
}
/**
* A help class that records the created organizations, and provides a `cleanUp` method to
* delete them. It also provides `roleApi` and `scopeApi` to manage the organization roles and
* scopes.
*
* @see OrganizationRoleApiTest for more information about `roleApi`.
* @see OrganizationScopeApiTest for more information about `scopeApi`.
*/
export class OrganizationApiTest extends OrganizationApi {
roleApi = new OrganizationRoleApiTest();
scopeApi = new OrganizationScopeApiTest();
protected organizations: Organization[] = [];
override async create(data: { name: string; description?: string }): Promise<Organization> {
const created = await super.create(data);
this.organizations.push(created);
return created;
}
/**
* Delete all created organizations, roles and scopes. No need to call `cleanUp` of `roleApi` and
* `scopeApi` manually.
*
* This method will ignore errors when deleting organizations, roles and scopes to avoid error
* when they are deleted by other tests.
*/
async cleanUp(): Promise<void> {
await Promise.all(
// Use `trySafe` to avoid error when organization is deleted by other tests.
this.organizations.map(async (organization) => trySafe(this.delete(organization.id)))
);
this.organizations = [];
}
}
/* eslint-enable @silverhand/fp/no-mutating-methods */

View file

@ -1,4 +1,7 @@
import { createUser } from '#src/api/index.js';
import { type User } from '@logto/schemas';
import { trySafe } from '@silverhand/essentials';
import { type CreateUserPayload, createUser, deleteUser } from '#src/api/index.js';
import {
generateUsername,
generateEmail,
@ -46,3 +49,24 @@ export const generateNewUser = async <T extends NewUserProfileOptions>(options:
return { user, userProfile };
};
export class UserApiTest {
protected users: User[] = [];
async create(data: CreateUserPayload): Promise<User> {
const user = await createUser(data);
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
this.users.push(user);
return user;
}
/**
* Delete all created users. This method will ignore errors when deleting users to avoid error
* when they are deleted by other tests.
*/
async cleanUp(): Promise<void> {
// 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 = [];
}
}

View file

@ -4,17 +4,22 @@ import { generateStandardId } from '@logto/shared';
import { isKeyInObject } from '@silverhand/essentials';
import { HTTPError } from 'got';
import { roleApi } from '#src/api/organization-role.js';
import { scopeApi } from '#src/api/organization-scope.js';
import { OrganizationRoleApiTest, OrganizationScopeApiTest } from '#src/helpers/organization.js';
const randomId = () => generateStandardId(4);
// Add additional layer of describe to run tests in band
describe('organization role APIs', () => {
describe('organization roles', () => {
const roleApi = new OrganizationRoleApiTest();
afterEach(async () => {
await roleApi.cleanUp();
});
it('should fail if the name of the new organization role already exists', async () => {
const name = 'test' + randomId();
const createdRole = await roleApi.create({ name });
await roleApi.create({ name });
const response = await roleApi.create({ name }).catch((error: unknown) => error);
assert(response instanceof HTTPError);
@ -23,13 +28,11 @@ describe('organization role APIs', () => {
const body: unknown = JSON.parse(String(raw));
expect(statusCode).toBe(422);
expect(isKeyInObject(body, 'code') && body.code).toBe('entity.unique_integrity_violation');
await roleApi.delete(createdRole.id);
});
it('should get organization roles successfully', async () => {
const [name1, name2] = ['test' + randomId(), 'test' + randomId()];
const createdRoles = await Promise.all([
await Promise.all([
roleApi.create({ name: name1, description: 'A test organization role.' }),
roleApi.create({ name: name2 }),
]);
@ -39,13 +42,11 @@ describe('organization role APIs', () => {
expect.objectContaining({ name: name1, description: 'A test organization role.' })
);
expect(roles).toContainEqual(expect.objectContaining({ name: name2, description: null }));
await Promise.all(createdRoles.map(async (role) => roleApi.delete(role.id)));
});
it('should get organization roles with pagination', async () => {
// Add 20 roles to exceed the default page size
const allRoles = await Promise.all(
await Promise.all(
Array.from({ length: 30 }).map(async () => roleApi.create({ name: 'test' + randomId() }))
);
@ -61,8 +62,6 @@ describe('organization role APIs', () => {
expect(roles2.length).toBeGreaterThanOrEqual(10);
expect(roles2[0]?.id).not.toBeFalsy();
expect(roles2[0]?.id).toBe(roles[10]?.id);
await Promise.all(allRoles.map(async (role) => roleApi.delete(role.id)));
});
it('should be able to create and get organization roles by id', async () => {
@ -70,7 +69,6 @@ describe('organization role APIs', () => {
const { scopes, ...role } = await roleApi.get(createdRole.id);
expect(role).toStrictEqual(createdRole);
await roleApi.delete(createdRole.id);
});
it('should fail when try to get an organization role that does not exist', async () => {
@ -91,7 +89,6 @@ describe('organization role APIs', () => {
name: newName,
description: 'test description.',
});
await roleApi.delete(createdRole.id);
});
it('should be able to delete organization role', async () => {
@ -108,6 +105,13 @@ describe('organization role APIs', () => {
});
describe('organization role - scope relations', () => {
const roleApi = new OrganizationRoleApiTest();
const scopeApi = new OrganizationScopeApiTest();
afterEach(async () => {
await Promise.all([roleApi.cleanUp(), scopeApi.cleanUp()]);
});
it('should be able to add and get scopes of a role', async () => {
const [role, scope1, scope2] = await Promise.all([
roleApi.create({ name: 'test' + randomId() }),
@ -127,12 +131,6 @@ describe('organization role APIs', () => {
name: scope2.name,
})
);
await Promise.all([
roleApi.delete(role.id),
scopeApi.delete(scope1.id),
scopeApi.delete(scope2.id),
]);
});
it('should fail when try to add non-existent scopes to a role', async () => {
@ -152,12 +150,6 @@ describe('organization role APIs', () => {
code: 'entity.relation_foreign_key_not_found',
})
);
await Promise.all([
roleApi.delete(role.id),
scopeApi.delete(scope1.id),
scopeApi.delete(scope2.id),
]);
});
it('should be able to remove scopes from a role', async () => {
@ -180,12 +172,6 @@ describe('organization role APIs', () => {
name: scope2.name,
})
);
await Promise.all([
roleApi.delete(role.id),
scopeApi.delete(scope1.id),
scopeApi.delete(scope2.id),
]);
});
it('should fail when try to remove non-existent scopes from a role', async () => {
@ -195,8 +181,6 @@ describe('organization role APIs', () => {
assert(response instanceof HTTPError);
expect(response.response.statusCode).toBe(404);
await Promise.all([roleApi.delete(role.id)]);
});
});
});

View file

@ -4,11 +4,17 @@ import { generateStandardId } from '@logto/shared';
import { isKeyInObject } from '@silverhand/essentials';
import { HTTPError } from 'got';
import { scopeApi } from '#src/api/organization-scope.js';
import { OrganizationScopeApiTest } from '#src/helpers/organization.js';
const randomId = () => generateStandardId(4);
describe('organization scopes', () => {
describe('organization scope APIs', () => {
const scopeApi = new OrganizationScopeApiTest();
afterEach(async () => {
await scopeApi.cleanUp();
});
it('should fail if the name of the new organization scope already exists', async () => {
const name = 'test' + randomId();
await scopeApi.create({ name });

View file

@ -3,15 +3,20 @@ import assert from 'node:assert';
import { generateStandardId } from '@logto/shared';
import { HTTPError } from 'got';
import { createUser, deleteUser } from '#src/api/admin-user.js';
import { roleApi } from '#src/api/organization-role.js';
import { organizationApi } from '#src/api/organization.js';
import { OrganizationApiTest } from '#src/helpers/organization.js';
import { UserApiTest } from '#src/helpers/user.js';
const randomId = () => generateStandardId(4);
// Add additional layer of describe to run tests in band
describe('organization APIs', () => {
describe('organizations', () => {
const organizationApi = new OrganizationApiTest();
afterEach(async () => {
await organizationApi.cleanUp();
});
it('should get organizations successfully', async () => {
await organizationApi.create({ name: 'test', description: 'A test organization.' });
await organizationApi.create({ name: 'test2' });
@ -23,15 +28,11 @@ describe('organization APIs', () => {
expect(organizations).toContainEqual(
expect.objectContaining({ name: 'test2', description: null })
);
await Promise.all(
organizations.map(async (organization) => organizationApi.delete(organization.id))
);
});
it('should get organizations with pagination', async () => {
// Add organizations to exceed the default page size
const allOrganizations = await Promise.all(
await Promise.all(
Array.from({ length: 30 }).map(async () => organizationApi.create({ name: 'test' }))
);
@ -47,10 +48,6 @@ describe('organization APIs', () => {
expect(organizations2.length).toBeGreaterThanOrEqual(10);
expect(organizations2[0]?.id).not.toBeFalsy();
expect(organizations2[0]?.id).toBe(organizations[10]?.id);
await Promise.all(
allOrganizations.map(async (organization) => organizationApi.delete(organization.id))
);
});
it('should be able to create and get organizations by id', async () => {
@ -58,7 +55,6 @@ describe('organization APIs', () => {
const organization = await organizationApi.get(createdOrganization.id);
expect(organization).toStrictEqual(createdOrganization);
await organizationApi.delete(createdOrganization.id);
});
it('should fail when try to get an organization that does not exist', async () => {
@ -78,7 +74,6 @@ describe('organization APIs', () => {
name: 'test2',
description: 'test description.',
});
await organizationApi.delete(createdOrganization.id);
});
it('should be able to delete organization', async () => {
@ -97,22 +92,24 @@ describe('organization APIs', () => {
});
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([
createUser({ username: 'test' + randomId() }),
createUser({ username: 'test' + randomId() }),
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 }));
await Promise.all([
organizationApi.delete(organization.id),
deleteUser(user1.id),
deleteUser(user2.id),
]);
});
it('should fail when try to add empty user list', async () => {
@ -121,7 +118,6 @@ describe('organization APIs', () => {
.addUsers(organization.id, [])
.catch((error: unknown) => error);
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
await organizationApi.delete(organization.id);
});
it('should fail when try to add user to an organization that does not exist', async () => {
@ -135,13 +131,12 @@ describe('organization APIs', () => {
it('should be able to delete organization user', async () => {
const organization = await organizationApi.create({ name: 'test' });
const user = await createUser({ username: 'test' + randomId() });
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);
await Promise.all([organizationApi.delete(organization.id), deleteUser(user.id)]);
});
it('should fail when try to delete user from an organization that does not exist', async () => {
@ -152,9 +147,17 @@ describe('organization APIs', () => {
});
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 createUser({ username: 'test' + randomId() });
const user = await userApi.create({ username: 'test' + randomId() });
const [role1, role2] = await Promise.all([
roleApi.create({ name: 'test' + randomId() }),
roleApi.create({ name: 'test' + randomId() }),
@ -175,12 +178,6 @@ describe('organization APIs', () => {
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 }));
await Promise.all([
organizationApi.delete(organization.id),
deleteUser(user.id),
roleApi.delete(role1.id),
roleApi.delete(role2.id),
]);
});
it('should be able to get all organizations with roles for a user', async () => {
@ -188,7 +185,7 @@ describe('organization APIs', () => {
organizationApi.create({ name: 'test' }),
organizationApi.create({ name: 'test' }),
]);
const user = await createUser({ username: 'test' + randomId() });
const user = await userApi.create({ username: 'test' + randomId() });
const [role1, role2] = await Promise.all([
roleApi.create({ name: 'test' + randomId() }),
roleApi.create({ name: 'test' + randomId() }),
@ -222,15 +219,6 @@ describe('organization APIs', () => {
expect(organization2WithRoles.organizationRoles).toContainEqual(
expect.objectContaining({ id: role2.id })
);
// Clean up
await Promise.all([
organizationApi.delete(organization1.id),
organizationApi.delete(organization2.id),
deleteUser(user.id),
roleApi.delete(role1.id),
roleApi.delete(role2.id),
]);
});
});
});