feat: unit tests and log refactor

Signed-off-by: Marco Crivellaro <marco.crive@gmail.com>
This commit is contained in:
Marco Crivellaro
2022-07-13 22:11:34 +01:00
parent 30706db290
commit d10d03c941
3 changed files with 309 additions and 21 deletions
+3 -6
View File
@@ -288,13 +288,10 @@ scaffolder:
# defaultCommitMessage: 'Initial commit'
# Use to customize the audit log emitted when a template task is created
auditlog:
enabled: true
# Properties of the user entity metadata (i.e. userEntity.metadata.name)
identityMetadata:
- name
# Annotations under the user entity metadata (i.e. userEntity.metadata.annotations['name/of-annotation'])
enabled: true # emit an audit log when a new scaffolder task is created, by default it logs the name of the authenticated user (if available)
# Optional authenticated user annotations to log (i.e. userEntity.metadata.annotations['name/of-annotation'])
# identityAnnotations:
# # - name/of-annotation
# - name/of-annotation
auth:
### Add auth.keyStore.provider to more granularly control how to store JWK data when running
@@ -35,24 +35,25 @@ import {
UrlReaders,
} from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { ConfigReader } from '@backstage/config';
import ObservableImpl from 'zen-observable';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import express from 'express';
import request from 'supertest';
import ObservableImpl from 'zen-observable';
/**
* TODO: The following should import directly from the router file.
* Due to a circular dependency between this plugin and the
* plugin-scaffolder-backend-module-cookiecutter plugin, it results in an error:
* TypeError: _pluginscaffolderbackend.createTemplateAction is not a function
*/
import { createRouter, DatabaseTaskStore, TaskBroker } from '../index';
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
import {
parseEntityRef,
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import { createRouter, DatabaseTaskStore, TaskBroker } from '../index';
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
function createDatabase(): PluginDatabaseManager {
return DatabaseManager.fromConfig(
@@ -595,3 +596,295 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
});
});
});
describe('createRouter logging', () => {
let app: express.Express;
let taskBroker: TaskBroker;
const catalogClient = { getEntityByRef: jest.fn() } as unknown as CatalogApi;
const mockTemplate: TemplateEntityV1beta3 = {
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
description: 'Create a new CRA website project',
name: 'create-react-app-template',
tags: ['experimental', 'react', 'cra'],
title: 'Create React App Template',
annotations: {
'backstage.io/managed-by-location': 'url:https://dev.azure.com',
},
},
spec: {
owner: 'web@example.com',
type: 'website',
steps: [],
parameters: {
type: 'object',
required: ['required'],
properties: {
required: {
type: 'string',
description: 'Required parameter',
},
},
},
},
};
const mockUser: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: 'guest',
annotations: {
'google.com/email': 'bobby@tables.com',
'example.com/uuid': 'bc48e920-f1c9-4094-bb5c-99d120e87ab8',
},
},
spec: {
profile: {
displayName: 'Robert Tables of the North',
},
},
};
beforeEach(async () => {
const logger = getVoidLogger();
const databaseTaskStore = await DatabaseTaskStore.create({
database: await createDatabase().getClient(),
});
taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
jest.spyOn(taskBroker, 'dispatch');
jest.spyOn(taskBroker, 'get');
jest.spyOn(taskBroker, 'list');
jest.spyOn(taskBroker, 'event$');
jest
.spyOn(catalogClient, 'getEntityByRef')
.mockImplementation(async ref => {
const { kind } = parseEntityRef(ref);
if (kind === 'template') {
return mockTemplate;
}
if (kind === 'user') {
return mockUser;
}
throw new Error(`no mock found for kind: ${kind}`);
});
});
afterEach(() => {
jest.resetAllMocks();
});
describe('POST /v2/tasks', () => {
it('does not log auditlog by default', async () => {
const config = new ConfigReader({});
const logger = getVoidLogger();
const loggerSpy = jest.spyOn(logger, 'info');
const router = await createRouter({
logger: logger,
config: config,
database: createDatabase(),
catalogClient,
reader: mockUrlReader,
taskBroker,
});
app = express().use(router);
const response = await request(app)
.post('/v2/tasks')
.send({
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
required: 'required-value',
},
});
expect(loggerSpy).toHaveBeenCalledTimes(0);
expect(response.status).toEqual(201);
});
it('does not log auditlog when not enabled', async () => {
const config = new ConfigReader({
scaffolder: {
auditlog: {
enabled: false,
},
},
});
const logger = getVoidLogger();
const loggerSpy = jest.spyOn(logger, 'info');
const router = await createRouter({
logger: logger,
config: config,
database: createDatabase(),
catalogClient,
reader: mockUrlReader,
taskBroker,
});
app = express().use(router);
const response = await request(app)
.post('/v2/tasks')
.send({
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
required: 'required-value',
},
});
expect(loggerSpy).toHaveBeenCalledTimes(0);
expect(response.status).toEqual(201);
});
it('emits auditlog when enabled without identity', async () => {
const config = new ConfigReader({
scaffolder: {
auditlog: {
enabled: true,
},
},
});
const logger = getVoidLogger();
const loggerSpy = jest.spyOn(logger, 'info');
const router = await createRouter({
logger: logger,
config: config,
database: createDatabase(),
catalogClient,
reader: mockUrlReader,
taskBroker,
});
app = express().use(router);
const response = await request(app)
.post('/v2/tasks')
.send({
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
required: 'required-value',
},
});
expect(loggerSpy).toHaveBeenCalledTimes(1);
expect(loggerSpy).toHaveBeenCalledWith(
"Scaffolding task for 'template:default/create-react-app-template'",
);
expect(response.status).toEqual(201);
});
it('emits auditlog when enabled with identity', async () => {
const config = new ConfigReader({
scaffolder: {
auditlog: {
enabled: true,
},
},
});
const logger = getVoidLogger();
const loggerSpy = jest.spyOn(logger, 'info');
const router = await createRouter({
logger: logger,
config: config,
database: createDatabase(),
catalogClient,
reader: mockUrlReader,
taskBroker,
});
app = express().use(router);
const mockToken =
'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob';
const response = await request(app)
.post('/v2/tasks')
.set('Authorization', `Bearer ${mockToken}`)
.send({
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
required: 'required-value',
},
});
expect(loggerSpy).toHaveBeenCalledTimes(1);
expect(loggerSpy).toHaveBeenCalledWith(
"Scaffolding task for 'template:default/create-react-app-template' created by guest",
);
expect(response.status).toEqual(201);
});
it('emits auditlog when enabled with identity and annotations', async () => {
const config = new ConfigReader({
scaffolder: {
auditlog: {
enabled: true,
identityAnnotations: ['google.com/email', 'example.com/uuid'],
},
},
});
const logger = getVoidLogger();
const loggerSpy = jest.spyOn(logger, 'info');
const router = await createRouter({
logger: logger,
config: config,
database: createDatabase(),
catalogClient,
reader: mockUrlReader,
taskBroker,
});
app = express().use(router);
const mockToken =
'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob';
const response = await request(app)
.post('/v2/tasks')
.set('Authorization', `Bearer ${mockToken}`)
.send({
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
required: 'required-value',
},
});
expect(loggerSpy).toHaveBeenCalledTimes(1);
expect(loggerSpy).toHaveBeenCalledWith(
'Scaffolding task for \'template:default/create-react-app-template\' created by guest [{"google.com/email":"bobby@tables.com"},{"example.com/uuid":"bc48e920-f1c9-4094-bb5c-99d120e87ab8"}]',
);
expect(response.status).toEqual(201);
});
});
});
@@ -198,29 +198,27 @@ export async function createRouter(
);
if (auditlogEnabled) {
const identityMetadata = config.getOptionalStringArray(
'scaffolder.auditlog.identityMetadata',
);
const identityAnnotations = config.getOptionalStringArray(
'scaffolder.auditlog.identityAnnotations',
);
const logMe: { [key: string]: string }[] = [];
identityMetadata?.forEach((prop: string) =>
logMe.push({ [prop]: userEntity?.metadata[prop] as string }),
);
identityAnnotations?.forEach((prop: string) =>
logMe.push({
[prop]: userEntity?.metadata.annotations?.[prop] as string,
}),
);
logger.info(
`Scaffolding template '${templateRef}'. ${
logMe.length > 0 && JSON.stringify(logMe)
}`,
);
let userLog = '';
if (userEntity) {
userLog = ` created by ${userEntity.metadata.name}`;
if (logMe.length > 0) {
userLog += ` ${JSON.stringify(logMe)}`;
}
}
logger.info(`Scaffolding task for '${templateRef}'${userLog}`);
}
const values = req.body.values;