0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00
logto/packages/connectors/connector-github/src/index.test.ts
Darcy Ye c95755502d
fix(connector): fix GitHub connector GET /emails forbidden error (#5925)
* fix(connector): fix GitHub connector GET /emails forbidden error

* chore: adopt suggestion

Co-authored-by: Gao Sun <gao@silverhand.io>

---------

Co-authored-by: Gao Sun <gao@silverhand.io>
2024-05-29 04:24:04 +00:00

274 lines
8.3 KiB
TypeScript

import nock from 'nock';
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
import {
accessTokenEndpoint,
authorizationEndpoint,
userEmailsEndpoint,
userInfoEndpoint,
} from './constant.js';
import createConnector, { getAccessToken } from './index.js';
import { mockedConfig } from './mock.js';
const getConfig = vi.fn().mockResolvedValue(mockedConfig);
describe('getAuthorizationUri', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('should get a valid uri by redirectUri and state', async () => {
const connector = await createConnector({ getConfig });
const authorizationUri = await connector.getAuthorizationUri(
{
state: 'some_state',
redirectUri: 'http://localhost:3000/callback',
connectorId: 'some_connector_id',
connectorFactoryId: 'some_connector_factory_id',
jti: 'some_jti',
headers: {},
},
vi.fn()
);
expect(authorizationUri).toEqual(
`${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&state=some_state&scope=read%3Auser+user%3Aemail`
);
});
});
describe('getAccessToken', () => {
afterEach(() => {
nock.cleanAll();
vi.clearAllMocks();
});
it('should get an accessToken by exchanging with code', async () => {
nock(accessTokenEndpoint).post('').reply(200, {
access_token: 'access_token',
scope: 'scope',
token_type: 'token_type',
});
const { accessToken } = await getAccessToken(mockedConfig, { code: 'code' });
expect(accessToken).toEqual('access_token');
});
it('throws SocialAuthCodeInvalid error if accessToken not found in response', async () => {
nock(accessTokenEndpoint)
.post('')
.reply(200, { access_token: '', scope: 'scope', token_type: 'token_type' });
await expect(getAccessToken(mockedConfig, { code: 'code' })).rejects.toStrictEqual(
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)
);
});
});
describe('getUserInfo', () => {
beforeEach(() => {
nock(accessTokenEndpoint).post('').query(true).reply(200, {
access_token: 'access_token',
scope: 'scope',
token_type: 'token_type',
});
});
afterEach(() => {
nock.cleanAll();
vi.clearAllMocks();
});
it('should get valid SocialUserInfo', async () => {
nock(userInfoEndpoint).get('').reply(200, {
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
email: 'octocat@github.com',
foo: 'bar',
});
nock(userEmailsEndpoint).get('').reply(200, []);
const connector = await createConnector({ getConfig });
const socialUserInfo = await connector.getUserInfo({ code: 'code' }, vi.fn());
expect(socialUserInfo).toStrictEqual({
id: '1',
avatar: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
email: 'octocat@github.com',
rawData: {
userInfo: {
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
email: 'octocat@github.com',
foo: 'bar',
},
userEmails: [],
},
});
});
it('should fallback to verified primary email if not public is available', async () => {
nock(userInfoEndpoint).get('').reply(200, {
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
email: undefined,
foo: 'bar',
});
nock(userEmailsEndpoint)
.get('')
.reply(200, [
{
email: 'foo@logto.io',
verified: true,
primary: true,
visibility: 'public',
},
{
email: 'foo1@logto.io',
verified: true,
primary: false,
visibility: null,
},
]);
const connector = await createConnector({ getConfig });
const socialUserInfo = await connector.getUserInfo({ code: 'code' }, vi.fn());
expect(socialUserInfo).toStrictEqual({
id: '1',
avatar: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
email: 'foo@logto.io',
rawData: {
userInfo: {
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
foo: 'bar',
},
userEmails: [
{
email: 'foo@logto.io',
verified: true,
primary: true,
visibility: 'public',
},
{
email: 'foo1@logto.io',
verified: true,
primary: false,
visibility: null,
},
],
},
});
});
it('should fallback to empty array when can not access to GET /users/emails endpoint', async () => {
nock(userInfoEndpoint).get('').reply(200, {
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
foo: 'bar',
});
nock(userEmailsEndpoint).get('').reply(403, []);
const connector = await createConnector({ getConfig });
const socialUserInfo = await connector.getUserInfo({ code: 'code' }, vi.fn());
expect(socialUserInfo).toStrictEqual({
id: '1',
avatar: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
email: undefined,
rawData: {
userInfo: {
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
foo: 'bar',
},
userEmails: [],
},
});
});
it('should convert `null` to `undefined` in SocialUserInfo', async () => {
nock(userInfoEndpoint).get('').reply(200, {
id: 1,
avatar_url: null,
name: null,
email: null,
});
nock(userEmailsEndpoint).get('').reply(200, []);
const connector = await createConnector({ getConfig });
const socialUserInfo = await connector.getUserInfo({ code: 'code' }, vi.fn());
expect(socialUserInfo).toMatchObject({
id: '1',
rawData: {
userInfo: { id: 1, avatar_url: null, name: null, email: null },
userEmails: [],
},
});
});
it('throws SocialAccessTokenInvalid error if remote response code is 401', async () => {
nock(userInfoEndpoint).get('').reply(401);
const connector = await createConnector({ getConfig });
await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual(
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
);
});
it('throws AuthorizationFailed error if error is access_denied', async () => {
nock(userInfoEndpoint).get('').reply(200, {
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
email: 'octocat@github.com',
});
const connector = await createConnector({ getConfig });
await expect(
connector.getUserInfo(
{
error: 'access_denied',
error_description: 'The user has denied your application access.',
error_uri:
'https://docs.github.com/apps/troubleshooting-authorization-request-errors#access-denied',
},
vi.fn()
)
).rejects.toStrictEqual(
new ConnectorError(
ConnectorErrorCodes.AuthorizationFailed,
'The user has denied your application access.'
)
);
});
it('throws General error if error is not access_denied', async () => {
nock(userInfoEndpoint).get('').reply(200, {
id: 1,
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
name: 'monalisa octocat',
email: 'octocat@github.com',
});
const connector = await createConnector({ getConfig });
await expect(
connector.getUserInfo(
{
error: 'general_error',
error_description: 'General error encountered.',
},
vi.fn()
)
).rejects.toStrictEqual(
new ConnectorError(
ConnectorErrorCodes.General,
'{"error":"general_error","error_description":"General error encountered."}'
)
);
});
it('throws unrecognized error', async () => {
nock(userInfoEndpoint).get('').reply(500);
const connector = await createConnector({ getConfig });
await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toThrow();
});
});