Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2023-11-16 13:54:44 +01:00
parent 9b90f94c90
commit 2c776eb024
5 changed files with 147 additions and 154 deletions
@@ -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;
@@ -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"
@@ -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`),
);
});
});
@@ -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);
+2 -1
View File
@@ -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