feat(badges): Remove hash implementation and replace it with uuid one.

Signed-off-by: Rbillon59 <r.billon@celonis.com>
This commit is contained in:
Rbillon59
2023-04-21 08:53:28 +02:00
parent 1aabf9e03f
commit 6263ef7ce9
13 changed files with 215 additions and 673 deletions
@@ -19,39 +19,28 @@ import {
resolvePackagePath,
} from '@backstage/backend-common';
import { Knex } from 'knex';
import crypto from 'crypto';
import { GetEntitiesResponse } from '@backstage/catalog-client';
import { v4 as uuidv4 } from 'uuid';
/**
* internal
* @public
*/
export interface BadgesStore {
createAllBadges(
entities: GetEntitiesResponse,
salt: string | undefined,
): Promise<void>;
getBadgeFromHash(
hash: string,
): Promise<{ name: string; namespace: string; kind: string } | undefined>;
getHashFromEntityMetadata(
addBadge(
name: string,
namespace: string,
kind: string,
): Promise<{ hash: string } | undefined>;
): Promise<{ uuid: string }>;
deleteObsoleteHashes(
entities: GetEntitiesResponse,
salt: string | undefined,
): Promise<void>;
getBadgeFromUuid(
uuid: string,
): Promise<{ name: string; namespace: string; kind: string } | undefined>;
countAllBadges(): Promise<number>;
getAllBadges(): Promise<
{ name: string; namespace: string; kind: string; hash: string }[]
>;
getUuidFromEntityMetadata(
name: string,
namespace: string,
kind: string,
): Promise<{ uuid: string } | undefined>;
}
const migrationsDir = resolvePackagePath(
@@ -84,99 +73,47 @@ export class DatabaseBadgesStore implements BadgesStore {
return new DatabaseBadgesStore(client);
}
async createAllBadges(
entities: GetEntitiesResponse,
salt: string,
): Promise<void> {
for (const entity of entities.items) {
const name = entity.metadata.name.toLocaleLowerCase();
const namespace =
entity.metadata.namespace?.toLocaleLowerCase() ?? 'default';
const kind = entity.kind.toLocaleLowerCase();
const entityHash = crypto
.createHash('sha256')
.update(`${kind}:${namespace}:${name}:${salt}`)
.digest('hex');
await this.db('badges')
.insert({
hash: entityHash,
namespace: namespace,
name: name,
kind: kind,
})
.onConflict()
.ignore();
}
}
async getAllBadges(): Promise<
{ name: string; namespace: string; kind: string; hash: string }[]
> {
const result = await this.db('badges').select('*').orderBy('name', 'asc');
return result;
}
async getBadgeFromHash(
hash: string,
async getBadgeFromUuid(
uuid: string,
): Promise<{ name: string; namespace: string; kind: string } | undefined> {
const result = await this.db('badges')
.select('namespace', 'name', 'kind')
.where({ hash: hash })
.where({ uuid: uuid })
.first();
return result;
}
async getHashFromEntityMetadata(
async getUuidFromEntityMetadata(
name: string,
namespace: string,
kind: string,
): Promise<{ hash: string } | undefined> {
): Promise<{ uuid: string } | undefined> {
const result = await this.db('badges')
.select('hash')
.select('uuid')
.where({ name: name, namespace: namespace, kind: kind })
.first();
return result;
}
async deleteObsoleteHashes(
entities: GetEntitiesResponse,
salt: string,
): Promise<void> {
const entityInCatalog: string[] = [];
const entityInBadgeDatabase: string[] = [];
let entityToDelete: string[] = [];
async addBadge(
name: string,
namespace: string,
kind: string,
): Promise<{ uuid: string }> {
const uuid = uuidv4();
for (const entity of entities.items) {
const name = entity.metadata.name.toLowerCase();
const namespace = entity.metadata.namespace?.toLowerCase() ?? 'default';
const kind = entity.kind.toLowerCase();
const entityHash = crypto
.createHash('sha256')
.update(`${kind}:${namespace}:${name}:${salt}`)
.digest('hex');
await this.db('badges')
.insert({
uuid: uuid,
name: name,
namespace: namespace,
kind: kind,
})
.onConflict(['name', 'namespace', 'kind'])
.ignore();
entityInCatalog.push(entityHash);
}
for (const entity of await this.getAllBadges()) {
entityInBadgeDatabase.push(entity.hash);
}
entityToDelete = entityInBadgeDatabase.filter(
entity => !entityInCatalog.includes(entity),
);
for (const entity of entityToDelete) {
await this.db('badges').where({ hash: entity }).del();
}
}
async countAllBadges(): Promise<number> {
const result = await this.db('badges').countDistinct('hash as count');
const count: number = +result[0].count;
return count;
return { uuid };
}
}
+34 -104
View File
@@ -17,6 +17,7 @@
import express from 'express';
import Router from 'express-promise-router';
import {
DatabaseManager,
errorHandler,
PluginEndpointDiscovery,
TokenManager,
@@ -27,11 +28,10 @@ import { NotFoundError } from '@backstage/errors';
import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder';
import { BadgeContext, BadgeFactories } from '../types';
import { isNil } from 'lodash';
import crypto from 'crypto';
import { Logger } from 'winston';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
import type { BadgesStore } from '../database/badgesStore';
import { BadgesStore, DatabaseBadgesStore } from '../database/badgesStore';
/** @public */
export interface RouterOptions {
@@ -43,7 +43,7 @@ export interface RouterOptions {
tokenManager: TokenManager;
logger: Logger;
identity: IdentityApi;
db: BadgesStore;
badgeStore?: BadgesStore;
}
/** @public */
@@ -57,39 +57,29 @@ export async function createRouter(
new DefaultBadgeBuilder(options.badgeFactories || {});
const router = Router();
const { db, config, logger, tokenManager, discovery, identity } = options;
const { config, logger, tokenManager, discovery, identity } = options;
const baseUrl = await discovery.getExternalBaseUrl('badges');
const salt = config.getOptionalString('custom.badges-backend.salt');
const cacheTimeToLive =
config.getOptionalNumber('badgeDatabaseRefreshCacheTimeToLive') ?? 3600;
let lastDatabaseRefresh = 0;
// Check if the users have enabled the obfuscation of the entity name
if (config.getOptionalBoolean('app.badges.obfuscate')) {
logger.info('Badges obfuscation is enabled');
if (isNil(salt)) {
throw new Error(
'Badges obfuscation is enabled but no salt has been provided',
);
}
const store = options.badgeStore
? options.badgeStore
: await DatabaseBadgesStore.create({
database: await DatabaseManager.fromConfig(config).forPlugin(
'badges',
),
});
// Use the generated hash instead of the triplet namespace/kind/name
router.get('/entity/:entityHash/badge-specs', async (req, res) => {
const { entityHash } = req.params;
// Chech if the database needs to be refreshed
if (await isBadgeDatabaseRefreshNeeded(lastDatabaseRefresh)) {
lastDatabaseRefresh = await refreshBadgeDatabase();
}
router.get('/entity/:entityUuid/badge-specs', async (req, res) => {
const { entityUuid } = req.params;
// Retrieve the badge info from the database
const badgeInfos = await db.getBadgeFromHash(entityHash);
const badgeInfos = await store.getBadgeFromUuid(entityUuid);
if (isNil(badgeInfos)) {
throw new NotFoundError(
`No badge found for entity hash "${entityHash}"`,
`No badge found for entity uuid "${entityUuid}"`,
);
}
@@ -118,7 +108,7 @@ export async function createRouter(
const specs = [];
for (const badgeInfo of await badgeBuilder.getBadges()) {
const context: BadgeContext = {
badgeUrl: await getBadgeObfuscatedUrl(entityHash, badgeInfo.id),
badgeUrl: await getBadgeObfuscatedUrl(entityUuid, badgeInfo.id),
config: config,
entity,
};
@@ -133,20 +123,15 @@ export async function createRouter(
res.status(200).json(specs);
});
// Use the generated hash instead of the triplet namespace/kind/name
router.get('/entity/:entityHash/:badgeId', async (req, res) => {
if (await isBadgeDatabaseRefreshNeeded(lastDatabaseRefresh)) {
lastDatabaseRefresh = await refreshBadgeDatabase();
}
const { entityHash, badgeId } = req.params;
router.get('/entity/:entityUuid/:badgeId', async (req, res) => {
const { entityUuid, badgeId } = req.params;
// Retrieve the badge info from the database
const badgeInfo = await db.getBadgeFromHash(entityHash);
const badgeInfo = await store.getBadgeFromUuid(entityUuid);
if (isNil(badgeInfo)) {
throw new NotFoundError(
`No badge found for entity hash "${entityHash}"`,
`No badge found for entity uuid "${entityUuid}"`,
);
}
@@ -180,7 +165,7 @@ export async function createRouter(
const badgeOptions = {
badgeInfo: { id: badgeId },
context: {
badgeUrl: await getBadgeObfuscatedUrl(entityHash, badgeId),
badgeUrl: await getBadgeObfuscatedUrl(entityUuid, badgeId),
config: config,
entity,
},
@@ -201,7 +186,6 @@ export async function createRouter(
res.status(200).send(data);
});
// Generate the hash for queried the namespace/kind/name triplet
router.get(
'/entity/:namespace/:kind/:name/obfuscated',
function authenticate(req, res, next) {
@@ -223,34 +207,21 @@ export async function createRouter(
}
},
async (req, res) => {
if (await isBadgeDatabaseRefreshNeeded(lastDatabaseRefresh)) {
lastDatabaseRefresh = await refreshBadgeDatabase();
}
const { namespace, kind, name } = req.params;
const storedEntityHash: { hash: string } | undefined =
await db.getHashFromEntityMetadata(name, namespace, kind);
let storedEntityUuid: { uuid: string } | undefined =
await store.getUuidFromEntityMetadata(name, namespace, kind);
if (isNil(storedEntityHash)) {
throw new NotFoundError(
`No hash found for entity "${namespace}/${kind}/${name}"`,
);
if (isNil(storedEntityUuid)) {
storedEntityUuid = await store.addBadge(name, namespace, kind);
if (isNil(storedEntityUuid)) {
throw new NotFoundError(
`No uuid found for entity "${namespace}/${kind}/${name}"`,
);
}
}
// Compare a live calculated hash with the stored one to avoid returning an obsolete entityHash (in case of a salt change)
const liveHash = crypto
.createHash('sha256')
.update(`${kind}:${namespace}:${name}:${salt}`)
.digest('hex');
if (storedEntityHash.hash !== liveHash) {
throw new NotFoundError(
"Stored Hash and live Hash don't match, did you change the salt? Try to refresh the badge database table and try again",
);
}
return res.status(200).json(storedEntityHash);
return res.status(200).json(storedEntityUuid);
},
);
@@ -348,10 +319,10 @@ export async function createRouter(
// This function return the obfuscated badge url based on the namespace/kind/name triplet
async function getBadgeObfuscatedUrl(
hash: string,
uuid: string,
badgeId: string,
): Promise<string> {
return `${baseUrl}/entity/${hash}/${badgeId}`;
return `${baseUrl}/entity/${uuid}/${badgeId}`;
}
// This function return the badge url based on the namespace/kind/name triplet
@@ -363,45 +334,4 @@ export async function createRouter(
): Promise<string> {
return `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`;
}
async function isBadgeDatabaseRefreshNeeded(
lastDatabaseRefreshTimestamp: number,
): Promise<boolean> {
if ((await db.countAllBadges()) === 0) {
logger.info('Badge database is empty, refreshing it');
return true;
}
if (
lastDatabaseRefreshTimestamp === 0 ||
Date.now() - lastDatabaseRefreshTimestamp > cacheTimeToLive * 1000
) {
logger.info(
'Badge database refresh cache to live exceeded, refreshing it',
);
return true;
}
return false;
}
async function refreshBadgeDatabase(): Promise<number> {
const token = await tokenManager.getToken();
const entities = await catalog.getEntities(
{
filter: {
kind: ['Component'],
},
},
token,
);
logger.info(
`Refreshing badge database with ${entities.items.length} entities`,
);
await db.createAllBadges(entities, salt);
logger.info('Badge database refreshed, deleting obsolete hashes');
await db.deleteObsoleteHashes(entities, salt);
return Date.now();
}
}
@@ -70,7 +70,7 @@ export async function startStandaloneServer(
tokenManager,
logger,
identity,
db: await DatabaseBadgesStore.create({
badgeStore: await DatabaseBadgesStore.create({
database: { getClient: async () => database },
}),
});
@@ -18,31 +18,8 @@ import { DatabaseBadgesStore } from '../database/badgesStore';
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
import { Knex } from 'knex';
import { Entity } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import { GetEntitiesResponse } from '@backstage/catalog-client';
import crypto from 'crypto';
describe('DatabaseBadgesStore', () => {
const config = new ConfigReader({
backend: {
baseUrl: 'http://127.0.0.1',
listen: {
port: 7007,
},
},
app: {
badges: {
obfuscate: true,
},
},
custom: {
'badges-backend': {
salt: 'random-string',
cacheTimeToLive: '60',
},
},
});
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
@@ -51,28 +28,6 @@ describe('DatabaseBadgesStore', () => {
},
};
const entities: Entity[] = [
entity,
{
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'test-2',
},
},
{
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'test-3',
},
},
];
const defaultEntityListResponse: GetEntitiesResponse = {
items: entities,
};
const databases = TestDatabases.create();
async function createDatabaseBadgesStore(databaseId: TestDatabaseId) {
@@ -93,53 +48,33 @@ describe('DatabaseBadgesStore', () => {
({ knex, badgeStore } = await createDatabaseBadgesStore(databaseId));
});
it('createAllBadges', async () => {
await badgeStore.createAllBadges(
defaultEntityListResponse,
config.getString('custom.badges-backend.salt'),
it('createABadge', async () => {
const uuid = await badgeStore.addBadge(
entity.metadata.name,
entity.metadata.namespace || 'default',
entity.kind,
);
const storedBadges = await badgeStore.getAllBadges();
const testedEntities: {
name: string;
namespace: string;
kind: string;
hash: string;
}[] = [];
entities.forEach(entityInLoop => {
const kind = entityInLoop.kind.toLowerCase();
const namespace = entityInLoop.metadata.namespace || 'default';
const name = entityInLoop.metadata.name.toLocaleLowerCase();
const salt = config.getString('custom.badges-backend.salt');
const entityHash = crypto
.createHash('sha256')
.update(`${kind}:${namespace}:${name}:${salt}`)
.digest('hex');
testedEntities.push({
hash: entityHash,
name: name,
namespace: namespace,
kind: kind,
});
expect(storedBadges).toEqual(expect.arrayContaining(testedEntities));
expect(storedBadges.length).toEqual(3);
});
const storedBadge = await badgeStore.getBadgeFromUuid(uuid.uuid);
expect(storedBadge?.kind).toEqual(entity.kind);
expect(storedBadge?.name).toEqual(entity.metadata.name);
expect(storedBadge?.namespace).toEqual(
entity.metadata.namespace || 'default',
);
});
it('getBadgeFromHash', async () => {
it('getBadgeFromUuid', async () => {
await knex('badges').truncate();
await knex('badges').insert([
{
hash: 'hash1',
uuid: 'uuid1',
name: 'test',
namespace: 'default',
kind: 'component',
},
]);
const storedEntity = await badgeStore.getBadgeFromHash('hash1');
const storedEntity = await badgeStore.getBadgeFromUuid('uuid1');
expect(storedEntity).toEqual({
name: 'test',
@@ -148,147 +83,26 @@ describe('DatabaseBadgesStore', () => {
});
});
it('getHashFromEntityMetadata', async () => {
it('getUuidFromEntityMetadata', async () => {
await knex('badges').truncate();
await knex('badges').insert([
{
hash: 'hash1',
uuid: 'uuid1',
name: 'test',
namespace: 'default',
kind: 'component',
},
]);
const storedHash = await badgeStore.getHashFromEntityMetadata(
const storedUuid = await badgeStore.getUuidFromEntityMetadata(
'test',
'default',
'component',
);
expect(storedHash).toEqual({
hash: 'hash1',
expect(storedUuid).toEqual({
uuid: 'uuid1',
});
});
it('deleteObsoleteHashes', async () => {
const entityHash = crypto
.createHash('sha256')
.update(
`component:default:test:${config.getString(
'custom.badges-backend.salt',
)}`,
)
.digest('hex');
await knex('badges').insert([
{
hash: 'obsoleteHash',
name: 'obsoleteEntity',
namespace: 'default',
kind: 'component',
},
{
hash: entityHash,
name: 'test',
namespace: 'default',
kind: 'component',
},
]);
await badgeStore.deleteObsoleteHashes(
defaultEntityListResponse,
config.getString('custom.badges-backend.salt'),
);
const storedBadges = await badgeStore.getAllBadges();
expect(storedBadges).toEqual(
expect.not.arrayContaining([
{
hash: 'obsoleteHash',
name: 'obsoleteEntity',
namespace: 'default',
kind: 'component',
},
]),
);
expect(storedBadges.length).toEqual(1);
expect(storedBadges).toEqual(
expect.arrayContaining([
{
hash: entityHash,
name: 'test',
namespace: 'default',
kind: 'component',
},
]),
);
});
it('countAllBadges', async () => {
await knex('badges').truncate();
await knex('badges').insert([
{
hash: 'hash1',
name: 'test',
namespace: 'default',
kind: 'component',
},
{
hash: 'hash2',
name: 'test',
namespace: 'default',
kind: 'component',
},
{
hash: 'hash3',
name: 'test',
namespace: 'default',
kind: 'component',
},
{
hash: 'hash4',
name: 'test',
namespace: 'default',
kind: 'component',
},
]);
const storedBadgesCount = await badgeStore.countAllBadges();
expect(storedBadgesCount).toEqual(4);
});
it('getAllBadges', async () => {
await knex('badges').truncate();
await knex('badges').insert([
{
hash: 'hash1',
name: 'test',
namespace: 'default',
kind: 'component',
},
{
hash: 'hash2',
name: 'test',
namespace: 'default',
kind: 'component',
},
{
hash: 'hash3',
name: 'test',
namespace: 'default',
kind: 'component',
},
{
hash: 'hash4',
name: 'test',
namespace: 'default',
kind: 'component',
},
]);
const storedBadgesCount = await badgeStore.countAllBadges();
expect(storedBadgesCount).toEqual(4);
});
});
});
@@ -32,7 +32,6 @@ import {
IdentityApiGetIdentityRequest,
} from '@backstage/plugin-auth-node';
import { BadgesStore } from '../database/badgesStore';
import crypto from 'crypto';
describe('createRouter', () => {
let app: express.Express;
@@ -80,18 +79,16 @@ describe('createRouter', () => {
listen: {
port: 7007,
},
database: {
client: 'better-sqlite3',
connection: ':memory:',
},
},
app: {
badges: {
obfuscate: true,
},
},
custom: {
'badges-backend': {
salt: 'random-string',
cacheTimeToLive: '60',
},
},
});
let discovery: PluginEndpointDiscovery;
@@ -131,40 +128,18 @@ describe('createRouter', () => {
kind: 'component',
};
const badgeEntities = [
{
hash: 'hash1',
name: 'test',
namespace: 'default',
kind: 'component',
},
{
hash: 'hash2',
name: 'test2',
namespace: 'default',
kind: 'component',
},
];
const badgeStore: jest.Mocked<BadgesStore> = {
createAllBadges: jest.fn(),
getBadgeFromHash: jest.fn().mockImplementation(async () => {
addBadge: jest.fn().mockImplementation(async () => {
return { uuid: 'uuid1' };
}),
getBadgeFromUuid: jest.fn().mockImplementation(async () => {
return badgeEntity;
}),
getHashFromEntityMetadata: jest
getUuidFromEntityMetadata: jest
.fn()
.mockImplementation(async () => 'niceHash'),
deleteObsoleteHashes: jest.fn(),
countAllBadges: jest.fn().mockImplementation(async () => 4),
getAllBadges: jest.fn().mockImplementation(async () => badgeEntities),
.mockImplementation(async () => 'uuid1'),
};
const salt = config.getString('custom.badges-backend.salt');
const entityHash = crypto
.createHash('sha256')
.update(`component:default:test:${salt}`)
.digest('hex');
beforeAll(async () => {
discovery = SingleHostDiscovery.fromConfig(config);
const tokenManager = ServerTokenManager.noop();
@@ -176,7 +151,6 @@ describe('createRouter', () => {
tokenManager,
logger: getVoidLogger(),
identity: { getIdentity },
db: badgeStore,
});
app = express().use(router);
});
@@ -185,7 +159,7 @@ describe('createRouter', () => {
jest.clearAllMocks();
});
it('works', async () => {
it('works with provided badgeStore', async () => {
const tokenManager = ServerTokenManager.noop();
const router = await createRouter({
badgeBuilder,
@@ -195,7 +169,7 @@ describe('createRouter', () => {
tokenManager,
logger: getVoidLogger(),
identity: { getIdentity },
db: badgeStore,
badgeStore: badgeStore,
});
expect(router).toBeDefined();
});
@@ -240,113 +214,9 @@ describe('createRouter', () => {
});
});
describe('GET /entity/:entityHash/badge-specs', () => {
it('returns all badge specs for entity', async () => {
catalog.getEntities.mockResolvedValueOnce({ items: entities });
catalog.getEntityByRef.mockResolvedValueOnce(entity);
badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
const response = await request(app).get(`/entity/hash1/badge-specs`);
expect(response.status).toEqual(200);
expect(response.body).toEqual([badge]);
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
expect(catalog.getEntityByRef).toHaveBeenCalledWith(
{
namespace: 'default',
kind: 'component',
name: 'test',
},
{ token: '' },
);
expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1);
expect(badgeBuilder.createBadgeJson).toHaveBeenCalledTimes(1);
expect(badgeBuilder.createBadgeJson).toHaveBeenCalledWith({
badgeInfo: { id: badge.id },
context: {
badgeUrl: expect.stringMatching(
/http:\/\/127.0.0.1\/api\/badges\/entity\/hash1\/test-badge/,
),
config,
entity,
},
});
});
});
describe('GET /entity/:entityHash/test-badge', () => {
it('returns badge for entity', async () => {
catalog.getEntityByRef.mockResolvedValueOnce(entity);
catalog.getEntities.mockResolvedValueOnce({ items: entities });
const image = '<svg>...</svg>';
badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
const response = await request(app).get(
'/entity/3a5f91c1e66519be5394c37a8ba69c3087b7c322c600e7497dc9d517353e5bed/test-badge',
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(Buffer.from(image));
expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
expect(catalog.getEntityByRef).toHaveBeenCalledWith(
{
namespace: 'default',
kind: 'component',
name: 'test',
},
{ token: '' },
);
expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(0);
expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledTimes(1);
expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledWith({
badgeInfo: { id: badge.id },
context: {
badgeUrl: expect.stringMatching(
/http:\/\/127.0.0.1\/api\/badges\/entity\/3a5f91c1e66519be5394c37a8ba69c3087b7c322c600e7497dc9d517353e5bed\/test-badge/,
),
config,
entity,
},
});
});
it('returns badge spec for entity', async () => {
catalog.getEntityByRef.mockResolvedValueOnce(entity);
catalog.getEntities.mockResolvedValueOnce({ items: entities });
badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
const url =
'/entity/3a5f91c1e66519be5394c37a8ba69c3087b7c322c600e7497dc9d517353e5bed/test-badge?format=json';
const response = await request(app).get(url);
expect(response.status).toEqual(200);
expect(response.body).toEqual(badge);
});
});
describe('GET /entity/:namespace/:kind/:name/obfuscated', () => {
catalog.getEntityByRef.mockResolvedValueOnce(entity);
catalog.getEntities.mockResolvedValueOnce({ items: entities });
badgeStore.getHashFromEntityMetadata.mockResolvedValue({
hash: entityHash,
});
it('returns obfuscated entity', async () => {
const obfuscatedEntity = await request(app)
.get('/entity/default/component/test/obfuscated')
.set('Authorization', 'Bearer fakeToken');
expect(obfuscatedEntity.status).toEqual(200);
// echo -n "component:default:test:random-string" | openssl dgst -sha256
expect(obfuscatedEntity.body).toEqual({
hash: entityHash,
});
});
it('returns obfuscated 401 if no auth', async () => {
const obfuscatedEntity = await request(app).get(
@@ -354,39 +224,82 @@ describe('createRouter', () => {
);
expect(obfuscatedEntity.status).toEqual(401);
});
});
describe('Errors', () => {
it('returns 404 for unknown entity hash', async () => {
badgeStore.getBadgeFromHash.mockResolvedValue(undefined);
it('returns obfuscated entity and badges', async () => {
const obfuscatedEntity = await request(app)
.get('/entity/default/component/test/obfuscated')
.set('Authorization', 'Bearer fakeToken');
expect(obfuscatedEntity.status).toEqual(200);
expect(obfuscatedEntity.body.uuid).toMatch(
new RegExp(
'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
),
);
const uuid = obfuscatedEntity.body.uuid;
const url = `/entity/${uuid}/test-badge?format=json`;
let response = await request(app).get(url);
expect(response.status).toEqual(200);
expect(response.body).toEqual(badge);
catalog.getEntityByRef.mockResolvedValueOnce(entity);
catalog.getEntities.mockResolvedValueOnce({ items: entities });
const image = '<svg>...</svg>';
badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
response = await request(app).get(`/entity/${uuid}/test-badge`);
expect(response.status).toEqual(200);
expect(response.body).toEqual(Buffer.from(image));
catalog.getEntities.mockResolvedValueOnce({ items: entities });
catalog.getEntityByRef.mockResolvedValueOnce(entity);
badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
async function testUrl(url: string) {
const response = await request(app).get(url);
expect(response.status).toEqual(404);
expect(response.body).toEqual({
error: {
message: expect.any(String),
name: 'NotFoundError',
},
request: {
method: 'GET',
url,
},
response: {
statusCode: 404,
},
});
}
await testUrl(
'/entity/3a5f91c1e66519be5394c37a8ba69cfsf3087b7c322c600e7497dc9d517353e5bed/badge-specs',
);
await testUrl(
'/entity/3a5f91c1e66519be5394c37a8ba69c3087b7csfsf322c600e7497dc9d517353e5bed/test-badge',
);
response = await request(app).get(`/entity/${uuid}/badge-specs`);
expect(response.status).toEqual(200);
expect(response.body).toEqual([badge]);
expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1);
expect(badgeBuilder.createBadgeJson).toHaveBeenCalledTimes(2);
});
describe('Errors', () => {
it('returns 404 for unknown entity uuid', async () => {
badgeStore.getBadgeFromUuid.mockResolvedValue(undefined);
catalog.getEntityByRef.mockResolvedValueOnce(entity);
catalog.getEntities.mockResolvedValueOnce({ items: entities });
badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
async function testUrl(url: string) {
const response = await request(app).get(url);
expect(response.status).toEqual(404);
expect(response.body).toEqual({
error: {
message: expect.any(String),
name: 'NotFoundError',
},
request: {
method: 'GET',
url,
},
response: {
statusCode: 404,
},
});
}
await testUrl(
'/entity/3a5f91c1e66519be5394c37a8ba69cfsf3087b7c322c600e7497dc9d517353e5bed/badge-specs',
);
await testUrl(
'/entity/3a5f91c1e66519be5394c37a8ba69c3087b7csfsf322c600e7497dc9d517353e5bed/test-badge',
);
});
});
});
});
+10 -10
View File
@@ -75,12 +75,9 @@ describe('createRouter', () => {
};
const badgeStore: jest.Mocked<BadgesStore> = {
createAllBadges: jest.fn(),
getBadgeFromHash: jest.fn(),
getHashFromEntityMetadata: jest.fn(),
deleteObsoleteHashes: jest.fn(),
countAllBadges: jest.fn(),
getAllBadges: jest.fn(),
getBadgeFromUuid: jest.fn(),
getUuidFromEntityMetadata: jest.fn(),
addBadge: jest.fn(),
};
beforeAll(async () => {
@@ -93,6 +90,10 @@ describe('createRouter', () => {
backend: {
baseUrl: 'http://127.0.0.1',
listen: { port: 7007 },
database: {
client: 'better-sqlite3',
connection: ':memory:',
},
},
});
@@ -123,7 +124,6 @@ describe('createRouter', () => {
tokenManager,
logger: getVoidLogger(),
identity: { getIdentity },
db: badgeStore,
});
app = express().use(router);
});
@@ -132,7 +132,7 @@ describe('createRouter', () => {
jest.resetAllMocks();
});
it('works', async () => {
it('works with badgeStore', async () => {
const tokenManager = ServerTokenManager.noop();
const router = await createRouter({
badgeBuilder,
@@ -142,7 +142,7 @@ describe('createRouter', () => {
tokenManager,
logger: getVoidLogger(),
identity: { getIdentity },
db: badgeStore,
badgeStore: badgeStore,
});
expect(router).toBeDefined();
});
@@ -261,7 +261,7 @@ describe('createRouter', () => {
});
});
it('returns 404 for hashed entities', async () => {
it('returns 404 for uuid entities', async () => {
catalog.getEntityByRef.mockResolvedValue(undefined);
async function testUrl(url: string) {
const response = await request(app).get(url);