diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 8df6079b9f..032344a4f4 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -507,7 +507,7 @@ export const providers: Readonly<{ | undefined, ) => AuthProviderFactory; resolvers: Readonly<{ - emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver; }>; }>; cfAccess: Readonly<{ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index a95465dbad..91fb5bed5b 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -78,6 +78,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@testing-library/jest-dom": "^5.16.5", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", @@ -89,6 +90,7 @@ "@types/passport-saml": "^1.1.3", "@types/passport-strategy": "^0.2.35", "@types/xml2js": "^0.4.7", + "cross-fetch": "^3.1.5", "msw": "^0.49.0", "supertest": "^6.1.3" }, diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts index 127ca71a17..3934cafd36 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts @@ -18,10 +18,16 @@ import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; import { AuthResolverContext } from '../types'; import { + bitbucketServer, BitbucketServerAuthProvider, BitbucketServerOAuthResult, } from './provider'; -import { commonByEmailResolver } from '../resolvers'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { fetch } from 'cross-fetch'; +import { rest } from 'msw'; + +global.fetch = fetch; jest.mock('../../lib/passport/PassportStrategyHelper', () => { return { @@ -51,60 +57,60 @@ const passportProfile = { photos: [{ value: 'https://bitbucket.org/user/123/avatar' }], }; -const mockFetchUserRequests = ( - failOnWhoAmI: boolean = false, - whoAmIValue: string = passportProfile.username, - failOnGetUser: boolean = false, - getUserOk: boolean = true, - avatarUrl: string = '/user/123/avatar', - setDisplayName: boolean = true, - setUserName: boolean = true, -) => { - const fetchMock = global.fetch as jest.Mock; - if (failOnWhoAmI) { - fetchMock.mockRejectedValueOnce(() => {}); - } else { - fetchMock.mockResolvedValueOnce({ - headers: { get: jest.fn(() => whoAmIValue) }, - }); - } - if (failOnGetUser) { - fetchMock.mockRejectedValueOnce(() => {}); - } else { - fetchMock.mockResolvedValueOnce({ - ok: getUserOk, - json: () => ({ - name: setUserName ? 'john.doe' : undefined, - emailAddress: 'john@doe.com', - id: 123, - displayName: setDisplayName ? 'John Doe' : undefined, - active: true, - slug: 'john.doe', - type: 'NORMAL', - links: { - self: [ - { - href: 'https://bitbucket.org/users/john.doe', - }, - ], - }, - avatarUrl: avatarUrl, - }), - }); - } -}; +const mockHost = 'bitbucket.org'; +const mockBaseUrl = `https://${mockHost}`; + +const whoAmIHandler = (options?: { fail?: boolean; value?: string }) => + rest.get( + `${mockBaseUrl}/plugins/servlet/applinks/whoami`, + (_req, res, ctx) => { + if (options?.fail) { + res.networkError('error'); + } + return res( + ctx.status(200), + ctx.set('X-Ausername', options?.value ?? passportProfile.username), + ); + }, + ); + +const getUserHandler = (options?: { + fail?: boolean; + status?: number; + avatarUrl?: string; + noDisplayName?: boolean; + noUserName?: boolean; +}) => + rest.get( + `${mockBaseUrl}/rest/api/latest/users/${passportProfile.username}`, + (_req, res, ctx) => { + if (options?.fail) { + res.networkError('error'); + } + return res( + ctx.status(options?.status ?? 200), + ctx.json({ + name: options?.noUserName ? undefined : 'john.doe', + emailAddress: 'john@doe.com', + id: 123, + displayName: options?.noDisplayName ? undefined : 'John Doe', + active: true, + slug: 'john.doe', + type: 'NORMAL', + links: { + self: [ + { + href: 'https://bitbucket.org/users/john.doe', + }, + ], + }, + avatarUrl: options?.avatarUrl ?? '/user/123/avatar', + }), + ); + }, + ); describe('BitbucketServerAuthProvider', () => { - const originalFetch = global.fetch; - - beforeEach(() => { - global.fetch = jest.fn(); - }); - - afterEach(() => { - global.fetch = originalFetch; - }); - const provider = new BitbucketServerAuthProvider({ resolverContext: { signInWithCatalogUser: jest.fn(info => { @@ -113,21 +119,26 @@ describe('BitbucketServerAuthProvider', () => { }; }), } as unknown as AuthResolverContext, - signInResolver: commonByEmailResolver, + signInResolver: + bitbucketServer.resolvers.emailMatchingUserEntityProfileEmail(), authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), callbackUrl: 'mock', clientId: 'mock', clientSecret: 'mock', - host: 'bitbucket.org', + host: mockHost, authorizationUrl: 'mock', tokenUrl: 'mock', }); describe('when transforming to type OAuthResponse', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + it('should map to a valid response', async () => { - mockFetchUserRequests(); + server.use(whoAmIHandler(), getUserHandler()); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; @@ -153,8 +164,10 @@ describe('BitbucketServerAuthProvider', () => { const { response } = await provider.handler({} as any); expect(response).toEqual(expected); }); + it('should throw if whoami fails', async () => { - mockFetchUserRequests(true); + server.use(whoAmIHandler({ fail: true }), getUserHandler()); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; mockFrameHandler.mockResolvedValueOnce({ @@ -166,8 +179,10 @@ describe('BitbucketServerAuthProvider', () => { `Failed to retrieve the username of the logged in user`, ); }); + it('should throw if whoami returns an invalid response', async () => { - mockFetchUserRequests(false, ''); + server.use(whoAmIHandler({ value: '' }), getUserHandler()); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; mockFrameHandler.mockResolvedValueOnce({ @@ -179,8 +194,9 @@ describe('BitbucketServerAuthProvider', () => { `Failed to retrieve the username of the logged in user`, ); }); + it('should throw if get user fails', async () => { - mockFetchUserRequests(false, passportProfile.username, true); + server.use(whoAmIHandler(), getUserHandler({ fail: true })); const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; mockFrameHandler.mockResolvedValueOnce({ @@ -192,8 +208,9 @@ describe('BitbucketServerAuthProvider', () => { `Failed to retrieve the user '${passportProfile.username}'`, ); }); + it('should throw if get user is not ok', async () => { - mockFetchUserRequests(false, passportProfile.username, false, false); + server.use(whoAmIHandler(), getUserHandler({ status: 500 })); const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; mockFrameHandler.mockResolvedValueOnce({ @@ -205,8 +222,9 @@ describe('BitbucketServerAuthProvider', () => { `Failed to retrieve the user '${passportProfile.username}'`, ); }); + it('should not set an avatar url if not given', async () => { - mockFetchUserRequests(false, passportProfile.username, false, true, ''); + server.use(whoAmIHandler(), getUserHandler({ avatarUrl: '' })); const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; @@ -231,15 +249,10 @@ describe('BitbucketServerAuthProvider', () => { const { response } = await provider.handler({} as any); expect(response).toEqual(expected); }); + it('should fallback to the username if no displayName is given', async () => { - mockFetchUserRequests( - false, - passportProfile.username, - false, - true, - '/user/123/avatar', - false, - ); + server.use(whoAmIHandler(), getUserHandler({ noDisplayName: true })); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; @@ -265,16 +278,13 @@ describe('BitbucketServerAuthProvider', () => { const { response } = await provider.handler({} as any); expect(response).toEqual(expected); }); + it('should fallback to the user id if no name is given', async () => { - mockFetchUserRequests( - false, - passportProfile.username, - false, - true, - '/user/123/avatar', - false, - false, + server.use( + whoAmIHandler(), + getUserHandler({ noDisplayName: true, noUserName: true }), ); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; @@ -303,8 +313,12 @@ describe('BitbucketServerAuthProvider', () => { }); describe('when authenticating', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + it('should forward the refresh token', async () => { - mockFetchUserRequests(); + server.use(whoAmIHandler(), getUserHandler()); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; mockFrameHandler.mockResolvedValueOnce({ @@ -334,8 +348,10 @@ describe('BitbucketServerAuthProvider', () => { expect(response).toEqual(expected); }); + it('should forward a new refresh token on refresh', async () => { - mockFetchUserRequests(); + server.use(whoAmIHandler(), getUserHandler()); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; const params = { scope: 'REPO_READ' }; const mockRefreshToken = jest.spyOn( diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts index 305f07c9d5..cd082ee953 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts @@ -298,6 +298,7 @@ export const bitbucketServer = createAuthProviderIntegration({ /** * Looks up the user by matching their email to the entity email. */ - emailMatchingUserEntityProfileEmail: () => commonByEmailResolver, + emailMatchingUserEntityProfileEmail: + (): SignInResolver => commonByEmailResolver, }, }); diff --git a/plugins/auth-backend/src/setupTests.ts b/plugins/auth-backend/src/setupTests.ts index d3232290a7..c1d649f2ad 100644 --- a/plugins/auth-backend/src/setupTests.ts +++ b/plugins/auth-backend/src/setupTests.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export {}; +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index 1621cdc5b1..7bfbb5ea1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4537,6 +4537,7 @@ __metadata: "@backstage/types": "workspace:^" "@davidzemon/passport-okta-oauth": ^0.0.5 "@google-cloud/firestore": ^6.0.0 + "@testing-library/jest-dom": ^5.16.5 "@types/body-parser": ^1.19.0 "@types/cookie-parser": ^1.4.2 "@types/express": ^4.17.6 @@ -4553,6 +4554,7 @@ __metadata: compression: ^1.7.4 cookie-parser: ^1.4.5 cors: ^2.8.5 + cross-fetch: ^3.1.5 express: ^4.17.1 express-promise-router: ^4.1.0 express-session: ^1.17.1 @@ -13578,7 +13580,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^5.10.1, @testing-library/jest-dom@npm:^5.16.4": +"@testing-library/jest-dom@npm:^5.10.1, @testing-library/jest-dom@npm:^5.16.4, @testing-library/jest-dom@npm:^5.16.5": version: 5.16.5 resolution: "@testing-library/jest-dom@npm:5.16.5" dependencies: