import nock from 'nock'; import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit'; import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant.js'; import createConnector, { getAccessToken } from './index.js'; import { mockedConfig } from './mock.js'; const { jest } = import.meta; const getConfig = jest.fn().mockResolvedValue(mockedConfig); describe('getAuthorizationUri', () => { afterEach(() => { jest.clearAllMocks(); }); it('should get a valid uri', async () => { const connector = await createConnector({ getConfig }); const authorizationUri = await connector.getAuthorizationUri( { state: 'dummy-state', redirectUri: 'dummy-redirect-uri', connectorId: 'dummy-connector-id', connectorFactoryId: 'dummy-connector-factory-id', jti: 'dummy-jti', headers: {}, }, jest.fn() ); expect(authorizationUri).toEqual( `${authorizationEndpoint}?app_id=%3Capp-id%3E&state=dummy-state` ); }); }); describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); jest.clearAllMocks(); }); const accessTokenEndpointUrl = new URL(accessTokenEndpoint); const parameters = new URLSearchParams({ appid: '', secret: '', code: 'code', grant_type: 'authorization_code', }); it('should get an accessToken by exchanging with code', async () => { nock(accessTokenEndpointUrl.origin) .get(accessTokenEndpointUrl.pathname) .query(parameters) .reply(200, { access_token: 'access_token', openid: 'openid', }); const { accessToken, openid } = await getAccessToken('code', mockedConfig); expect(accessToken).toEqual('access_token'); expect(openid).toEqual('openid'); }); it('throws SocialAuthCodeInvalid error if errcode is 40029', async () => { nock(accessTokenEndpointUrl.origin) .get(accessTokenEndpointUrl.pathname) .query(parameters) .reply(200, { errcode: 40_029, errmsg: 'invalid code' }); await expect(getAccessToken('code', mockedConfig)).rejects.toMatchError( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'invalid code') ); }); it('throws SocialAuthCodeInvalid error if errcode is 40163', async () => { nock(accessTokenEndpointUrl.origin) .get(accessTokenEndpointUrl.pathname) .query(true) .reply(200, { errcode: 40_163, errmsg: 'code been used' }); await expect(getAccessToken('code', mockedConfig)).rejects.toMatchError( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'code been used') ); }); it('throws error with message otherwise', async () => { nock(accessTokenEndpointUrl.origin) .get(accessTokenEndpointUrl.pathname) .query(true) .reply(200, { errcode: -1, errmsg: 'system error' }); await expect(getAccessToken('wrong_code', mockedConfig)).rejects.toMatchError( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'system error', errcode: -1, }) ); }); }); const nockNoOpenIdAccessTokenResponse = () => { const accessTokenEndpointUrl = new URL(accessTokenEndpoint); const parameters = new URLSearchParams({ appid: '', secret: '', code: 'code', grant_type: 'authorization_code', }); nock(accessTokenEndpointUrl.origin) .get(accessTokenEndpointUrl.pathname) .query(parameters) .reply(200, { access_token: 'access_token', }); }; describe('getUserInfo', () => { beforeEach(() => { const accessTokenEndpointUrl = new URL(accessTokenEndpoint); const parameters = new URLSearchParams({ appid: '', secret: '', code: 'code', grant_type: 'authorization_code', }); nock(accessTokenEndpointUrl.origin) .get(accessTokenEndpointUrl.pathname) .query(parameters) .reply(200, { access_token: 'access_token', openid: 'openid', }); }); afterEach(() => { nock.cleanAll(); jest.clearAllMocks(); }); const userInfoEndpointUrl = new URL(userInfoEndpoint); const parameters = new URLSearchParams({ access_token: 'access_token', openid: 'openid' }); it('should get valid SocialUserInfo', 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', }); const connector = await createConnector({ getConfig }); const socialUserInfo = await connector.getUserInfo({ code: 'code' }, jest.fn()); expect(socialUserInfo).toMatchObject({ id: 'this_is_an_arbitrary_wechat_union_id', avatar: 'https://github.com/images/error/octocat_happy.gif', name: 'wechat bot', }); }); it('throws General error if code not provided in input', async () => { const connector = await createConnector({ getConfig }); await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError( new ConnectorError(ConnectorErrorCodes.General, '{}') ); }); it('throws error if `openid` is missing', async () => { nockNoOpenIdAccessTokenResponse(); nock(userInfoEndpointUrl.origin) .get(userInfoEndpointUrl.pathname) .query(parameters) .reply(200, { errcode: 41_009, errmsg: 'missing openid', }); const connector = await createConnector({ getConfig }); await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'missing openid', errcode: 41_009, }) ); }); it('throws SocialAccessTokenInvalid error if errcode is 40001', async () => { nock(userInfoEndpointUrl.origin) .get(userInfoEndpointUrl.pathname) .query(parameters) .reply(200, { errcode: 40_001, errmsg: 'invalid credential' }); const connector = await createConnector({ getConfig }); await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'invalid credential') ); }); it('throws unrecognized error', async () => { nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(500); const connector = await createConnector({ getConfig }); await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toThrow(); }); it('throws Error if request failed and errcode is not 40001', async () => { nock(userInfoEndpointUrl.origin) .get(userInfoEndpointUrl.pathname) .query(parameters) .reply(200, { errcode: 40_003, errmsg: 'invalid openid' }); const connector = await createConnector({ getConfig }); await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'invalid openid', errcode: 40_003, }) ); }); it('throws SocialAccessTokenInvalid error if response code is 401', async () => { nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(401); const connector = await createConnector({ getConfig }); await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid) ); }); });