mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #6086 from logto-io/gao-org-app-apis
feat(core): add organization - application apis
This commit is contained in:
commit
1620bbd718
10 changed files with 298 additions and 77 deletions
|
@ -1,6 +1,11 @@
|
|||
import type { Application, CreateApplication } from '@logto/schemas';
|
||||
import { ApplicationType, Applications, SearchJointMode } from '@logto/schemas';
|
||||
import { pick } from '@silverhand/essentials';
|
||||
import {
|
||||
ApplicationType,
|
||||
Applications,
|
||||
OrganizationApplicationRelations,
|
||||
SearchJointMode,
|
||||
} from '@logto/schemas';
|
||||
import { condArray, pick } from '@silverhand/essentials';
|
||||
import type { CommonQueryMethods, SqlSqlToken } from '@silverhand/slonik';
|
||||
import { sql } from '@silverhand/slonik';
|
||||
|
||||
|
@ -23,6 +28,7 @@ import {
|
|||
} from './application-user-consent-scopes.js';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(Applications);
|
||||
const organizationApplicationRelations = convertToIdentifiers(OrganizationApplicationRelations);
|
||||
|
||||
/**
|
||||
* The schema field keys that can be used for searching apps. For the actual field names,
|
||||
|
@ -34,13 +40,13 @@ export const applicationSearchKeys = Object.freeze(['id', 'name', 'description']
|
|||
|
||||
/**
|
||||
* The actual database field names that can be used for searching apps. For the schema field
|
||||
* keys, see {@link userSearchKeys}.
|
||||
* keys, see {@link applicationSearchKeys}.
|
||||
*/
|
||||
const applicationSearchFields = Object.freeze(
|
||||
Object.values(pick(Applications.fields, ...applicationSearchKeys))
|
||||
);
|
||||
|
||||
const buildApplicationConditions = (search: Search) => {
|
||||
const buildApplicationSearchConditions = (search: Search) => {
|
||||
return conditionalSql(
|
||||
search.matches.length > 0,
|
||||
() =>
|
||||
|
@ -53,40 +59,60 @@ const buildApplicationConditions = (search: Search) => {
|
|||
};
|
||||
|
||||
const buildConditionArray = (conditions: SqlSqlToken[]) => {
|
||||
const filteredConditions = conditions.filter((condition) => condition.sql !== '');
|
||||
const filteredConditions = conditions.filter((condition) => condition.sql.trim() !== '');
|
||||
return conditionalArraySql(
|
||||
filteredConditions,
|
||||
(filteredConditions) => sql`where ${sql.join(filteredConditions, sql` and `)}`
|
||||
);
|
||||
};
|
||||
|
||||
type ApplicationConditions = {
|
||||
/** The search config object, can apply to fields in {@link applicationSearchFields}. */
|
||||
search: Search;
|
||||
/** Exclude applications with these ids. */
|
||||
excludeApplicationIds?: string[];
|
||||
/** Exclude applications associated with an organization. */
|
||||
excludeOrganizationId?: string;
|
||||
/** Filter applications by types, if not provided, all types will be included. */
|
||||
types?: ApplicationType[];
|
||||
/** Filter applications by whether it is a third party application. */
|
||||
isThirdParty?: boolean;
|
||||
};
|
||||
|
||||
const buildApplicationConditions = ({
|
||||
search,
|
||||
excludeApplicationIds,
|
||||
excludeOrganizationId,
|
||||
types,
|
||||
isThirdParty,
|
||||
}: ApplicationConditions) => {
|
||||
return buildConditionArray(
|
||||
condArray(
|
||||
excludeApplicationIds?.length &&
|
||||
sql`${fields.id} not in (${sql.join(excludeApplicationIds, sql`, `)})`,
|
||||
excludeOrganizationId &&
|
||||
sql`
|
||||
not exists (
|
||||
select 1 from ${organizationApplicationRelations.table}
|
||||
where ${organizationApplicationRelations.fields.applicationId} = ${fields.id}
|
||||
and ${organizationApplicationRelations.fields.organizationId}=${excludeOrganizationId}
|
||||
)`,
|
||||
types?.length && sql`${fields.type} in (${sql.join(types, sql`, `)})`,
|
||||
typeof isThirdParty === 'boolean' && sql`${fields.isThirdParty} = ${isThirdParty}`,
|
||||
buildApplicationSearchConditions(search)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
||||
/**
|
||||
* Get the number of applications that match the search conditions, conditions are joined in `and` mode.
|
||||
*
|
||||
* @param search The search config object, can apply to `id`, `name` and `description` field for application.
|
||||
* @param excludeApplicationIds Exclude applications with these ids.
|
||||
* @param isThirdParty Optional boolean, filter applications by whether it is a third party application.
|
||||
* @param types Optional array of {@link ApplicationType}, filter applications by types, if not provided, all types will be included.
|
||||
* @returns A Promise that resolves the number of applications that match the search conditions.
|
||||
*/
|
||||
const countApplications = async (
|
||||
search: Search,
|
||||
excludeApplicationIds: string[],
|
||||
isThirdParty?: boolean,
|
||||
types?: ApplicationType[]
|
||||
) => {
|
||||
const countApplications = async (conditions: ApplicationConditions) => {
|
||||
const { count } = await pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
from ${table}
|
||||
${buildConditionArray([
|
||||
excludeApplicationIds.length > 0
|
||||
? sql`${fields.id} not in (${sql.join(excludeApplicationIds, sql`, `)})`
|
||||
: sql``,
|
||||
types && types.length > 0 ? sql`${fields.type} in (${sql.join(types, sql`, `)})` : sql``,
|
||||
typeof isThirdParty === 'boolean' ? sql`${fields.isThirdParty} = ${isThirdParty}` : sql``,
|
||||
buildApplicationConditions(search),
|
||||
])}
|
||||
${buildApplicationConditions(conditions)}
|
||||
`);
|
||||
|
||||
return { count: Number(count) };
|
||||
|
@ -94,47 +120,17 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
|
||||
/**
|
||||
* Get the list of applications that match the search conditions, conditions are joined in `and` mode.
|
||||
*
|
||||
* @param conditions The conditions to filter applications.
|
||||
* @param conditions.search The search config object, can apply to `id`, `name` and `description` field for application
|
||||
* @param conditions.excludeApplicationIds Exclude applications with these ids.
|
||||
* @param conditions.types Optional array of {@link ApplicationType}, filter applications by types, if not provided, all types will be included.
|
||||
* @param conditions.isThirdParty Optional boolean, filter applications by whether it is a third party application.
|
||||
* @param conditions.pagination Optional pagination config object.
|
||||
* @param conditions.pagination.limit The number of applications to return.
|
||||
* @param conditions.pagination.offset The offset of applications to return.
|
||||
* @returns A Promise that resolves the list of applications that match the search conditions.
|
||||
*/
|
||||
const findApplications = async ({
|
||||
search,
|
||||
excludeApplicationIds,
|
||||
types,
|
||||
isThirdParty,
|
||||
pagination,
|
||||
}: {
|
||||
search: Search;
|
||||
excludeApplicationIds: string[];
|
||||
types?: ApplicationType[];
|
||||
isThirdParty?: boolean;
|
||||
pagination?: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
}) =>
|
||||
const findApplications = async (
|
||||
conditions: ApplicationConditions,
|
||||
pagination?: { limit: number; offset: number }
|
||||
) =>
|
||||
pool.any<Application>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
${buildConditionArray([
|
||||
excludeApplicationIds.length > 0
|
||||
? sql`${fields.id} not in (${sql.join(excludeApplicationIds, sql`, `)})`
|
||||
: sql``,
|
||||
types && types.length > 0 ? sql`${fields.type} in (${sql.join(types, sql`, `)})` : sql``,
|
||||
typeof isThirdParty === 'boolean' ? sql`${fields.isThirdParty} = ${isThirdParty}` : sql``,
|
||||
buildApplicationConditions(search),
|
||||
])}
|
||||
${buildApplicationConditions(conditions)}
|
||||
order by ${fields.createdAt} desc
|
||||
${conditionalSql(pagination?.limit, (value) => sql`limit ${value}`)}
|
||||
${conditionalSql(pagination?.offset, (value) => sql`offset ${value}`)}
|
||||
${conditionalSql(pagination, ({ limit, offset }) => sql`limit ${limit} offset ${offset}`)}
|
||||
`);
|
||||
|
||||
const findTotalNumberOfApplications = async () => getTotalRowCountWithPool(pool)(table);
|
||||
|
@ -153,14 +149,13 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
) => updateApplication({ set, where: { id }, jsonbMode: 'merge' });
|
||||
|
||||
const countAllApplications = async () =>
|
||||
countApplications(
|
||||
{
|
||||
countApplications({
|
||||
search: {
|
||||
matches: [],
|
||||
joint: SearchJointMode.And, // Dummy since there is no match
|
||||
isCaseSensitive: false, // Dummy since there is no match
|
||||
},
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
const countM2mApplications = async () => {
|
||||
const { count } = await pool.one<{ count: string }>(sql`
|
||||
|
@ -193,7 +188,7 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
${buildConditionArray([
|
||||
sql`${fields.type} = ${ApplicationType.MachineToMachine}`,
|
||||
sql`${fields.id} in (${sql.join(applicationIds, sql`, `)})`,
|
||||
buildApplicationConditions(search),
|
||||
buildApplicationSearchConditions(search),
|
||||
])}
|
||||
`);
|
||||
|
||||
|
@ -216,7 +211,7 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
${buildConditionArray([
|
||||
sql`${fields.type} = ${ApplicationType.MachineToMachine}`,
|
||||
sql`${fields.id} in (${sql.join(applicationIds, sql`, `)})`,
|
||||
buildApplicationConditions(search),
|
||||
buildApplicationSearchConditions(search),
|
||||
])}
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
OrganizationRoles,
|
||||
OrganizationRoleApplicationRelations,
|
||||
type ApplicationWithOrganizationRoles,
|
||||
type OrganizationWithRoles,
|
||||
} from '@logto/schemas';
|
||||
import { type CommonQueryMethods, sql } from '@silverhand/slonik';
|
||||
|
||||
|
@ -25,6 +26,45 @@ export class ApplicationRelationQueries extends TwoRelationsQueries<
|
|||
super(pool, OrganizationApplicationRelations.table, Organizations, Applications);
|
||||
}
|
||||
|
||||
async getOrganizationsByApplicationId(
|
||||
applicationId: string,
|
||||
{ limit, offset }: GetEntitiesOptions
|
||||
): Promise<[totalCount: number, organizations: readonly OrganizationWithRoles[]]> {
|
||||
const organizations = convertToIdentifiers(Organizations, true);
|
||||
const roles = convertToIdentifiers(OrganizationRoles, true);
|
||||
const { fields } = convertToIdentifiers(OrganizationApplicationRelations, true);
|
||||
const relations = convertToIdentifiers(OrganizationRoleApplicationRelations, true);
|
||||
|
||||
const [{ count }, entities] = await Promise.all([
|
||||
this.pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
from ${this.table}
|
||||
left join ${organizations.table}
|
||||
on ${fields.organizationId} = ${organizations.fields.id}
|
||||
where ${fields.applicationId} = ${applicationId}
|
||||
`),
|
||||
this.pool.any<OrganizationWithRoles>(sql`
|
||||
select
|
||||
${sql.join(Object.values(organizations.fields), sql`, `)},
|
||||
${aggregateRoles()}
|
||||
from ${this.table}
|
||||
left join ${organizations.table}
|
||||
on ${fields.organizationId} = ${organizations.fields.id}
|
||||
left join ${relations.table}
|
||||
on ${relations.fields.organizationId} = ${organizations.fields.id}
|
||||
and ${relations.fields.applicationId} = ${fields.applicationId}
|
||||
left join ${roles.table}
|
||||
on ${relations.fields.organizationRoleId} = ${roles.fields.id}
|
||||
where ${fields.applicationId} = ${applicationId}
|
||||
group by ${organizations.fields.id}
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`),
|
||||
]);
|
||||
|
||||
return [Number(count), entities];
|
||||
}
|
||||
|
||||
/** Get the applications of an organization with their organization roles. */
|
||||
async getApplicationsByOrganizationId(
|
||||
organizationId: string,
|
||||
|
|
|
@ -65,7 +65,7 @@ export default function adminUserSearchRoutes<T extends ManagementApiRouter>(
|
|||
if (excludeRoleId && excludeOrganizationId) {
|
||||
throw new RequestError({
|
||||
code: 'request.invalid_input',
|
||||
status: 422,
|
||||
status: 400,
|
||||
details:
|
||||
'Parameter `excludeRoleId` and `excludeOrganizationId` cannot be used at the same time.',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"paths": {
|
||||
"/api/applications/{id}/organizations": {
|
||||
"get": {
|
||||
"tags": ["Dev feature"],
|
||||
"summary": "Get application organizations",
|
||||
"description": "Get the list of organizations that an application is associated with.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "An array of organizations that the application is associated with."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { organizationWithOrganizationRolesGuard } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
|
||||
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';
|
||||
|
||||
export default function applicationOrganizationRoutes<T extends ManagementApiRouter>(
|
||||
...[router, { queries }]: RouterInitArgs<T>
|
||||
) {
|
||||
// TODO: Remove
|
||||
if (!EnvSet.values.isDevFeaturesEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.get(
|
||||
'/applications/:id/organizations',
|
||||
koaPagination(),
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string() }),
|
||||
response: organizationWithOrganizationRolesGuard.array(),
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id } = ctx.guard.params;
|
||||
|
||||
// Ensure that the user exists.
|
||||
await queries.applications.findApplicationById(id);
|
||||
|
||||
const [count, entities] =
|
||||
await queries.organizations.relations.apps.getOrganizationsByApplicationId(
|
||||
id,
|
||||
ctx.pagination
|
||||
);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = entities;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -76,6 +76,7 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
|||
.or(applicationTypeGuard.transform((type) => [type]))
|
||||
.optional(),
|
||||
excludeRoleId: string().optional(),
|
||||
excludeOrganizationId: string().optional(),
|
||||
isThirdParty: z.union([z.literal('true'), z.literal('false')]).optional(),
|
||||
}),
|
||||
response: z.array(Applications.guard),
|
||||
|
@ -84,7 +85,21 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
|||
async (ctx, next) => {
|
||||
const { limit, offset, disabled: paginationDisabled } = ctx.pagination;
|
||||
const { searchParams } = ctx.URL;
|
||||
const { types, excludeRoleId, isThirdParty: isThirdPartyParam } = ctx.guard.query;
|
||||
const {
|
||||
types,
|
||||
excludeRoleId,
|
||||
excludeOrganizationId,
|
||||
isThirdParty: isThirdPartyParam,
|
||||
} = ctx.guard.query;
|
||||
|
||||
if (excludeRoleId && excludeOrganizationId) {
|
||||
throw new RequestError({
|
||||
code: 'request.invalid_input',
|
||||
status: 400,
|
||||
details:
|
||||
'Parameter `excludeRoleId` and `excludeOrganizationId` cannot be used at the same time.',
|
||||
});
|
||||
}
|
||||
|
||||
const isThirdParty = parseIsThirdPartQueryParam(isThirdPartyParam);
|
||||
|
||||
|
@ -100,20 +115,35 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
|||
);
|
||||
|
||||
if (paginationDisabled) {
|
||||
ctx.body = await findApplications({ search, excludeApplicationIds, types, isThirdParty });
|
||||
ctx.body = await findApplications({
|
||||
search,
|
||||
excludeApplicationIds,
|
||||
excludeOrganizationId,
|
||||
types,
|
||||
isThirdParty,
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
const [{ count }, applications] = await Promise.all([
|
||||
countApplications(search, excludeApplicationIds, isThirdParty, types),
|
||||
findApplications({
|
||||
countApplications({
|
||||
search,
|
||||
excludeApplicationIds,
|
||||
types,
|
||||
excludeOrganizationId,
|
||||
isThirdParty,
|
||||
pagination: { limit, offset },
|
||||
types,
|
||||
}),
|
||||
findApplications(
|
||||
{
|
||||
search,
|
||||
excludeApplicationIds,
|
||||
excludeOrganizationId,
|
||||
types,
|
||||
isThirdParty,
|
||||
},
|
||||
{ limit, offset }
|
||||
),
|
||||
]);
|
||||
|
||||
// Return totalCount to pagination middleware
|
||||
|
|
|
@ -12,6 +12,7 @@ import type TenantContext from '#src/tenants/TenantContext.js';
|
|||
import koaAuth from '../middleware/koa-auth/index.js';
|
||||
|
||||
import adminUserRoutes from './admin-user/index.js';
|
||||
import applicationOrganizationRoutes from './applications/application-organization.js';
|
||||
import applicationProtectedAppMetadataRoutes from './applications/application-protected-app-metadata.js';
|
||||
import applicationRoleRoutes from './applications/application-role.js';
|
||||
import applicationSignInExperienceRoutes from './applications/application-sign-in-experience.js';
|
||||
|
@ -52,16 +53,17 @@ const createRouters = (tenant: TenantContext) => {
|
|||
managementRouter.use(koaTenantGuard(tenant.id, tenant.queries));
|
||||
managementRouter.use(koaManagementApiHooks(tenant.libraries.hooks));
|
||||
|
||||
// TODO: FIXME @sijie @darcy mount these routes in `applicationRoutes` instead
|
||||
applicationRoutes(managementRouter, tenant);
|
||||
applicationRoleRoutes(managementRouter, tenant);
|
||||
applicationProtectedAppMetadataRoutes(managementRouter, tenant);
|
||||
applicationOrganizationRoutes(managementRouter, tenant);
|
||||
|
||||
// Third-party application related routes
|
||||
applicationUserConsentScopeRoutes(managementRouter, tenant);
|
||||
applicationSignInExperienceRoutes(managementRouter, tenant);
|
||||
applicationUserConsentOrganizationRoutes(managementRouter, tenant);
|
||||
|
||||
applicationProtectedAppMetadataRoutes(managementRouter, tenant);
|
||||
|
||||
logtoConfigRoutes(managementRouter, tenant);
|
||||
connectorRoutes(managementRouter, tenant);
|
||||
resourceRoutes(managementRouter, tenant);
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
type OidcClientMetadata,
|
||||
type Role,
|
||||
type ProtectedAppMetadata,
|
||||
type OrganizationWithRoles,
|
||||
} from '@logto/schemas';
|
||||
import { formUrlEncodedHeaders } from '@logto/shared';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
|
@ -108,3 +109,11 @@ export const generateM2mLog = async (applicationId: string) => {
|
|||
}),
|
||||
});
|
||||
};
|
||||
|
||||
/** Get organizations that an application is associated with. */
|
||||
export const getOrganizations = async (applicationId: string, page: number, pageSize: number) =>
|
||||
authedAdminApi
|
||||
.get(`applications/${applicationId}/organizations`, {
|
||||
searchParams: { page, page_size: pageSize },
|
||||
})
|
||||
.json<OrganizationWithRoles[]>();
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import { type Application, ApplicationType } from '@logto/schemas';
|
||||
|
||||
import {
|
||||
createApplication as createApplicationApi,
|
||||
deleteApplication,
|
||||
getApplications,
|
||||
getOrganizations,
|
||||
} from '#src/api/application.js';
|
||||
import { OrganizationApiTest } from '#src/helpers/organization.js';
|
||||
import { devFeatureTest, generateTestName } from '#src/utils.js';
|
||||
|
||||
devFeatureTest.describe('application organizations', () => {
|
||||
const organizationApi = new OrganizationApiTest();
|
||||
const applications: Application[] = [];
|
||||
const createApplication = async (...args: Parameters<typeof createApplicationApi>) => {
|
||||
const created = await createApplicationApi(...args);
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
applications.push(created);
|
||||
return created;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
applications.push(
|
||||
await createApplication(generateTestName(), ApplicationType.MachineToMachine)
|
||||
);
|
||||
await Promise.all(
|
||||
Array.from({ length: 30 }).map(async () => {
|
||||
const organization = await organizationApi.create({ name: generateTestName() });
|
||||
await organizationApi.applications.add(organization.id, [applications[0]!.id]);
|
||||
return organization;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all([
|
||||
organizationApi.cleanUp(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
...applications.map(async ({ id }) => deleteApplication(id).catch(() => {})),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get organizations by application id with pagination', async () => {
|
||||
const organizations1 = await getOrganizations(applications[0]!.id, 1, 30);
|
||||
const organizations2 = await getOrganizations(applications[0]!.id, 2, 10);
|
||||
const organizations3 = await getOrganizations(applications[0]!.id, 2, 20);
|
||||
|
||||
expect(organizations1).toEqual(
|
||||
expect.arrayContaining(
|
||||
organizationApi.organizations.map((object) => expect.objectContaining(object))
|
||||
)
|
||||
);
|
||||
expect(organizations1).toHaveLength(30);
|
||||
expect(organizations2).toHaveLength(10);
|
||||
expect(organizations3).toHaveLength(10);
|
||||
expect(organizations2[0]?.id).toBe(organizations1[10]?.id);
|
||||
expect(organizations3[0]?.id).toBe(organizations1[20]?.id);
|
||||
});
|
||||
|
||||
it('should be able to fetch applications by excluding an organization', async () => {
|
||||
const excludedOrganization = await organizationApi.create({ name: generateTestName() });
|
||||
const applications = await Promise.all(
|
||||
Array.from({ length: 3 }).map(async () =>
|
||||
createApplication(generateTestName(), ApplicationType.MachineToMachine)
|
||||
)
|
||||
);
|
||||
await organizationApi.applications.add(excludedOrganization.id, [applications[0]!.id]);
|
||||
|
||||
const fetchedApplications = await getApplications(undefined, {
|
||||
excludeOrganizationId: excludedOrganization.id,
|
||||
page_size: '100', // Just in case
|
||||
});
|
||||
|
||||
expect(fetchedApplications).not.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining(applications[0]!)])
|
||||
);
|
||||
expect(fetchedApplications).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining(applications[1]!),
|
||||
expect.objectContaining(applications[2]!),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '#src/api/index.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
|
||||
describe('admin console application', () => {
|
||||
describe('application APIs', () => {
|
||||
it('should create application successfully', async () => {
|
||||
const applicationName = 'test-create-app';
|
||||
const applicationType = ApplicationType.SPA;
|
||||
|
|
Loading…
Reference in a new issue