diff --git a/plugins/scaffolder-backend-module-sentry/api-report.md b/plugins/scaffolder-backend-module-sentry/api-report.md index e850c0f1c9..746cce1348 100644 --- a/plugins/scaffolder-backend-module-sentry/api-report.md +++ b/plugins/scaffolder-backend-module-sentry/api-report.md @@ -4,17 +4,13 @@ ```ts import { Config } from '@backstage/config'; -import { FetchApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; // @public -export function createSentryCreateProjectAction( - options: { - config: Config; - }, - fetchApi?: FetchApi, -): TemplateAction< +export function createSentryCreateProjectAction(options: { + config: Config; +}): TemplateAction< { organizationSlug: string; teamSlug: string; diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index eccae89620..999cc7114c 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -29,14 +29,15 @@ }, "dependencies": { "@backstage/config": "workspace:^", - "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "yaml": "^2.3.3" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/types": "workspace:^" + "@backstage/types": "workspace:^", + "msw": "^1.0.0" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts index b66f842688..0486012760 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts @@ -13,14 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; -import { createSentryCreateProjectAction } from './createProject'; -import { ActionContext } from '@backstage/plugin-scaffolder-node'; import { InputError } from '@backstage/errors'; +import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { JsonObject } from '@backstage/types'; import { randomBytes } from 'crypto'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { createSentryCreateProjectAction } from './createProject'; describe('sentry:project:create action', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + const createScaffolderConfig = (configData: JsonObject = {}) => ({ config: new ConfigReader({ scaffolder: { @@ -29,26 +35,6 @@ describe('sentry:project:create action', () => { }), }); - let fetch: jest.Func; - - const mockFetch = (response = {}) => { - const mockedResponse = { - status: 201, - headers: { - get: () => 'application/json', - }, - json: async () => - Promise.resolve({ - detail: 'project creation mocked result', - }), - text: async () => Promise.resolve('Unexpected error.'), - ...response, - }; - - fetch = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse)); - return mockedResponse; - }; - const getActionContext = (): ActionContext<{ organizationSlug: string; teamSlug: string; @@ -69,64 +55,71 @@ describe('sentry:project:create action', () => { output: jest.fn(), }); - beforeEach(() => { - mockFetch(); - }); - - test('should request sentry project create with specified parameters.', async () => { - const action = createSentryCreateProjectAction(createScaffolderConfig(), { - fetch, - }); + it('should request sentry project create with specified parameters.', async () => { + expect.assertions(3); + const action = createSentryCreateProjectAction(createScaffolderConfig()); const actionContext = getActionContext(); + worker.use( + rest.post( + `https://sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`, + async (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Bearer ${actionContext.input.authToken}`, + ); + expect(req.headers.get('Content-Type')).toBe(`application/json`); + await expect(req.json()).resolves.toEqual({ + name: actionContext.input.name, + }); + return res( + ctx.status(201), + ctx.json({ + detail: 'project creation mocked result', + }), + ); + }, + ), + ); + await action.handler(actionContext); - - expect(fetch).toHaveBeenNthCalledWith( - 1, - `https://sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`, - { - method: 'POST', - headers: { - Authorization: `Bearer ${actionContext.input.authToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: actionContext.input.name, - }), - }, - ); }); - test('should request sentry project create with added optional specified project slug', async () => { - const action = createSentryCreateProjectAction(createScaffolderConfig(), { - fetch, - }); + it('should request sentry project create with added optional specified project slug', async () => { + expect.assertions(3); + const action = createSentryCreateProjectAction(createScaffolderConfig()); const actionContext = getActionContext(); - actionContext.input = { ...actionContext.input, slug: 'project-slug' }; - await action.handler(actionContext); - - expect(fetch).toHaveBeenNthCalledWith( - 1, - `https://sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`, - { - method: 'POST', - headers: { - Authorization: `Bearer ${actionContext.input.authToken}`, - 'Content-Type': 'application/json', + worker.use( + rest.post( + `https://sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`, + async (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Bearer ${actionContext.input.authToken}`, + ); + expect(req.headers.get('Content-Type')).toBe(`application/json`); + await expect(req.json()).resolves.toEqual({ + name: actionContext.input.name, + slug: actionContext.input.slug, + }); + return res( + ctx.status(201), + ctx.json({ + detail: 'project creation mocked result', + }), + ); }, - body: JSON.stringify({ - name: actionContext.input.name, - slug: actionContext.input.slug, - }), - }, + ), ); + + await action.handler(actionContext); }); - test('should take Sentry auth token from scaffolder config when input authToken is missing.', async () => { + it('should take Sentry auth token from scaffolder config when input authToken is missing.', async () => { + expect.assertions(3); + const sentryScaffolderConfigToken = randomBytes(5).toString('hex'); const action = createSentryCreateProjectAction( createScaffolderConfig({ @@ -134,89 +127,98 @@ describe('sentry:project:create action', () => { token: sentryScaffolderConfigToken, }, }), - { fetch }, ); - const actionContext = getActionContext(); - actionContext.input.authToken = undefined; + worker.use( + rest.post( + `https://sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`, + async (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Bearer ${sentryScaffolderConfigToken}`, + ); + expect(req.headers.get('Content-Type')).toBe(`application/json`); + await expect(req.json()).resolves.toEqual({ + name: actionContext.input.name, + }); + return res( + ctx.status(201), + ctx.json({ + detail: 'project creation mocked result', + }), + ); + }, + ), + ); + await action.handler(actionContext); - - expect(fetch).toHaveBeenNthCalledWith( - 1, - `https://sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`, - { - method: 'POST', - headers: { - Authorization: `Bearer ${sentryScaffolderConfigToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: actionContext.input.name, - }), - }, - ); }); - test('should throw InputError when auth token is missing from input parameters and scaffolder config.', async () => { - const action = createSentryCreateProjectAction(createScaffolderConfig(), { - fetch, - }); + it('should throw InputError when auth token is missing from input parameters and scaffolder config.', async () => { + const action = createSentryCreateProjectAction(createScaffolderConfig()); const actionContext = getActionContext(); - actionContext.input.authToken = undefined; - expect.assertions(1); - - await expect(async () => { - await action.handler(actionContext); - }).rejects.toThrow(new InputError('No valid sentry token given')); - }); - - test('should throw InputError when sentry API returns unexpected content-type.', async () => { - const actionContext = getActionContext(); - - const mockedFetchResponse = mockFetch({ - headers: { - get: () => 'text/html', - }, - }); - - const action = createSentryCreateProjectAction(createScaffolderConfig(), { - fetch, - }); - - expect.assertions(1); - - await expect(async () => { - await action.handler(actionContext); - }).rejects.toThrow( - new InputError( - `Unexpected Sentry Response Type: ${await mockedFetchResponse.text()}`, + worker.use( + rest.post( + `https://sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`, + async (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + `Bearer ${actionContext.input.authToken}`, + ); + expect(req.headers.get('Content-Type')).toBe(`application/json`); + await expect(req.json()).resolves.toEqual({ + name: actionContext.input.name, + }); + return res( + ctx.status(201), + ctx.json({ + detail: 'project creation mocked result', + }), + ); + }, ), ); + + await expect(() => action.handler(actionContext)).rejects.toThrow( + new InputError('No valid sentry token given'), + ); }); - test('should throw InputError when sentry API returns unexpected status code.', async () => { + it('should throw InputError when sentry API returns unexpected content-type.', async () => { + const action = createSentryCreateProjectAction(createScaffolderConfig()); const actionContext = getActionContext(); - const mockedFetchResponse = mockFetch({ - status: 400, - }); - - const action = createSentryCreateProjectAction(createScaffolderConfig(), { - fetch, - }); - - expect.assertions(1); - - await expect(async () => { - await action.handler(actionContext); - }).rejects.toThrow( - new InputError( - `Sentry Response was: ${(await mockedFetchResponse.json()).detail}`, + worker.use( + rest.post( + `https://sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`, + async (_, res, ctx) => { + return res(ctx.status(201), ctx.text('Bad response')); + }, ), ); + + await expect(() => action.handler(actionContext)).rejects.toThrow( + new InputError(`Unexpected Sentry Response Type: Bad response`), + ); + }); + + it('should throw InputError when sentry API returns unexpected status code.', async () => { + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + + worker.use( + rest.post( + `https://sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`, + async (_, res, ctx) => { + return res(ctx.status(400), ctx.json({ detail: 'OUCH' })); + }, + ), + ); + + await expect(() => action.handler(actionContext)).rejects.toThrow( + new InputError(`Sentry Response was: OUCH`), + ); }); }); diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts index bb2ee6039e..259b670f9e 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.ts @@ -17,24 +17,18 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; -import { examples } from './createProject.examples'; -import { FetchApi } from '@backstage/core-plugin-api'; /** - * Creates the `sentry:create-project` Scaffolder action. + * Creates the `sentry:project:create` Scaffolder action. * * @remarks * * See {@link https://backstage.io/docs/features/software-templates/writing-custom-actions}. * * @param options - Configuration of the Sentry API. - * @param fetchApi - fetch implementation to use for calling Sentry service. * @public */ -export function createSentryCreateProjectAction( - options: { config: Config }, - fetchApi?: FetchApi, -) { +export function createSentryCreateProjectAction(options: { config: Config }) { const { config } = options; return createTemplateAction<{ @@ -45,7 +39,6 @@ export function createSentryCreateProjectAction( authToken?: string; }>({ id: 'sentry:project:create', - examples, schema: { input: { required: ['organizationSlug', 'teamSlug', 'name'], @@ -95,7 +88,7 @@ export function createSentryCreateProjectAction( throw new InputError(`No valid sentry token given`); } - const response = await (fetchApi?.fetch ?? fetch)( + const response = await fetch( `https://sentry.io/api/0/teams/${organizationSlug}/${teamSlug}/projects/`, { method: 'POST', @@ -119,7 +112,7 @@ export function createSentryCreateProjectAction( const result = await response.json(); if (code !== 201) { - throw new InputError(`Sentry Response was: ${result.detail}`); + throw new InputError(`Sentry Response was: ${await result.detail}`); } ctx.output('id', result.id); diff --git a/yarn.lock b/yarn.lock index 7a0ca3c78a..3000d86b7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8658,12 +8658,13 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-sentry@workspace:plugins/scaffolder-backend-module-sentry" dependencies: + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" - "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" + msw: ^1.0.0 yaml: ^2.3.3 languageName: unknown linkType: soft