-
Notifications
You must be signed in to change notification settings - Fork 7
test(server-nestjs): migrate Vault client spec to MSW + @msw/data #2670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: test/msw-nexus-migrate
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,43 +1,170 @@ | ||
| import type { VaultSecret } from './vault-client.service' | ||
| import type { ProjectWithDetails, ZoneWithDetails } from './vault-datastore.service' | ||
| import type { HttpHandler } from 'msw' | ||
| import { faker } from '@faker-js/faker' | ||
| import { Collection } from '@msw/data' | ||
| import { http, HttpResponse } from 'msw' | ||
| import { z } from 'zod' | ||
|
|
||
| export function makeProjectWithDetails(overrides: Partial<ProjectWithDetails> = {}): ProjectWithDetails { | ||
| return { | ||
| id: faker.string.uuid(), | ||
| slug: faker.helpers.slugify(`test-project-${faker.string.uuid()}`), | ||
| name: faker.company.name(), | ||
| description: faker.company.buzzPhrase(), | ||
| environments: [], | ||
| plugins: [], | ||
| ...overrides, | ||
| } satisfies ProjectWithDetails | ||
| } | ||
| export const VAULT_INTERNAL_URL = 'https://vault.internal' | ||
|
|
||
| const kvSecretSchema = z.object({ | ||
| path: z.string(), | ||
| data: z.record(z.unknown()).optional(), | ||
| metadata: z.object({ | ||
| created_time: z.string(), | ||
| destroyed: z.boolean(), | ||
| version: z.number(), | ||
| }).optional(), | ||
| }) | ||
|
|
||
| const identityGroupSchema = z.object({ | ||
| id: z.string(), | ||
| name: z.string(), | ||
| alias: z.object({ | ||
| name: z.string(), | ||
| }).optional(), | ||
| }) | ||
|
|
||
| const authMethodSchema = z.object({ | ||
| type: z.string(), | ||
| description: z.string().optional(), | ||
| }) | ||
|
|
||
| export function makeZoneWithDetails(overrides: Partial<ZoneWithDetails> = {}): ZoneWithDetails { | ||
| export function makeVaultDb() { | ||
| return { | ||
| id: faker.string.uuid(), | ||
| slug: faker.helpers.slugify(`test-zone-${faker.string.uuid()}`), | ||
| clusters: [], | ||
| ...overrides, | ||
| } satisfies ZoneWithDetails | ||
| kv: new Collection({ schema: kvSecretSchema }), | ||
| identityGroups: new Collection({ schema: identityGroupSchema }), | ||
| authMethods: new Collection({ schema: authMethodSchema }), | ||
| } | ||
| } | ||
|
|
||
| export function makeVaultSecret(overrides: Partial<VaultSecret> = {}): VaultSecret { | ||
| export function makeVaultSecret<T>(data: T): { data: { data: T, metadata: { created_time: string, destroyed: boolean, version: number } } } { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🔴 Bloquant] |
||
| return { | ||
| data: {}, | ||
| metadata: makeVaultSecretMetadata(), | ||
| ...overrides, | ||
| } satisfies VaultSecret | ||
| data: { | ||
| data, | ||
| metadata: { | ||
| created_time: new Date().toISOString(), | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟡 Nit] |
||
| destroyed: false, | ||
| version: 1, | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| export function makeVaultSecretMetadata(overrides: Partial<VaultSecret['metadata']> = {}): VaultSecret['metadata'] { | ||
| return { | ||
| created_time: faker.date.soon().toISOString(), | ||
| custom_metadata: null, | ||
| deletion_time: '', | ||
| destroyed: false, | ||
| version: 1, | ||
| ...overrides, | ||
| export function makeVaultHandlers(db: ReturnType<typeof makeVaultDb>): HttpHandler[] { | ||
| const base = `${VAULT_INTERNAL_URL}/v1` | ||
|
|
||
| const findKv = async (path: string) => | ||
| db.kv.findFirst((q: any) => q.where({ path })) | ||
|
|
||
| const upsertKv = async (path: string, body: any) => { | ||
| const record = await findKv(path) | ||
| if (record) { | ||
| await db.kv.update(record, { data: body?.data ?? body }) | ||
| } else { | ||
| await db.kv.create({ | ||
| path, | ||
| data: body?.data ?? body, | ||
| metadata: { | ||
| created_time: new Date().toISOString(), | ||
| destroyed: false, | ||
| version: 1, | ||
| }, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| return [ | ||
| // KV read/write — Vault KV engine uses data/ prefix | ||
| http.get(`${base}/kv/data/*`, async ({ request }) => { | ||
| const path = new URL(request.url).pathname.replace('/v1/kv/data/', '') | ||
| const data = await findKv(path) | ||
| if (!data) return HttpResponse.json({}, { status: 404 }) | ||
| return HttpResponse.json({ data: { data: data.data, metadata: data.metadata } }) | ||
| }), | ||
| http.post(`${base}/kv/data/*`, async ({ request }) => { | ||
| const path = new URL(request.url).pathname.replace('/v1/kv/data/', '') | ||
| const body = await request.json() as any | ||
| await upsertKv(path, body) | ||
| return new HttpResponse(null, { status: 204 }) | ||
| }), | ||
| http.put(`${base}/kv/data/*`, async ({ request }) => { | ||
| const path = new URL(request.url).pathname.replace('/v1/kv/data/', '') | ||
| const body = await request.json() as any | ||
| await upsertKv(path, body) | ||
| return new HttpResponse(null, { status: 204 }) | ||
| }), | ||
|
|
||
| // KV metadata list (GET) | ||
| http.get(`${base}/kv/metadata/*`, async ({ request }) => { | ||
| const path = new URL(request.url).pathname.replace(`${base}/kv/metadata/`, '') | ||
| const all = await db.kv.findMany() | ||
| const keys = Array.from(new Set(all | ||
| .filter((r: any) => r.path.startsWith(path)) | ||
| .map((r: any) => r.path.slice(path.length).split('/')[0]) | ||
| .filter(Boolean))) | ||
| return HttpResponse.json({ data: { keys } }) | ||
| }), | ||
| http.delete(`${base}/kv/metadata/*`, () => new HttpResponse(null, { status: 204 })), | ||
|
|
||
| // LIST method — Vault uses HTTP LIST for directory listing | ||
| http.all(`${base}/kv/metadata/*`, async ({ request }) => { | ||
| if (request.method !== 'LIST') return new Response(null, { status: 404 }) | ||
| const path = new URL(request.url).pathname.replace(`${base}/kv/metadata/`, '') | ||
| const all = await db.kv.findMany() | ||
| const keys = Array.from(new Set(all | ||
| .filter((r: any) => r.path.startsWith(path)) | ||
| .map((r: any) => r.path.slice(path.length).split('/')[0]) | ||
| .filter(Boolean))) | ||
| return HttpResponse.json({ data: { keys } }) | ||
| }), | ||
|
|
||
| // Sys policies | ||
| http.post(`${base}/sys/policies/acl/:policy`, () => new HttpResponse(null, { status: 204 })), | ||
| http.delete(`${base}/sys/policies/acl/:policy`, () => new HttpResponse(null, { status: 204 })), | ||
|
|
||
| // Sys mounts | ||
| http.post(`${base}/sys/mounts/:name`, () => new HttpResponse(null, { status: 204 })), | ||
| http.post(`${base}/sys/mounts/:name/tune`, () => new HttpResponse(null, { status: 204 })), | ||
| http.delete(`${base}/sys/mounts/:name`, () => new HttpResponse(null, { status: 204 })), | ||
|
|
||
| // Auth approle | ||
| http.post(`${base}/auth/approle/role/:role`, () => new HttpResponse(null, { status: 204 })), | ||
| http.delete(`${base}/auth/approle/role/:role`, () => new HttpResponse(null, { status: 204 })), | ||
| http.get(`${base}/auth/approle/role/:role/role-id`, () => | ||
| HttpResponse.json({ data: { role_id: faker.string.uuid() } })), | ||
| http.post(`${base}/auth/approle/role/:role/secret-id`, () => | ||
| HttpResponse.json({ data: { secret_id: faker.string.uuid() } })), | ||
|
|
||
| // Sys auth | ||
| http.get(`${base}/sys/auth`, async () => { | ||
| const methods = await db.authMethods.findMany() | ||
| const obj: Record<string, { type: string, description?: string }> = {} | ||
| methods.forEach((m: any, i: number) => { | ||
| obj[`${m.type}-${i}/`] = { type: m.type, description: m.description } | ||
| }) | ||
| return HttpResponse.json({ data: obj }) | ||
| }), | ||
|
|
||
| // Identity groups | ||
| http.get(`${base}/identity/group/name/:name`, async ({ params }) => { | ||
| const data = await db.identityGroups.findFirst((q: any) => q.where({ name: String(params.name) })) | ||
| if (!data) return HttpResponse.json({}, { status: 404 }) | ||
| return HttpResponse.json({ data }) | ||
| }), | ||
| http.post(`${base}/identity/group/name/:name`, async ({ request, params }) => { | ||
| const body = await request.json() as any | ||
| await db.identityGroups.create({ | ||
| id: faker.string.uuid(), | ||
| name: String(params.name), | ||
| alias: body?.alias, | ||
| }) | ||
| return new HttpResponse(null, { status: 204 }) | ||
| }), | ||
| http.post(`${base}/identity/group-alias`, () => new HttpResponse(null, { status: 204 })), | ||
| http.delete(`${base}/identity/group/name/:name`, () => new HttpResponse(null, { status: 204 })), | ||
|
|
||
| // Token create | ||
| http.post(`${base}/auth/token/create`, () => | ||
| HttpResponse.json({ auth: { client_token: 'token' } })), | ||
| ] | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[✨ Éloge]
onUnhandledRequest: 'error'+ handlers réinstallés par test : bon durcissement, chaque test porte son propre état réseau.