From 7f0aa8978641bd34693d20a5574f12fa2845469f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 29 Aug 2021 16:58:46 +0200 Subject: [PATCH] integration-react: more tests for ScmAuth + some redesign Signed-off-by: Patrik Oldsberg --- .../integration-react/src/api/ScmAuth.test.ts | 136 ++++++++++++++++-- packages/integration-react/src/api/ScmAuth.ts | 56 ++++---- .../integration-react/src/api/ScmAuthApi.ts | 4 +- 3 files changed, 148 insertions(+), 48 deletions(-) diff --git a/packages/integration-react/src/api/ScmAuth.test.ts b/packages/integration-react/src/api/ScmAuth.test.ts index ea0261f94b..22bdbdfa87 100644 --- a/packages/integration-react/src/api/ScmAuth.test.ts +++ b/packages/integration-react/src/api/ScmAuth.test.ts @@ -20,26 +20,20 @@ import { ScmAuth } from './ScmAuth'; class MockOAuthApi implements OAuthApi { constructor(private readonly accessToken: string) {} - async getAccessToken() { + getAccessToken = jest.fn(async () => { return this.accessToken; - } + }); } -const mockApis = { - github: new MockOAuthApi('github-access-token'), - ghe: new MockOAuthApi('ghe-access-token'), -}; - describe('ScmAuth', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - it('should provide credentials for GitHub and GHE', async () => { + const mockGithubAuth = new MockOAuthApi('github-access-token'); + const mockGheAuth = new MockOAuthApi('ghe-access-token'); + const api = ScmAuth.mux( - ScmAuth.forGithub(mockApis.github), - ScmAuth.forGithub(mockApis.ghe, { - hostname: 'ghe.example.com', + ScmAuth.forGithub(mockGithubAuth), + ScmAuth.forGithub(mockGheAuth, { + host: 'ghe.example.com', }), ); @@ -54,6 +48,9 @@ describe('ScmAuth', () => { await expect( api.getCredentials({ url: 'https://ghe.example.com/backstage/backstage', + additionalScope: { + repoWrite: true, + }, }), ).resolves.toEqual({ token: 'ghe-access-token', @@ -61,5 +58,116 @@ describe('ScmAuth', () => { Authorization: 'Bearer ghe-access-token', }, }); + + expect(mockGithubAuth.getAccessToken).toHaveBeenCalledTimes(1); + expect(mockGithubAuth.getAccessToken).toHaveBeenCalledWith( + ['repo', 'read:org', 'read:user'], + {}, + ); + expect(mockGheAuth.getAccessToken).toHaveBeenCalledTimes(1); + expect(mockGheAuth.getAccessToken).toHaveBeenCalledWith( + ['repo', 'read:org', 'read:user', 'gist'], + {}, + ); + }); + + it('should use correct scopes for each provider', async () => { + const mockAuthApi = { + getAccessToken: async (scopes: string[]) => { + return scopes.join(' '); + }, + }; + + const githubAuth = ScmAuth.forGithub(mockAuthApi); + await expect( + githubAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ + token: 'repo read:org read:user', + }); + await expect( + githubAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { repoWrite: true }, + }), + ).resolves.toMatchObject({ + token: 'repo read:org read:user gist', + }); + + const gitlabAuth = ScmAuth.forGitlab(mockAuthApi); + await expect( + gitlabAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ + token: 'read_user read_api read_repository', + }); + await expect( + gitlabAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { repoWrite: true }, + }), + ).resolves.toMatchObject({ + token: 'read_user read_api write_repository api', + }); + + const azureAuth = ScmAuth.forAzure(mockAuthApi); + await expect( + azureAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ + token: 'vso.build vso.code vso.graph vso.project vso.profile', + }); + await expect( + azureAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { repoWrite: true }, + }), + ).resolves.toMatchObject({ + token: 'vso.build vso.code_manage vso.graph vso.project vso.profile', + }); + + const bitbucketAuth = ScmAuth.forBitbucket(mockAuthApi); + await expect( + bitbucketAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ + token: 'account team pullrequest snippet issue', + }); + await expect( + bitbucketAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { repoWrite: true }, + }), + ).resolves.toMatchObject({ + token: 'account team pullrequest:write snippet:write issue:write', + }); + }); + + it('should throw an error for unknown URLs', async () => { + const emptyMux = ScmAuth.mux(); + await expect( + emptyMux.getCredentials({ url: 'http://example.com' }), + ).rejects.toThrow( + "No authentication provider available for access to 'http://example.com'", + ); + + const scmAuth = ScmAuth.mux( + ScmAuth.forAuthApi(new MockOAuthApi('token'), { + host: 'example.com', + scopeMapping: { + default: ['a'], + repoWrite: ['b'], + }, + }), + ); + await expect( + scmAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ token: 'token' }); + await expect( + scmAuth.getCredentials({ url: 'http://not.example.com' }), + ).rejects.toThrow( + "No authentication provider available for access to 'http://not.example.com'", + ); + await expect( + scmAuth.getCredentials({ url: 'http://example.com:8080' }), + ).rejects.toThrow( + "No authentication provider available for access to 'http://example.com:8080'", + ); }); }); diff --git a/packages/integration-react/src/api/ScmAuth.ts b/packages/integration-react/src/api/ScmAuth.ts index e7cbc91942..4aceeea712 100644 --- a/packages/integration-react/src/api/ScmAuth.ts +++ b/packages/integration-react/src/api/ScmAuth.ts @@ -33,14 +33,14 @@ class ScmAuthMux implements ScmAuthApi { this.#providers = providers; } - getCredentials( + async getCredentials( options: ScmAuthTokenOptions, - ): Promise { + ): Promise { const url = new URL(options.url); const provider = this.#providers.find(p => p.isUrlSupported(url)); if (!provider) { throw new Error( - `No authentication provider available for SCM access to '${options.url}'`, + `No authentication provider available for access to '${options.url}'`, ); } @@ -52,24 +52,24 @@ export class ScmAuth implements ScmAuthApi { static forAuthApi( authApi: OAuthApi, options: { - hostname: string; + host: string; scopeMapping: { default: string[]; repoWrite: string[]; }; }, - ): ScmAuthApi { - return new ScmAuth(authApi, options.hostname, options.scopeMapping); + ): ScmAuth { + return new ScmAuth(authApi, options.host, options.scopeMapping); } static forGithub( githubAuthApi: OAuthApi, options?: { - hostname?: string; + host?: string; }, ): ScmAuth { - const hostname = options?.hostname ?? 'github.com'; - return new ScmAuth(githubAuthApi, hostname, { + const host = options?.host ?? 'github.com'; + return new ScmAuth(githubAuthApi, host, { default: ['repo', 'read:org', 'read:user'], repoWrite: ['repo', 'read:org', 'read:user', 'gist'], }); @@ -78,11 +78,11 @@ export class ScmAuth implements ScmAuthApi { static forGitlab( gitlabAuthApi: OAuthApi, options?: { - hostname?: string; + host?: string; }, ): ScmAuth { - const hostname = options?.hostname ?? 'gitlab.com'; - return new ScmAuth(gitlabAuthApi, hostname, { + const host = options?.host ?? 'gitlab.com'; + return new ScmAuth(gitlabAuthApi, host, { default: ['read_user', 'read_api', 'read_repository'], repoWrite: ['read_user', 'read_api', 'write_repository', 'api'], }); @@ -91,11 +91,11 @@ export class ScmAuth implements ScmAuthApi { static forAzure( microsoftAuthApiRef: OAuthApi, options?: { - hostname?: string; + host?: string; }, ): ScmAuth { - const hostname = options?.hostname ?? 'dev.azure.com'; - return new ScmAuth(microsoftAuthApiRef, hostname, { + const host = options?.host ?? 'dev.azure.com'; + return new ScmAuth(microsoftAuthApiRef, host, { default: [ 'vso.build', 'vso.code', @@ -116,11 +116,11 @@ export class ScmAuth implements ScmAuthApi { static forBitbucket( bitbucketAuthApi: OAuthApi, options?: { - hostname?: string; + host?: string; }, ): ScmAuth { - const hostname = options?.hostname ?? 'bitbucket.org'; - return new ScmAuth(bitbucketAuthApi, hostname, { + const host = options?.host ?? 'bitbucket.org'; + return new ScmAuth(bitbucketAuthApi, host, { default: ['account', 'team', 'pullrequest', 'snippet', 'issue'], repoWrite: [ 'account', @@ -137,16 +137,12 @@ export class ScmAuth implements ScmAuthApi { } #api: OAuthApi; - #hostname: string; + #host: string; #scopeMapping: ScopeMapping; - private constructor( - api: OAuthApi, - hostname: string, - scopeMapping: ScopeMapping, - ) { + private constructor(api: OAuthApi, host: string, scopeMapping: ScopeMapping) { this.#api = api; - this.#hostname = hostname; + this.#host = host; this.#scopeMapping = scopeMapping; } @@ -154,20 +150,18 @@ export class ScmAuth implements ScmAuthApi { * Checks whether the implementation is able to provide authentication for the given URL. */ isUrlSupported(url: URL): boolean { - return url.hostname === this.#hostname; + return url.host === this.#host; } async getCredentials( options: ScmAuthTokenOptions, ): Promise { - const scopes = options.additionalScope?.repoWrite + const { url, additionalScope, ...restOptions } = options; + const scopes = additionalScope?.repoWrite ? this.#scopeMapping.repoWrite : this.#scopeMapping.default; - const token = await this.#api.getAccessToken(scopes, { - instantPopup: options.instantPopup, - optional: options.optional, - }); + const token = await this.#api.getAccessToken(scopes, restOptions); return { token, headers: { diff --git a/packages/integration-react/src/api/ScmAuthApi.ts b/packages/integration-react/src/api/ScmAuthApi.ts index 427b1e2119..c526d4502f 100644 --- a/packages/integration-react/src/api/ScmAuthApi.ts +++ b/packages/integration-react/src/api/ScmAuthApi.ts @@ -64,9 +64,7 @@ export interface ScmAuthApi { /** * Requests credentials for accessing an SCM resource. */ - getCredentials( - options: ScmAuthTokenOptions, - ): Promise; + getCredentials(options: ScmAuthTokenOptions): Promise; } export const scmAuthApiRef: ApiRef = createApiRef({