0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

test(core): add api response guard and error case tests to connector api (#3806)

This commit is contained in:
wangsijie 2023-05-05 21:20:58 +08:00 committed by GitHub
parent 260f39f72d
commit 5875d4cb3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 79 additions and 22 deletions

View file

@ -1,6 +1,11 @@
import { buildRawConnector } from '@logto/cli/lib/connector/index.js'; import { buildRawConnector } from '@logto/cli/lib/connector/index.js';
import { demoConnectorIds, validateConfig } from '@logto/connector-kit'; import { demoConnectorIds, validateConfig } from '@logto/connector-kit';
import { connectorFactoryResponseGuard, Connectors, ConnectorType } from '@logto/schemas'; import {
connectorFactoryResponseGuard,
Connectors,
ConnectorType,
connectorResponseGuard,
} from '@logto/schemas';
import { buildIdGenerator } from '@logto/shared'; import { buildIdGenerator } from '@logto/shared';
import cleanDeep from 'clean-deep'; import cleanDeep from 'clean-deep';
import { string, object } from 'zod'; import { string, object } from 'zod';
@ -48,6 +53,8 @@ export default function connectorRoutes<T extends AuthedRouter>(
syncProfile: true, syncProfile: true,
}) })
.merge(Connectors.createGuard.pick({ id: true }).partial()), // `id` is optional .merge(Connectors.createGuard.pick({ id: true }).partial()), // `id` is optional
response: connectorResponseGuard,
status: [200, 422],
}), }),
async (ctx, next) => { async (ctx, next) => {
const { const {
@ -156,6 +163,8 @@ export default function connectorRoutes<T extends AuthedRouter>(
query: object({ query: object({
target: string().optional(), target: string().optional(),
}), }),
response: connectorResponseGuard.array(),
status: [200, 400],
}), }),
async (ctx, next) => { async (ctx, next) => {
const { target: filterTarget } = ctx.query; const { target: filterTarget } = ctx.query;
@ -184,7 +193,11 @@ export default function connectorRoutes<T extends AuthedRouter>(
router.get( router.get(
'/connectors/:id', '/connectors/:id',
koaGuard({ params: object({ id: string().min(1) }) }), koaGuard({
params: object({ id: string().min(1) }),
response: connectorResponseGuard,
status: [200, 404],
}),
async (ctx, next) => { async (ctx, next) => {
const { const {
params: { id }, params: { id },
@ -221,7 +234,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
koaGuard({ koaGuard({
params: object({ id: string().min(1) }), params: object({ id: string().min(1) }),
response: connectorFactoryResponseGuard, response: connectorFactoryResponseGuard,
status: [200], status: [200, 404],
}), }),
async (ctx, next) => { async (ctx, next) => {
const { const {
@ -251,6 +264,8 @@ export default function connectorRoutes<T extends AuthedRouter>(
body: Connectors.createGuard body: Connectors.createGuard
.pick({ config: true, metadata: true, syncProfile: true }) .pick({ config: true, metadata: true, syncProfile: true })
.partial(), .partial(),
response: connectorResponseGuard,
status: [200, 400, 404, 422],
}), }),
async (ctx, next) => { async (ctx, next) => {
const { const {
@ -300,7 +315,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
router.delete( router.delete(
'/connectors/:id', '/connectors/:id',
koaGuard({ params: object({ id: string().min(1) }) }), koaGuard({ params: object({ id: string().min(1) }), status: [204, 404] }),
async (ctx, next) => { async (ctx, next) => {
const { const {
params: { id }, params: { id },

View file

@ -21,6 +21,7 @@ import {
listConnectorFactories, listConnectorFactories,
getConnectorFactory, getConnectorFactory,
} from '#src/api/connector.js'; } from '#src/api/connector.js';
import { createResponseWithCode } from '#src/helpers/admin-tenant.js';
const connectorIdMap = new Map<string, string>(); const connectorIdMap = new Map<string, string>();
@ -30,6 +31,16 @@ const mockConnectorSetups = [
{ connectorId: mockSocialConnectorId, config: mockSocialConnectorConfig }, { connectorId: mockSocialConnectorId, config: mockSocialConnectorConfig },
]; ];
const cleanUpConnectorTable = async () => {
const connectors = await listConnectors();
await Promise.all(
connectors.map(async ({ id }) => {
await deleteConnectorById(id);
})
);
connectorIdMap.clear();
};
/* /*
* We'd better only use mock connectors in integration tests. * We'd better only use mock connectors in integration tests.
* Since we will refactor connectors soon, keep using some real connectors * Since we will refactor connectors soon, keep using some real connectors
@ -52,16 +63,7 @@ test('connector set-up flow', async () => {
}) })
); );
/** await cleanUpConnectorTable();
* Clean up `connector` table
*/
const connectors = await listConnectors();
await Promise.all(
connectors.map(async ({ id }) => {
await deleteConnectorById(id);
})
);
connectorIdMap.clear();
/* /*
* Set up social/SMS/email connectors * Set up social/SMS/email connectors
@ -141,6 +143,36 @@ test('connector set-up flow', async () => {
); );
}); });
test('create connector with non-exist connectorId', async () => {
await cleanUpConnectorTable();
await expect(postConnector({ connectorId: 'non-exist-id' })).rejects.toMatchObject(
createResponseWithCode(422)
);
});
test('create non standard social connector with target', async () => {
await cleanUpConnectorTable();
await expect(
postConnector({ connectorId: mockSocialConnectorId, metadata: { target: 'target' } })
).rejects.toMatchObject(createResponseWithCode(400));
});
test('create duplicated social connector', async () => {
await cleanUpConnectorTable();
await postConnector({ connectorId: mockSocialConnectorId });
await expect(postConnector({ connectorId: mockSocialConnectorId })).rejects.toMatchObject(
createResponseWithCode(422)
);
});
test('override metadata for non-standard social connector', async () => {
await cleanUpConnectorTable();
const { id } = await postConnector({ connectorId: mockSocialConnectorId });
await expect(updateConnectorConfig(id, {}, { target: 'target' })).rejects.toMatchObject(
createResponseWithCode(400)
);
});
test('send SMS/email test message', async () => { test('send SMS/email test message', async () => {
const connectors = await listConnectors(); const connectors = await listConnectors();
await Promise.all( await Promise.all(

View file

@ -1,18 +1,28 @@
import type { BaseConnector, ConnectorMetadata } from '@logto/connector-kit';
import { ConnectorType, connectorMetadataGuard } from '@logto/connector-kit'; import { ConnectorType, connectorMetadataGuard } from '@logto/connector-kit';
import { z } from 'zod'; import { z } from 'zod';
import type { Connector } from '../db-entries/index.js'; import { Connectors } from '../db-entries/index.js';
export type { ConnectorMetadata } from '@logto/connector-kit'; export type { ConnectorMetadata } from '@logto/connector-kit';
export { ConnectorType, ConnectorPlatform } from '@logto/connector-kit'; export { ConnectorType, ConnectorPlatform } from '@logto/connector-kit';
export type ConnectorResponse = Pick< export const connectorResponseGuard = Connectors.guard
Connector, .pick({
'id' | 'syncProfile' | 'config' | 'metadata' | 'connectorId' id: true,
> & syncProfile: true,
Omit<BaseConnector<ConnectorType>, 'configGuard' | 'metadata'> & config: true,
ConnectorMetadata & { isDemo?: boolean }; metadata: true,
connectorId: true,
})
.merge(connectorMetadataGuard)
.merge(
z.object({
type: z.nativeEnum(ConnectorType),
isDemo: z.boolean().optional(),
})
);
export type ConnectorResponse = z.infer<typeof connectorResponseGuard>;
export const connectorFactoryResponseGuard = z export const connectorFactoryResponseGuard = z
.object({ .object({