mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
496 lines
18 KiB
TypeScript
496 lines
18 KiB
TypeScript
/* eslint-disable max-lines */
|
|
import type { EmailConnector, SmsConnector } from '@logto/connector-kit';
|
|
import { ConnectorPlatform, VerificationCodeType } from '@logto/connector-kit';
|
|
import { ConnectorType } from '@logto/schemas';
|
|
import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
|
import { any } from 'zod';
|
|
|
|
import {
|
|
mockMetadata,
|
|
mockMetadata0,
|
|
mockMetadata1,
|
|
mockMetadata2,
|
|
mockMetadata3,
|
|
mockConnector,
|
|
mockConnectorFactory,
|
|
mockLogtoConnectorList,
|
|
mockLogtoConnector,
|
|
} from '#src/__mocks__/index.js';
|
|
import { defaultConnectorMethods } from '#src/connectors/consts.js';
|
|
import type { LogtoConnector } from '#src/connectors/types.js';
|
|
import RequestError from '#src/errors/RequestError/index.js';
|
|
import assertThat from '#src/utils/assert-that.js';
|
|
import { createRequester } from '#src/utils/test-utils.js';
|
|
|
|
const { jest } = import.meta;
|
|
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
|
|
|
|
mockEsm('#src/libraries/connector.js', () => ({
|
|
checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(),
|
|
}));
|
|
|
|
const { removeUnavailableSocialConnectorTargets } = mockEsm(
|
|
'#src/libraries/sign-in-experience/index.js',
|
|
() => ({
|
|
removeUnavailableSocialConnectorTargets: jest.fn(),
|
|
})
|
|
);
|
|
|
|
const {
|
|
findConnectorById,
|
|
countConnectorByConnectorId,
|
|
deleteConnectorById,
|
|
deleteConnectorByIds,
|
|
insertConnector,
|
|
} = await mockEsmWithActual('#src/queries/connector.js', () => ({
|
|
findConnectorById: jest.fn(),
|
|
countConnectorByConnectorId: jest.fn(),
|
|
deleteConnectorById: jest.fn(),
|
|
deleteConnectorByIds: jest.fn(),
|
|
insertConnector: jest.fn(async (body: unknown) => body),
|
|
}));
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
const getLogtoConnectors = jest.fn<Promise<LogtoConnector[]>, []>();
|
|
const { loadConnectorFactories } = mockEsm('#src/connectors/index.js', () => ({
|
|
loadConnectorFactories: jest.fn(),
|
|
getLogtoConnectors,
|
|
getLogtoConnectorById: async (connectorId: string) => {
|
|
const connectors = await getLogtoConnectors();
|
|
const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId);
|
|
assertThat(
|
|
connector,
|
|
new RequestError({
|
|
code: 'entity.not_found',
|
|
connectorId,
|
|
status: 404,
|
|
})
|
|
);
|
|
|
|
return connector;
|
|
},
|
|
}));
|
|
const connectorRoutes = await pickDefault(import('./connector.js'));
|
|
|
|
describe('connector route', () => {
|
|
const connectorRequest = createRequester({ authedRoutes: connectorRoutes });
|
|
|
|
describe('GET /connectors', () => {
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('throws if more than one email connector exists', async () => {
|
|
getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList);
|
|
const response = await connectorRequest.get('/connectors').send({});
|
|
expect(response).toHaveProperty('statusCode', 400);
|
|
});
|
|
|
|
it('throws if more than one SMS connector exists', async () => {
|
|
getLogtoConnectors.mockResolvedValueOnce(
|
|
mockLogtoConnectorList.filter((connector) => connector.type !== ConnectorType.Email)
|
|
);
|
|
const response = await connectorRequest.get('/connectors').send({});
|
|
expect(response).toHaveProperty('statusCode', 400);
|
|
});
|
|
|
|
it('shows all connectors', async () => {
|
|
getLogtoConnectors.mockResolvedValueOnce(
|
|
mockLogtoConnectorList.filter((connector) => connector.type === ConnectorType.Social)
|
|
);
|
|
const response = await connectorRequest.get('/connectors').send({});
|
|
expect(response).toHaveProperty('statusCode', 200);
|
|
});
|
|
});
|
|
|
|
describe('GET /connector-factories', () => {
|
|
it('show all connector factories', async () => {
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{ ...mockConnectorFactory, metadata: mockMetadata0, type: ConnectorType.Sms },
|
|
{ ...mockConnectorFactory, metadata: mockMetadata1, type: ConnectorType.Social },
|
|
{ ...mockConnectorFactory, metadata: mockMetadata2, type: ConnectorType.Email },
|
|
{ ...mockConnectorFactory, metadata: mockMetadata3, type: ConnectorType.Social },
|
|
]);
|
|
const response = await connectorRequest.get('/connector-factories').send({});
|
|
expect(response.body).toMatchObject([
|
|
{ ...mockMetadata0, type: ConnectorType.Sms },
|
|
{ ...mockMetadata1, type: ConnectorType.Social },
|
|
{ ...mockMetadata2, type: ConnectorType.Email },
|
|
{ ...mockMetadata3, type: ConnectorType.Social },
|
|
]);
|
|
expect(response).toHaveProperty('statusCode', 200);
|
|
});
|
|
});
|
|
|
|
describe('GET /connectors/:id', () => {
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('throws when connector can not be found by given connectorId (locally)', async () => {
|
|
getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList.slice(2));
|
|
const response = await connectorRequest.get('/connectors/findConnector').send({});
|
|
expect(response).toHaveProperty('statusCode', 404);
|
|
});
|
|
|
|
it('throws when connector can not be found by given connectorId (remotely)', async () => {
|
|
getLogtoConnectors.mockResolvedValueOnce([]);
|
|
const response = await connectorRequest.get('/connectors/id0').send({});
|
|
expect(response).toHaveProperty('statusCode', 404);
|
|
});
|
|
|
|
it('shows found connector information', async () => {
|
|
getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList);
|
|
const response = await connectorRequest.get('/connectors/id0').send({});
|
|
expect(response).toHaveProperty('statusCode', 200);
|
|
});
|
|
});
|
|
|
|
describe('POST /connectors', () => {
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('should post a new connector record', async () => {
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{
|
|
...mockConnectorFactory,
|
|
metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
|
|
},
|
|
]);
|
|
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
|
getLogtoConnectors.mockResolvedValueOnce([
|
|
{
|
|
dbEntry: { ...mockConnector, connectorId: 'id0' },
|
|
metadata: { ...mockMetadata, id: 'id0' },
|
|
type: ConnectorType.Sms,
|
|
...mockLogtoConnector,
|
|
},
|
|
]);
|
|
await connectorRequest.post('/connectors').send({
|
|
connectorId: 'connectorId',
|
|
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
|
});
|
|
expect(insertConnector).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
connectorId: 'connectorId',
|
|
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('throws when connector factory not found', async () => {
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{
|
|
...mockConnectorFactory,
|
|
metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
|
|
},
|
|
]);
|
|
const response = await connectorRequest.post('/connectors').send({
|
|
connectorId: 'id0',
|
|
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
|
});
|
|
expect(response).toHaveProperty('statusCode', 422);
|
|
});
|
|
|
|
it('should post a new record when add more than 1 instance with standard connector factory', async () => {
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{
|
|
...mockConnectorFactory,
|
|
metadata: {
|
|
...mockMetadata,
|
|
id: 'id0',
|
|
isStandard: true,
|
|
platform: ConnectorPlatform.Universal,
|
|
},
|
|
},
|
|
]);
|
|
countConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });
|
|
getLogtoConnectors.mockResolvedValueOnce([
|
|
{
|
|
dbEntry: { ...mockConnector, connectorId: 'id0' },
|
|
metadata: { ...mockMetadata, id: 'id0', platform: ConnectorPlatform.Universal },
|
|
type: ConnectorType.Social,
|
|
...mockLogtoConnector,
|
|
},
|
|
]);
|
|
await connectorRequest.post('/connectors').send({
|
|
connectorId: 'id0',
|
|
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
|
metadata: { target: 'new_target' },
|
|
});
|
|
expect(insertConnector).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
connectorId: 'id0',
|
|
config: {
|
|
cliend_id: 'client_id',
|
|
client_secret: 'client_secret',
|
|
},
|
|
metadata: { target: 'new_target' },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('throws when add more than 1 instance with target is an empty string with standard connector factory', async () => {
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{
|
|
...mockConnectorFactory,
|
|
metadata: {
|
|
...mockMetadata,
|
|
id: 'id0',
|
|
isStandard: true,
|
|
platform: ConnectorPlatform.Universal,
|
|
},
|
|
},
|
|
]);
|
|
const response = await connectorRequest.post('/connectors').send({
|
|
connectorId: 'id0',
|
|
metadata: { target: '' },
|
|
});
|
|
expect(response).toHaveProperty('statusCode', 400);
|
|
});
|
|
|
|
it('throws when add more than 1 instance with non-standard connector factory', async () => {
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{
|
|
...mockConnectorFactory,
|
|
metadata: { ...mockConnectorFactory.metadata, id: 'id0' },
|
|
},
|
|
]);
|
|
countConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });
|
|
const response = await connectorRequest.post('/connectors').send({
|
|
connectorId: 'id0',
|
|
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
|
});
|
|
expect(response).toHaveProperty('statusCode', 422);
|
|
});
|
|
|
|
it('should add a new record and delete old records with same connector type when add passwordless connectors', async () => {
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{
|
|
...mockConnectorFactory,
|
|
type: ConnectorType.Sms,
|
|
metadata: { ...mockConnectorFactory.metadata, id: 'id1' },
|
|
},
|
|
]);
|
|
getLogtoConnectors.mockResolvedValueOnce([
|
|
{
|
|
dbEntry: { ...mockConnector, connectorId: 'id0' },
|
|
metadata: { ...mockMetadata, id: 'id0' },
|
|
type: ConnectorType.Sms,
|
|
...mockLogtoConnector,
|
|
},
|
|
]);
|
|
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
|
await connectorRequest.post('/connectors').send({
|
|
connectorId: 'id1',
|
|
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
|
});
|
|
expect(insertConnector).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
connectorId: 'id1',
|
|
config: {
|
|
cliend_id: 'client_id',
|
|
client_secret: 'client_secret',
|
|
},
|
|
})
|
|
);
|
|
expect(deleteConnectorByIds).toHaveBeenCalledWith(['id']);
|
|
});
|
|
|
|
it('throws when add more than 1 social connector instance with same target and platform (add from standard connector)', async () => {
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{
|
|
...mockConnectorFactory,
|
|
metadata: {
|
|
...mockConnectorFactory.metadata,
|
|
id: 'id0',
|
|
platform: ConnectorPlatform.Universal,
|
|
isStandard: true,
|
|
},
|
|
},
|
|
]);
|
|
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
|
getLogtoConnectors.mockResolvedValueOnce([
|
|
{
|
|
dbEntry: { ...mockConnector, connectorId: 'id0', metadata: { target: 'target' } },
|
|
metadata: {
|
|
...mockMetadata,
|
|
id: 'id0',
|
|
target: 'target',
|
|
platform: ConnectorPlatform.Universal,
|
|
},
|
|
type: ConnectorType.Social,
|
|
...mockLogtoConnector,
|
|
},
|
|
]);
|
|
const response = await connectorRequest.post('/connectors').send({
|
|
connectorId: 'id0',
|
|
metadata: { target: 'target' },
|
|
});
|
|
expect(response).toHaveProperty('statusCode', 422);
|
|
});
|
|
|
|
it('throws when add more than 1 social connector instance with same target and platform (add social connector)', async () => {
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{
|
|
...mockConnectorFactory,
|
|
metadata: {
|
|
...mockConnectorFactory.metadata,
|
|
id: 'id0',
|
|
platform: ConnectorPlatform.Universal,
|
|
target: 'target',
|
|
},
|
|
},
|
|
]);
|
|
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
|
getLogtoConnectors.mockResolvedValueOnce([
|
|
{
|
|
dbEntry: { ...mockConnector, connectorId: 'id0', metadata: { target: 'target' } },
|
|
metadata: {
|
|
...mockMetadata,
|
|
id: 'id0',
|
|
target: 'target',
|
|
platform: ConnectorPlatform.Universal,
|
|
},
|
|
type: ConnectorType.Social,
|
|
...mockLogtoConnector,
|
|
},
|
|
]);
|
|
const response = await connectorRequest.post('/connectors').send({
|
|
connectorId: 'id0',
|
|
});
|
|
expect(response).toHaveProperty('statusCode', 422);
|
|
});
|
|
});
|
|
|
|
describe('POST /connectors/:id/test', () => {
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('should get SMS connector and send test message', async () => {
|
|
const mockedMetadata = {
|
|
...mockMetadata,
|
|
};
|
|
const sendMessage = jest.fn();
|
|
const mockedSmsConnector: LogtoConnector<SmsConnector> = {
|
|
dbEntry: mockConnector,
|
|
metadata: mockedMetadata,
|
|
type: ConnectorType.Sms,
|
|
configGuard: any(),
|
|
...defaultConnectorMethods,
|
|
sendMessage,
|
|
};
|
|
getLogtoConnectors.mockResolvedValueOnce([mockedSmsConnector]);
|
|
const response = await connectorRequest
|
|
.post('/connectors/id/test')
|
|
.send({ phone: '12345678901', config: { test: 123 } });
|
|
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
{
|
|
to: '12345678901',
|
|
type: VerificationCodeType.Test,
|
|
payload: {
|
|
code: '123456',
|
|
},
|
|
},
|
|
{ test: 123 }
|
|
);
|
|
expect(response).toHaveProperty('statusCode', 204);
|
|
});
|
|
|
|
it('should get email connector and send test message', async () => {
|
|
const sendMessage = jest.fn();
|
|
const mockedEmailConnector: LogtoConnector<EmailConnector> = {
|
|
dbEntry: mockConnector,
|
|
metadata: mockMetadata,
|
|
type: ConnectorType.Email,
|
|
configGuard: any(),
|
|
...defaultConnectorMethods,
|
|
sendMessage,
|
|
};
|
|
getLogtoConnectors.mockResolvedValueOnce([mockedEmailConnector]);
|
|
const response = await connectorRequest
|
|
.post('/connectors/id/test')
|
|
.send({ email: 'test@email.com', config: { test: 123 } });
|
|
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
{
|
|
to: 'test@email.com',
|
|
type: VerificationCodeType.Test,
|
|
payload: {
|
|
code: 'email-test',
|
|
},
|
|
},
|
|
{ test: 123 }
|
|
);
|
|
expect(response).toHaveProperty('statusCode', 204);
|
|
});
|
|
|
|
it('should throw when neither phone nor email is provided', async () => {
|
|
const response = await connectorRequest.post('/connectors/id/test').send({});
|
|
expect(response).toHaveProperty('statusCode', 400);
|
|
});
|
|
|
|
it('should throw when sms connector is not found', async () => {
|
|
getLogtoConnectors.mockResolvedValueOnce([]);
|
|
const response = await connectorRequest
|
|
.post('/connectors/id/test')
|
|
.send({ phone: '12345678901' });
|
|
expect(response).toHaveProperty('statusCode', 400);
|
|
});
|
|
|
|
it('should throw when email connector is not found', async () => {
|
|
getLogtoConnectors.mockResolvedValueOnce([]);
|
|
const response = await connectorRequest
|
|
.post('/connectors/id/test')
|
|
.send({ email: 'test@email.com' });
|
|
expect(response).toHaveProperty('statusCode', 400);
|
|
});
|
|
});
|
|
|
|
describe('DELETE /connectors/:id', () => {
|
|
beforeEach(() => {
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('delete connector instance and remove unavailable social connector targets', async () => {
|
|
findConnectorById.mockResolvedValueOnce(mockConnector);
|
|
loadConnectorFactories.mockResolvedValueOnce([mockConnectorFactory]);
|
|
await connectorRequest.delete('/connectors/id').send({});
|
|
expect(deleteConnectorById).toHaveBeenCalledTimes(1);
|
|
expect(removeUnavailableSocialConnectorTargets).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('delete connector instance (connector factory is not social type)', async () => {
|
|
findConnectorById.mockResolvedValueOnce(mockConnector);
|
|
loadConnectorFactories.mockResolvedValueOnce([
|
|
{ ...mockConnectorFactory, type: ConnectorType.Sms },
|
|
]);
|
|
await connectorRequest.delete('/connectors/id').send({});
|
|
expect(deleteConnectorById).toHaveBeenCalledTimes(1);
|
|
expect(removeUnavailableSocialConnectorTargets).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
it('delete connector instance (connector factory is not found)', async () => {
|
|
findConnectorById.mockResolvedValueOnce(mockConnector);
|
|
loadConnectorFactories.mockResolvedValueOnce([]);
|
|
await connectorRequest.delete('/connectors/id').send({});
|
|
expect(deleteConnectorById).toHaveBeenCalledTimes(1);
|
|
expect(removeUnavailableSocialConnectorTargets).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
it('throws when connector not exists with `id`', async () => {
|
|
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
findConnectorById.mockResolvedValueOnce(undefined);
|
|
const response = await connectorRequest.delete('/connectors/id').send({});
|
|
expect(response).toHaveProperty('statusCode', 500);
|
|
});
|
|
});
|
|
});
|
|
/* eslint-enable max-lines */
|