From 2ec31bc834fd76082b18ea37817d4b70b518108f Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Thu, 27 Jul 2023 08:35:06 -0700 Subject: [PATCH] refactor repo-mock package (#408) Signed-off-by: Brian DeHamer --- .changeset/fast-coins-laugh.md | 5 ++ .../repo-mock/src/__tests__/index.test.ts | 6 ++ packages/repo-mock/src/handler.ts | 57 +++++++++++++++ packages/repo-mock/src/index.ts | 69 ++++++------------- packages/repo-mock/src/mock.ts | 23 +++++++ packages/repo-mock/src/repo.ts | 39 +++++++++++ packages/repo-mock/src/shared.types.ts | 17 +++++ packages/repo-mock/src/target.ts | 5 +- 8 files changed, 169 insertions(+), 52 deletions(-) create mode 100644 .changeset/fast-coins-laugh.md create mode 100644 packages/repo-mock/src/handler.ts create mode 100644 packages/repo-mock/src/mock.ts create mode 100644 packages/repo-mock/src/repo.ts create mode 100644 packages/repo-mock/src/shared.types.ts diff --git a/.changeset/fast-coins-laugh.md b/.changeset/fast-coins-laugh.md new file mode 100644 index 00000000..6fb1b3d2 --- /dev/null +++ b/.changeset/fast-coins-laugh.md @@ -0,0 +1,5 @@ +--- +'@tufjs/repo-mock': minor +--- + +Export new helpers: `initializeTUFRepo` and `tufHandlers` diff --git a/packages/repo-mock/src/__tests__/index.test.ts b/packages/repo-mock/src/__tests__/index.test.ts index c2611d2e..eb85c09b 100644 --- a/packages/repo-mock/src/__tests__/index.test.ts +++ b/packages/repo-mock/src/__tests__/index.test.ts @@ -62,6 +62,12 @@ describe('mockRepo', () => { await expect(fetch(`${baseURL}/metadata/2.root.json`)).rejects.toThrow( /404/ ); + + // No mock should be set-up for the 1.root.json file as this should never be + // fetched in a normal TUF flow. + await expect(fetch(`${baseURL}/metadata/1.root.json`)).rejects.toThrow( + /No match for request/ + ); }); it('mocks the targets endpoints', async () => { diff --git a/packages/repo-mock/src/handler.ts b/packages/repo-mock/src/handler.ts new file mode 100644 index 00000000..befa09ef --- /dev/null +++ b/packages/repo-mock/src/handler.ts @@ -0,0 +1,57 @@ +import { Metadata } from '@tufjs/models'; +import { TUFRepo } from './repo'; +import { Handler, HandlerFn } from './shared.types'; + +export interface TUFHandlerOptions { + metadataPathPrefix?: string; + targetPathPrefix?: string; +} + +export function tufHandlers( + tufRepo: TUFRepo, + opts: TUFHandlerOptions +): Handler[] { + const metadataPrefix = opts.metadataPathPrefix ?? '/metadata'; + const targetPrefix = opts.targetPathPrefix ?? '/targets'; + + const handlers: Handler[] = [ + { + path: `${metadataPrefix}/1.root.json`, + fn: respondWithMetadata(tufRepo.rootMeta), + }, + { + path: `${metadataPrefix}/timestamp.json`, + fn: respondWithMetadata(tufRepo.timestampMeta), + }, + { + path: `${metadataPrefix}/snapshot.json`, + fn: respondWithMetadata(tufRepo.snapshotMeta), + }, + { + path: `${metadataPrefix}/targets.json`, + fn: respondWithMetadata(tufRepo.targetsMeta), + }, + { + path: `${metadataPrefix}/2.root.json`, + fn: () => ({ statusCode: 404, response: '' }), + }, + ]; + + tufRepo.targets.forEach((target) => { + handlers.push({ + path: `${targetPrefix}/${target.name}`, + fn: () => ({ statusCode: 200, response: target.content }), + }); + }); + + return handlers; +} + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +function respondWithMetadata(meta: Metadata): HandlerFn { + return () => ({ + statusCode: 200, + response: JSON.stringify(meta.toJSON()), + contentType: 'application/json', + }); +} diff --git a/packages/repo-mock/src/index.ts b/packages/repo-mock/src/index.ts index 60ee80c9..f5c654ea 100644 --- a/packages/repo-mock/src/index.ts +++ b/packages/repo-mock/src/index.ts @@ -2,66 +2,39 @@ import fs from 'fs'; import nock from 'nock'; import os from 'os'; import path from 'path'; -import { KeyPair } from './key'; -import { - createRootMeta, - createSnapshotMeta, - createTargetsMeta, - createTimestampMeta, -} from './metadata'; -import { Target, collectTargets } from './target'; +import { TUFHandlerOptions, tufHandlers } from './handler'; +import { mock } from './mock'; +import { initializeTUFRepo } from './repo'; +import { Target } from './shared.types'; -export type { Target } from './target'; +export { TUFHandlerOptions, tufHandlers } from './handler'; +export { TUFRepo, initializeTUFRepo } from './repo'; +export type { Target } from './shared.types'; -interface MockRepoOptions { +type MockRepoOptions = { baseURL?: string; - metadataPathPrefix?: string; - targetPathPrefix?: string; cachePath?: string; - responseCount?: number; -} +} & TUFHandlerOptions; export function mockRepo( baseURL: string, targets: Target[], - options: Omit = {} + options: TUFHandlerOptions = {} ): string { - const metadataPrefix = options.metadataPathPrefix ?? '/metadata'; - const targetPrefix = options.targetPathPrefix ?? '/targets'; - const count = options.responseCount ?? 1; - const keyPair = new KeyPair(); - - // Translate the input targets into TUF TargetFile objects - const targetFiles = collectTargets(targets); - - // Generate all of the TUF metadata objects - const targetsMeta = createTargetsMeta(targetFiles, keyPair); - const snapshotMeta = createSnapshotMeta(targetsMeta, keyPair); - const timestampMeta = createTimestampMeta(snapshotMeta, keyPair); - const rootMeta = createRootMeta(keyPair); - - // Calculate paths for all of the metadata files - const rootPath = `${metadataPrefix}/2.root.json`; - const timestampPath = `${metadataPrefix}/timestamp.json`; - const snapshotPath = `${metadataPrefix}/snapshot.json`; - const targetsPath = `${metadataPrefix}/targets.json`; - - // Mock the metadata endpoints - // Note: the root metadata file request always returns a 404 to indicate that - // the client should use the initial root metadata file from the cache - nock(baseURL).get(rootPath).times(count).reply(404); - nock(baseURL).get(timestampPath).times(count).reply(200, timestampMeta); - nock(baseURL).get(snapshotPath).times(count).reply(200, snapshotMeta); - nock(baseURL).get(targetsPath).times(count).reply(200, targetsMeta); + const tufRepo = initializeTUFRepo(targets); + const handlers = tufHandlers(tufRepo, options); + + handlers.forEach((handler) => { + // Don't set-up a mock for the 1.root.json file as this should never be + // fetched in a normal TUF flow. + if (handler.path.endsWith('1.root.json')) { + return; + } - // Mock the target endpoints - targets.forEach((target) => { - nock(baseURL) - .get(`${targetPrefix}/${target.name}`) - .reply(200, target.content); + mock(baseURL, handler); }); - return JSON.stringify(rootMeta); + return JSON.stringify(tufRepo.rootMeta.toJSON()); } export function clearMock() { diff --git a/packages/repo-mock/src/mock.ts b/packages/repo-mock/src/mock.ts new file mode 100644 index 00000000..88bb011f --- /dev/null +++ b/packages/repo-mock/src/mock.ts @@ -0,0 +1,23 @@ +import nock from 'nock'; +import type { Handler, HandlerFn } from './shared.types'; + +type NockHandler = (uri: string, request: nock.Body) => nock.ReplyFnResult; + +// Sets-up nock-based mocking for the given handler +export function mock(base: string, handler: Handler): void { + nock(base).get(handler.path).reply(adapt(handler.fn)); +} + +// Adapts our HandlerFn to nock's NockHandler format +function adapt(handler: HandlerFn): NockHandler { + /* istanbul ignore next */ + return (): nock.ReplyFnResult => { + const { statusCode, response, contentType } = handler(); + + return [ + statusCode, + response, + { 'Content-Type': contentType || 'text/plain' }, + ]; + }; +} diff --git a/packages/repo-mock/src/repo.ts b/packages/repo-mock/src/repo.ts new file mode 100644 index 00000000..9828d385 --- /dev/null +++ b/packages/repo-mock/src/repo.ts @@ -0,0 +1,39 @@ +import { Metadata, Root, Snapshot, Targets, Timestamp } from '@tufjs/models'; +import { KeyPair } from './key'; +import { + createRootMeta, + createSnapshotMeta, + createTargetsMeta, + createTimestampMeta, +} from './metadata'; +import { collectTargets } from './target'; + +import type { Target } from './shared.types'; + +export interface TUFRepo { + rootMeta: Metadata; + timestampMeta: Metadata; + snapshotMeta: Metadata; + targetsMeta: Metadata; + targets: Target[]; +} + +export function initializeTUFRepo(targets: Target[]): TUFRepo { + const keyPair = new KeyPair(); + // Translate the input targets into TUF TargetFile objects + const targetFiles = collectTargets(targets); + + // Generate all of the TUF metadata objects + const targetsMeta = createTargetsMeta(targetFiles, keyPair); + const snapshotMeta = createSnapshotMeta(targetsMeta, keyPair); + const timestampMeta = createTimestampMeta(snapshotMeta, keyPair); + const rootMeta = createRootMeta(keyPair); + + return { + rootMeta, + snapshotMeta, + timestampMeta, + targetsMeta, + targets, + }; +} diff --git a/packages/repo-mock/src/shared.types.ts b/packages/repo-mock/src/shared.types.ts new file mode 100644 index 00000000..3c8e495e --- /dev/null +++ b/packages/repo-mock/src/shared.types.ts @@ -0,0 +1,17 @@ +export interface Target { + name: string; + content: string | Buffer; +} + +export type HandlerFn = () => HandlerFnResult; + +export type HandlerFnResult = { + statusCode: number; + response: string | Buffer; + contentType?: string; +}; + +export type Handler = { + path: string; + fn: HandlerFn; +}; diff --git a/packages/repo-mock/src/target.ts b/packages/repo-mock/src/target.ts index fc817d2a..21dc4ba0 100644 --- a/packages/repo-mock/src/target.ts +++ b/packages/repo-mock/src/target.ts @@ -1,10 +1,7 @@ import { TargetFile } from '@tufjs/models'; import { digestSHA256 } from './crypto'; -export interface Target { - name: string; - content: string | Buffer; -} +import type { Target } from './shared.types'; export function collectTargets(targets: Target[]): TargetFile[] { return targets.map((target) => {