mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
refactor(core, connector): fix code to fit new ESlint rule (#942)
* refactor(core): separate/refactor connector route UTs * refactor(connector-wechat*): fix wechat connectors error handler to fit new eslint rule * refactor(connector-wechat*): fix error handler
This commit is contained in:
parent
545a3929e4
commit
ae97e7bd27
7 changed files with 369 additions and 489 deletions
|
@ -108,6 +108,17 @@ describe('getUserInfo', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('throws error if `openid` is missing', async () => {
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(0, {
|
||||
unionid: 'this_is_an_arbitrary_wechat_union_id',
|
||||
headimgurl: 'https://github.com/images/error/octocat_happy.gif',
|
||||
nickname: 'wechat bot',
|
||||
});
|
||||
await expect(
|
||||
weChatNativeMethods.getUserInfo({ accessToken: 'accessToken' })
|
||||
).rejects.toMatchError(new Error('`openid` is required by WeChat API.'));
|
||||
});
|
||||
|
||||
it('throws SocialAccessTokenInvalid error if errcode is 40001', async () => {
|
||||
nock(userInfoEndpointUrl.origin)
|
||||
.get(userInfoEndpointUrl.pathname)
|
||||
|
@ -138,10 +149,10 @@ describe('getUserInfo', () => {
|
|||
it('throws SocialAccessTokenInvalid error if response code is 401', async () => {
|
||||
nock(userInfoEndpointUrl.origin)
|
||||
.get(userInfoEndpointUrl.pathname)
|
||||
.query(new URLSearchParams({ access_token: 'accessToken' }))
|
||||
.query(new URLSearchParams({ access_token: 'wrongAccessToken', openid: 'openid' }))
|
||||
.reply(401);
|
||||
await expect(
|
||||
weChatNativeMethods.getUserInfo({ accessToken: 'accessToken' })
|
||||
weChatNativeMethods.getUserInfo({ accessToken: 'wrongAccessToken', openid: 'openid' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -80,11 +80,17 @@ export default class WeChatNativeConnector implements SocialConnector {
|
|||
return { accessToken, openid };
|
||||
};
|
||||
|
||||
// FIXME:
|
||||
// eslint-disable-next-line complexity
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
const { accessToken, openid } = accessTokenObject;
|
||||
|
||||
// 'openid' is defined as a required input argument in WeChat API doc, but it does not necessarily have to
|
||||
// be the return value from getAccessToken per testing.
|
||||
// In other words, 'openid' is required but the response of getUserInfo is consistent as long as
|
||||
// access_token is valid.
|
||||
// We are expecting to get 41009 'missing openid' response according to the developers doc, but the
|
||||
// fact is that we still got 40001 'invalid credentials' response.
|
||||
assert(openid, new Error('`openid` is required by WeChat API.'));
|
||||
|
||||
try {
|
||||
const { unionid, headimgurl, nickname, errcode, errmsg } = await got
|
||||
.get(userInfoEndpoint, {
|
||||
|
@ -93,25 +99,19 @@ export default class WeChatNativeConnector implements SocialConnector {
|
|||
})
|
||||
.json<UserInfoResponse>();
|
||||
|
||||
if (!openid || errcode || errmsg) {
|
||||
// 'openid' is defined as a required input argument in WeChat API doc, but it does not necessarily to
|
||||
// be the return value from getAccessToken per testing.
|
||||
// In another word, 'openid' is required but the response of getUserInfo is consistent as long as
|
||||
// access_token is valid.
|
||||
// We are expecting to get 41009 'missing openid' response according to the developers doc, but the
|
||||
// fact is that we still got 40001 'invalid credentials' response.
|
||||
if (errcode === 40_001) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
// Response properties of user info can be separated into two groups: (1) {unionid, headimgurl, nickname}, (2) {errcode, errmsg}.
|
||||
// These two groups are mutually exclusive: if group (1) is not empty, group (2) should be empty and vice versa.
|
||||
// 'errmsg' and 'errcode' turn to non-empty values or empty values at the same time. Hence, if 'errmsg' is non-empty then 'errcode' should be non-empty.
|
||||
|
||||
throw new Error(errmsg);
|
||||
}
|
||||
assert(errcode !== 40_001, new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
assert(!errcode, new Error(errmsg));
|
||||
|
||||
return { id: unionid ?? openid, avatar: headimgurl, name: nickname };
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof GotRequestError && error.response?.statusCode === 401) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
assert(
|
||||
!(error instanceof GotRequestError && error.response?.statusCode === 401),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
|
|
@ -108,6 +108,17 @@ describe('getUserInfo', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('throws error if `openid` is missing', async () => {
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(0, {
|
||||
unionid: 'this_is_an_arbitrary_wechat_union_id',
|
||||
headimgurl: 'https://github.com/images/error/octocat_happy.gif',
|
||||
nickname: 'wechat bot',
|
||||
});
|
||||
await expect(weChatMethods.getUserInfo({ accessToken: 'accessToken' })).rejects.toMatchError(
|
||||
new Error('`openid` is required by WeChat API.')
|
||||
);
|
||||
});
|
||||
|
||||
it('throws SocialAccessTokenInvalid error if errcode is 40001', async () => {
|
||||
nock(userInfoEndpointUrl.origin)
|
||||
.get(userInfoEndpointUrl.pathname)
|
||||
|
@ -138,10 +149,10 @@ describe('getUserInfo', () => {
|
|||
it('throws SocialAccessTokenInvalid error if response code is 401', async () => {
|
||||
nock(userInfoEndpointUrl.origin)
|
||||
.get(userInfoEndpointUrl.pathname)
|
||||
.query(new URLSearchParams({ access_token: 'accessToken' }))
|
||||
.query(new URLSearchParams({ access_token: 'wrongAccessToken', openid: 'openid' }))
|
||||
.reply(401);
|
||||
await expect(weChatMethods.getUserInfo({ accessToken: 'accessToken' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
await expect(
|
||||
weChatMethods.getUserInfo({ accessToken: 'wrongAccessToken', openid: 'openid' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -79,11 +79,17 @@ export default class WeChatConnector implements SocialConnector {
|
|||
return { accessToken, openid };
|
||||
};
|
||||
|
||||
// FIXME:
|
||||
// eslint-disable-next-line complexity
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
const { accessToken, openid } = accessTokenObject;
|
||||
|
||||
// 'openid' is defined as a required input argument in WeChat API doc, but it does not necessarily have to
|
||||
// be the return value from getAccessToken per testing.
|
||||
// In other words, 'openid' is required but the response of getUserInfo is consistent as long as
|
||||
// access_token is valid.
|
||||
// We are expecting to get 41009 'missing openid' response according to the developers doc, but the
|
||||
// fact is that we still got 40001 'invalid credentials' response.
|
||||
assert(openid, new Error('`openid` is required by WeChat API.'));
|
||||
|
||||
try {
|
||||
const { unionid, headimgurl, nickname, errcode, errmsg } = await got
|
||||
.get(userInfoEndpoint, {
|
||||
|
@ -92,25 +98,19 @@ export default class WeChatConnector implements SocialConnector {
|
|||
})
|
||||
.json<UserInfoResponse>();
|
||||
|
||||
if (!openid || errcode || errmsg) {
|
||||
// 'openid' is defined as a required input argument in WeChat API doc, but it does not necessarily to
|
||||
// be the return value from getAccessToken per testing.
|
||||
// In another word, 'openid' is required but the response of getUserInfo is consistent as long as
|
||||
// access_token is valid.
|
||||
// We are expecting to get 41009 'missing openid' response according to the developers doc, but the
|
||||
// fact is that we still got 40001 'invalid credentials' response.
|
||||
if (errcode === 40_001) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
// Response properties of user info can be separated into two groups: (1) {unionid, headimgurl, nickname}, (2) {errcode, errmsg}.
|
||||
// These two groups are mutually exclusive: if group (1) is not empty, group (2) should be empty and vice versa.
|
||||
// 'errmsg' and 'errcode' turn to non-empty values or empty values at the same time. Hence, if 'errmsg' is non-empty then 'errcode' should be non-empty.
|
||||
|
||||
throw new Error(errmsg);
|
||||
}
|
||||
assert(errcode !== 40_001, new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
assert(!errcode, new Error(errmsg));
|
||||
|
||||
return { id: unionid ?? openid, avatar: headimgurl, name: nickname };
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof GotRequestError && error.response?.statusCode === 401) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
assert(
|
||||
!(error instanceof GotRequestError && error.response?.statusCode === 401),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
|
|
@ -1,47 +1,18 @@
|
|||
/* eslint-disable max-lines, max-nested-callbacks */
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
EmailMessageTypes,
|
||||
SmsMessageTypes,
|
||||
ValidateConfig,
|
||||
} from '@logto/connector-types';
|
||||
import { EmailMessageTypes, ValidateConfig } from '@logto/connector-types';
|
||||
import { Connector, ConnectorType } from '@logto/schemas';
|
||||
|
||||
import { mockConnectorInstanceList, mockConnectorList } from '@/__mocks__';
|
||||
import { mockConnectorInstanceList, mockMetadata, mockConnector } from '@/__mocks__';
|
||||
import {
|
||||
ConnectorMetadata,
|
||||
EmailConnectorInstance,
|
||||
SmsConnectorInstance,
|
||||
} from '@/connectors/types';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { updateConnector } from '@/queries/connector';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import connectorRoutes from './connector';
|
||||
|
||||
const mockMetadata: ConnectorMetadata = {
|
||||
id: 'id0',
|
||||
target: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Connector',
|
||||
'zh-CN': '连接器',
|
||||
},
|
||||
logo: './logo.png',
|
||||
description: { en: 'Connector', 'zh-CN': '连接器' },
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
};
|
||||
const mockConnector: Connector = {
|
||||
id: 'id0',
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
};
|
||||
|
||||
type ConnectorInstance = {
|
||||
connector: Connector;
|
||||
metadata: ConnectorMetadata;
|
||||
|
@ -49,21 +20,26 @@ type ConnectorInstance = {
|
|||
sendMessage?: unknown;
|
||||
};
|
||||
|
||||
const getConnectorInstanceByIdPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
(connectorId: string) => Promise<ConnectorInstance>
|
||||
>;
|
||||
const getConnectorInstancesPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<ConnectorInstance[]>
|
||||
>;
|
||||
|
||||
jest.mock('@/queries/connector', () => ({
|
||||
findAllConnectors: jest.fn(),
|
||||
updateConnector: jest.fn(),
|
||||
}));
|
||||
jest.mock('@/connectors', () => ({
|
||||
getConnectorInstanceById: async (connectorId: string) =>
|
||||
getConnectorInstanceByIdPlaceHolder(connectorId),
|
||||
getConnectorInstances: async () => getConnectorInstancesPlaceHolder(),
|
||||
getConnectorInstanceById: async (connectorId: string) => {
|
||||
const connectorInstances = await getConnectorInstancesPlaceHolder();
|
||||
const connector = connectorInstances.find(({ connector }) => connector.id === connectorId);
|
||||
assertThat(
|
||||
connector,
|
||||
new RequestError({
|
||||
code: 'entity.not_found',
|
||||
connectorId,
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
|
||||
return connector;
|
||||
},
|
||||
}));
|
||||
|
||||
describe('connector route', () => {
|
||||
|
@ -107,353 +83,20 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('throws when connector can not be found by given connectorId (locally)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
const found = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.connector.id === 'connector'
|
||||
);
|
||||
assertThat(found, new RequestError({ code: 'entity.not_found', status: 404 }));
|
||||
|
||||
return found;
|
||||
});
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce(mockConnectorInstanceList.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 () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (id: string) => {
|
||||
const foundConnectorInstance = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.connector.id === id
|
||||
);
|
||||
assertThat(
|
||||
foundConnectorInstance,
|
||||
new RequestError({ code: 'entity.not_found', status: 404 })
|
||||
);
|
||||
|
||||
const foundConnector = mockConnectorList.find((connector) => connector.id === 'connector0');
|
||||
assertThat(foundConnector, 'entity.not_found');
|
||||
|
||||
return { foundConnector, ...foundConnectorInstance };
|
||||
});
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([]);
|
||||
const response = await connectorRequest.get('/connectors/id0').send({});
|
||||
expect(response).toHaveProperty('statusCode', 400);
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('shows found connector information', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (id: string) => {
|
||||
const foundConnectorInstance = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.connector.id === id
|
||||
);
|
||||
assertThat(
|
||||
foundConnectorInstance,
|
||||
new RequestError({ code: 'entity.not_found', status: 404 })
|
||||
);
|
||||
|
||||
const foundConnector = mockConnectorList.find((connector) => connector.id === id);
|
||||
assertThat(foundConnector, 'entity.not_found');
|
||||
|
||||
return { foundConnector, ...foundConnectorInstance };
|
||||
});
|
||||
const response = await connectorRequest.get('/connectors/id0').send({});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /connectors/:id/enabled', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('throws if connector can not be found (locally)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
const found = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.connector.id === 'connector'
|
||||
);
|
||||
assertThat(found, new RequestError({ code: 'entity.not_found', status: 404 }));
|
||||
|
||||
return found;
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/findConnector/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('throws if connector can not be found (remotely)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (id: string) => {
|
||||
const foundConnectorInstance = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.connector.id === id
|
||||
);
|
||||
assertThat(
|
||||
foundConnectorInstance,
|
||||
new RequestError({ code: 'entity.not_found', status: 404 })
|
||||
);
|
||||
|
||||
const foundConnector = mockConnectorList.find((connector) => connector.id === 'connector0');
|
||||
assertThat(foundConnector, 'entity.not_found');
|
||||
|
||||
return { foundConnector, ...foundConnectorInstance };
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id0/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(response).toHaveProperty('statusCode', 400);
|
||||
});
|
||||
|
||||
it('enables one of the social connectors (with valid config)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/connector_0/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'connector_0' },
|
||||
set: { enabled: true },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: mockMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
||||
it('enables one of the social connectors (with invalid config)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
},
|
||||
};
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/connector_0/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(response).toHaveProperty('statusCode', 500);
|
||||
});
|
||||
|
||||
it('disables one of the social connectors', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
},
|
||||
};
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/connector_0/enabled')
|
||||
.send({ enabled: false });
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'connector_0' },
|
||||
set: { enabled: false },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: mockMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
||||
it('enables one of the email/sms connectors (with valid config)', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce(mockConnectorInstanceList);
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
id: 'id1',
|
||||
type: ConnectorType.SMS,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
id: 'id1',
|
||||
name: 'connector_1',
|
||||
platform: null,
|
||||
type: ConnectorType.SMS,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id1/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(updateConnector).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
where: { id: 'id1' },
|
||||
set: { enabled: false },
|
||||
})
|
||||
);
|
||||
expect(updateConnector).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
where: { id: 'id5' },
|
||||
set: { enabled: false },
|
||||
})
|
||||
);
|
||||
expect(updateConnector).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
where: { id: 'id1' },
|
||||
set: { enabled: true },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: mockedMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
||||
it('enables one of the email/sms connectors (with invalid config)', async () => {
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
id: 'connector_1',
|
||||
name: 'connector_1',
|
||||
platform: null,
|
||||
type: ConnectorType.SMS,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
},
|
||||
};
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/connector_1/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(response).toHaveProperty('statusCode', 500);
|
||||
});
|
||||
|
||||
it('disables one of the email/sms connectors', async () => {
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
id: 'connector_4',
|
||||
type: ConnectorType.Email,
|
||||
platform: null,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
id: 'connector_4',
|
||||
name: 'connector_4',
|
||||
platform: null,
|
||||
type: ConnectorType.Email,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/connector_4/enabled')
|
||||
.send({ enabled: false });
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'connector_4' },
|
||||
set: { enabled: false },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: mockedMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /connectors/:id', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('throws when connector can not be found by given connectorId (locally)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
const found = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.connector.id === 'connector'
|
||||
);
|
||||
assertThat(found, new RequestError({ code: 'entity.not_found', status: 404 }));
|
||||
|
||||
return found;
|
||||
});
|
||||
const response = await connectorRequest.patch('/connectors/findConnector').send({});
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('throws when connector can not be found by given connectorId (remotely)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (id: string) => {
|
||||
const foundConnectorInstance = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.connector.id === id
|
||||
);
|
||||
assertThat(
|
||||
foundConnectorInstance,
|
||||
new RequestError({ code: 'entity.not_found', status: 404 })
|
||||
);
|
||||
|
||||
const foundConnector = mockConnectorList.find((connector) => connector.id === 'id_0');
|
||||
assertThat(foundConnector, 'entity.not_found');
|
||||
|
||||
return { foundConnector, ...foundConnectorInstance };
|
||||
});
|
||||
const response = await connectorRequest.patch('/connectors/id0').send({});
|
||||
expect(response).toHaveProperty('statusCode', 400);
|
||||
});
|
||||
|
||||
it('config validation fails', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
},
|
||||
};
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/connector_0')
|
||||
.send({ config: { cliend_id: 'client_id', client_secret: 'client_secret' } });
|
||||
expect(response).toHaveProperty('statusCode', 500);
|
||||
});
|
||||
|
||||
it('successfully updates connector configs', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/connector_0')
|
||||
.send({ config: { cliend_id: 'client_id', client_secret: 'client_secret' } });
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'connector_0' },
|
||||
set: { config: { cliend_id: 'client_id', client_secret: 'client_secret' } },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: mockMetadata,
|
||||
});
|
||||
const response = await connectorRequest.get('/connectors/id0').send({});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
});
|
||||
|
@ -464,18 +107,9 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should get email connector and send message', async () => {
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
type: ConnectorType.Email,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
type: ConnectorType.Email,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
const mockedEmailConnector: EmailConnectorInstance = {
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
sendMessage: async (
|
||||
|
@ -485,9 +119,7 @@ describe('connector route', () => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
): Promise<any> => {},
|
||||
};
|
||||
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([mockedEmailConnector]);
|
||||
|
||||
const sendMessageSpy = jest.spyOn(mockedEmailConnector, 'sendMessage');
|
||||
const response = await connectorRequest
|
||||
.post('/connectors/test/email')
|
||||
|
@ -500,29 +132,7 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should throw when email connector is not found', async () => {
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
type: ConnectorType.SMS,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
type: ConnectorType.SMS,
|
||||
};
|
||||
const mockedSmsConnector: SmsConnectorInstance = {
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
sendMessage: async (
|
||||
address: string,
|
||||
type: keyof SmsMessageTypes,
|
||||
_payload: SmsMessageTypes[typeof type]
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
): Promise<any> => {},
|
||||
};
|
||||
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([mockedSmsConnector]);
|
||||
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([]);
|
||||
const response = await connectorRequest
|
||||
.post('/connectors/test/email')
|
||||
.send({ email: 'test@email.com' });
|
||||
|
@ -540,13 +150,8 @@ describe('connector route', () => {
|
|||
...mockMetadata,
|
||||
type: ConnectorType.SMS,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
type: ConnectorType.SMS,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
const mockedSmsConnectorInstance: SmsConnectorInstance = {
|
||||
connector: mockedConnector,
|
||||
connector: mockConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
|
@ -557,9 +162,7 @@ describe('connector route', () => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
): Promise<any> => {},
|
||||
};
|
||||
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([mockedSmsConnectorInstance]);
|
||||
|
||||
const sendMessageSpy = jest.spyOn(mockedSmsConnectorInstance, 'sendMessage');
|
||||
const response = await connectorRequest
|
||||
.post('/connectors/test/sms')
|
||||
|
@ -572,30 +175,7 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should throw when sms connector is not found', async () => {
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
type: ConnectorType.Email,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
type: ConnectorType.Email,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
const mockedEmailConnectorInstance: EmailConnectorInstance = {
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
sendMessage: async (
|
||||
address: string,
|
||||
type: keyof EmailMessageTypes,
|
||||
_payload: EmailMessageTypes[typeof type]
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
): Promise<any> => {},
|
||||
};
|
||||
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([mockedEmailConnectorInstance]);
|
||||
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([]);
|
||||
const response = await connectorRequest
|
||||
.post('/connectors/test/sms')
|
||||
.send({ phone: '12345678901' });
|
||||
|
@ -603,4 +183,3 @@ describe('connector route', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
/* eslint-enable max-lines, max-nested-callbacks */
|
||||
|
|
|
@ -124,9 +124,7 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
'/connectors/:id',
|
||||
koaGuard({
|
||||
params: object({ id: string().min(1) }),
|
||||
body: Connectors.createGuard
|
||||
.omit({ id: true, type: true, enabled: true, createdAt: true })
|
||||
.partial(),
|
||||
body: Connectors.createGuard.omit({ id: true, enabled: true, createdAt: true }).partial(),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
|
|
281
packages/core/src/routes/connector.update.test.ts
Normal file
281
packages/core/src/routes/connector.update.test.ts
Normal file
|
@ -0,0 +1,281 @@
|
|||
import { ConnectorError, ConnectorErrorCodes, ValidateConfig } from '@logto/connector-types';
|
||||
import { Connector, ConnectorType } from '@logto/schemas';
|
||||
|
||||
import { mockConnectorInstanceList, mockMetadata, mockConnector } from '@/__mocks__';
|
||||
import { ConnectorMetadata } from '@/connectors/types';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { updateConnector } from '@/queries/connector';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import connectorRoutes from './connector';
|
||||
|
||||
type ConnectorInstance = {
|
||||
connector: Connector;
|
||||
metadata: ConnectorMetadata;
|
||||
validateConfig?: ValidateConfig;
|
||||
sendMessage?: unknown;
|
||||
};
|
||||
|
||||
const getConnectorInstancesPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<ConnectorInstance[]>
|
||||
>;
|
||||
const getConnectorInstanceByIdPlaceHolder = jest.fn(async (connectorId: string) => {
|
||||
const connectorInstances = await getConnectorInstancesPlaceHolder();
|
||||
const connector = connectorInstances.find(({ connector }) => connector.id === connectorId);
|
||||
assertThat(
|
||||
connector,
|
||||
new RequestError({
|
||||
code: 'entity.not_found',
|
||||
connectorId,
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...connector,
|
||||
validateConfig: validateConfigPlaceHolder,
|
||||
sendMessage: sendMessagePlaceHolder,
|
||||
};
|
||||
});
|
||||
const validateConfigPlaceHolder = jest.fn();
|
||||
const sendMessagePlaceHolder = jest.fn();
|
||||
|
||||
jest.mock('@/queries/connector', () => ({
|
||||
updateConnector: jest.fn(),
|
||||
}));
|
||||
jest.mock('@/connectors', () => ({
|
||||
getConnectorInstances: async () => getConnectorInstancesPlaceHolder(),
|
||||
getConnectorInstanceById: async (connectorId: string) =>
|
||||
getConnectorInstanceByIdPlaceHolder(connectorId),
|
||||
}));
|
||||
|
||||
describe('connector PATCH routes', () => {
|
||||
const connectorRequest = createRequester({ authedRoutes: connectorRoutes });
|
||||
|
||||
describe('PATCH /connectors/:id/enabled', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('throws if connector can not be found (locally)', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce(mockConnectorInstanceList.slice(1));
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/findConnector/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('throws if connector can not be found (remotely)', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([]);
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id0/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('enables one of the social connectors (with valid config)', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
connector: mockConnector,
|
||||
metadata: { ...mockMetadata, type: ConnectorType.Social },
|
||||
},
|
||||
]);
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'id' },
|
||||
set: { enabled: true },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: { ...mockMetadata, type: ConnectorType.Social },
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
||||
it('enables one of the social connectors (with invalid config)', async () => {
|
||||
validateConfigPlaceHolder.mockImplementationOnce(async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
});
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
},
|
||||
]);
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(response).toHaveProperty('statusCode', 500);
|
||||
});
|
||||
|
||||
it('disables one of the social connectors', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
},
|
||||
]);
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id/enabled')
|
||||
.send({ enabled: false });
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'id' },
|
||||
set: { enabled: false },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: mockMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
||||
it('enables one of the email/sms connectors (with valid config)', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce(mockConnectorInstanceList);
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
id: 'id1',
|
||||
type: ConnectorType.SMS,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
id: 'id1',
|
||||
};
|
||||
getConnectorInstanceByIdPlaceHolder.mockResolvedValueOnce({
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
sendMessage: jest.fn(),
|
||||
});
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id1/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
expect(updateConnector).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
where: { id: 'id1' },
|
||||
set: { enabled: false },
|
||||
})
|
||||
);
|
||||
expect(updateConnector).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
where: { id: 'id5' },
|
||||
set: { enabled: false },
|
||||
})
|
||||
);
|
||||
expect(updateConnector).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
where: { id: 'id1' },
|
||||
set: { enabled: true },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: mockedMetadata,
|
||||
});
|
||||
});
|
||||
|
||||
it('enables one of the email/sms connectors (with invalid config)', async () => {
|
||||
validateConfigPlaceHolder.mockImplementationOnce(async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
});
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
connector: mockConnector,
|
||||
metadata: {
|
||||
...mockMetadata,
|
||||
type: ConnectorType.SMS,
|
||||
},
|
||||
},
|
||||
]);
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id/enabled')
|
||||
.send({ enabled: true });
|
||||
expect(response).toHaveProperty('statusCode', 500);
|
||||
});
|
||||
|
||||
it('disables one of the email/sms connectors', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
},
|
||||
]);
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id/enabled')
|
||||
.send({ enabled: false });
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'id' },
|
||||
set: { enabled: false },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: mockMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /connectors/:id', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('throws when connector can not be found by given connectorId (locally)', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce(mockConnectorInstanceList.slice(0, 1));
|
||||
const response = await connectorRequest.patch('/connectors/findConnector').send({});
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('throws when connector can not be found by given connectorId (remotely)', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([]);
|
||||
const response = await connectorRequest.patch('/connectors/id0').send({});
|
||||
expect(response).toHaveProperty('statusCode', 404);
|
||||
});
|
||||
|
||||
it('config validation fails', async () => {
|
||||
validateConfigPlaceHolder.mockImplementationOnce(async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
});
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
},
|
||||
]);
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id')
|
||||
.send({ config: { cliend_id: 'client_id', client_secret: 'client_secret' } });
|
||||
expect(response).toHaveProperty('statusCode', 500);
|
||||
});
|
||||
|
||||
it('successfully updates connector configs', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
},
|
||||
]);
|
||||
const response = await connectorRequest
|
||||
.patch('/connectors/id')
|
||||
.send({ config: { cliend_id: 'client_id', client_secret: 'client_secret' } });
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'id' },
|
||||
set: { config: { cliend_id: 'client_id', client_secret: 'client_secret' } },
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: mockMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue