Gate admin menu and authoring with shared JWT
This commit is contained in:
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import pg from 'pg'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import type { CaseState } from '../src/types.js'
|
||||
import { runMigrations } from './migrations.js'
|
||||
@@ -15,6 +16,13 @@ let adminClient: InstanceType<typeof Client>
|
||||
let appServer: Awaited<typeof import('./index.js')>['server']
|
||||
let appPool: Awaited<typeof import('./index.js')>['pool']
|
||||
let baseUrl = ''
|
||||
let adminAuthorization = ''
|
||||
|
||||
function adminFetch(url: string, init: RequestInit = {}) {
|
||||
const headers = new Headers(init.headers)
|
||||
headers.set('authorization', adminAuthorization)
|
||||
return fetch(url, { ...init, headers })
|
||||
}
|
||||
|
||||
async function availablePort() {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
@@ -44,11 +52,13 @@ suite('level persistence API', () => {
|
||||
const port = await availablePort()
|
||||
process.env.DATABASE_URL = databaseUrl
|
||||
process.env.LEVEL_EDITING_ENABLED = 'true'
|
||||
process.env.JWT_SECRET = 'osint-integration-jwt-secret'
|
||||
process.env.PORT = String(port)
|
||||
const serverModule = await import('./index.js')
|
||||
appServer = serverModule.server
|
||||
appPool = serverModule.pool
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
adminAuthorization = `Bearer ${jwt.sign({ sub: 'integration-admin', role: 'admin' }, process.env.JWT_SECRET)}`
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -60,7 +70,10 @@ suite('level persistence API', () => {
|
||||
})
|
||||
|
||||
it('persists one normalized level across authoring and play views', async () => {
|
||||
const createResponse = await fetch(`${baseUrl}/api/levels`, {
|
||||
expect(await (await fetch(`${baseUrl}/api/session`)).json()).toEqual({ authenticated: false, isAdmin: false })
|
||||
expect(await (await adminFetch(`${baseUrl}/api/session`)).json()).toEqual({ authenticated: true, isAdmin: true })
|
||||
expect((await fetch(`${baseUrl}/api/levels`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })).status).toBe(403)
|
||||
const createResponse = await adminFetch(`${baseUrl}/api/levels`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ id: 'api-smoke-level', title: 'API Smoke Level' }),
|
||||
@@ -88,14 +101,14 @@ suite('level persistence API', () => {
|
||||
state.relations = [{ id: `contains:${folderId}:${documentId}`, fromWidgetId: folderId, toWidgetId: documentId, type: 'contains', sortOrder: 0, config: { x: 1051, y: 417 } }]
|
||||
state.connections = [{ id: randomUUID(), fromEvidenceId: folderId, toEvidenceId: documentId, label: 'Primary source', tightness: 85, tagStyle: 'compact', tagPosition: 72, tagOffset: -12 }]
|
||||
|
||||
const saveResponse = await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
const saveResponse = await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(state),
|
||||
})
|
||||
expect(await saveResponse.json()).toEqual({ ok: true, mode: 'author' })
|
||||
|
||||
const loaded = await (await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
const loaded = await (await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
expect(loaded.viewport).toEqual(state.viewport)
|
||||
expect(loaded.timelineRange).toEqual(state.timelineRange)
|
||||
expect(loaded.evidence[0]).toMatchObject({ id: folderId, x: 685, y: 417, config: { open: true } })
|
||||
@@ -115,25 +128,25 @@ suite('level persistence API', () => {
|
||||
|
||||
const upload = new FormData()
|
||||
upload.append('file', new Blob(['OSINT smoke evidence'], { type: 'text/plain' }), 'smoke-evidence.txt')
|
||||
const uploadResponse = await fetch(`${baseUrl}/api/levels/${state.id}/documents?edit=1`, { method: 'POST', body: upload })
|
||||
const uploadResponse = await adminFetch(`${baseUrl}/api/levels/${state.id}/documents?edit=1`, { method: 'POST', body: upload })
|
||||
expect(uploadResponse.status).toBe(201)
|
||||
const uploaded = await uploadResponse.json() as CaseState['documents'][number]
|
||||
expect(uploaded).toMatchObject({ title: 'smoke-evidence.txt', fileName: 'smoke-evidence.txt', mimeType: 'text/plain', fileType: 'text' })
|
||||
expect(uploaded.assetId).toBeTruthy()
|
||||
expect(await (await fetch(`${baseUrl}/api/assets/${uploaded.assetId}`)).text()).toBe('OSINT smoke evidence')
|
||||
|
||||
const withUpload = await (await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
const withUpload = await (await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
const uploadedDocument = withUpload.documents.find(document => document.id === uploaded.id)!
|
||||
uploadedDocument.title = 'Renamed smoke evidence'
|
||||
uploadedDocument.publishedAt = '2022-06-15T10:30:00.000Z'
|
||||
uploadedDocument.metadata = { witness: 'Integration test', confidence: 'high' }
|
||||
const metadataSave = await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
const metadataSave = await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(withUpload),
|
||||
})
|
||||
expect(metadataSave.ok).toBe(true)
|
||||
const afterMetadataSave = await (await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
const afterMetadataSave = await (await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
expect(afterMetadataSave.documents.find(document => document.id === uploaded.id)).toMatchObject({
|
||||
title: 'Renamed smoke evidence',
|
||||
publishedAt: '2022-06-15T10:30:00.000Z',
|
||||
@@ -143,15 +156,15 @@ suite('level persistence API', () => {
|
||||
const undatedState = structuredClone(afterMetadataSave)
|
||||
const undatedEvent = undatedState.evidence.find(exhibit => exhibit.id === eventId)!
|
||||
delete undatedEvent.eventDate
|
||||
const undatedSave = await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
const undatedSave = await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(undatedState),
|
||||
})
|
||||
expect(undatedSave.ok).toBe(true)
|
||||
const loadedUndated = await (await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
const loadedUndated = await (await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
expect(loadedUndated.evidence.find(exhibit => exhibit.id === eventId)?.eventDate).toBeUndefined()
|
||||
expect((await appPool.query<{ occurred_at: Date | null }>('SELECT occurred_at FROM osint.event_exhibits WHERE exhibit_id=$1', [eventId])).rows[0].occurred_at).toBeNull()
|
||||
loadedUndated.evidence.find(exhibit => exhibit.id === eventId)!.eventDate = '2021-04-18T14:30:00Z'
|
||||
expect((await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
expect((await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(loadedUndated),
|
||||
})).ok).toBe(true)
|
||||
|
||||
@@ -176,7 +189,7 @@ suite('level persistence API', () => {
|
||||
const savedPlayerState = await (await fetch(`${baseUrl}/api/levels/${state.id}`)).json() as CaseState
|
||||
expect(savedPlayerState.viewport).toEqual(playerState.viewport)
|
||||
expect(savedPlayerState.evidence[0]).toMatchObject({ id: folderId, x: 812, y: 533 })
|
||||
const sameLevelInEditView = await (await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
const sameLevelInEditView = await (await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
expect(sameLevelInEditView.viewport).toEqual(playerState.viewport)
|
||||
expect(sameLevelInEditView.evidence[0]).toMatchObject({ id: folderId, x: 812, y: 533 })
|
||||
|
||||
@@ -205,7 +218,7 @@ suite('level persistence API', () => {
|
||||
expect(resetState.timelineRange).toEqual(state.timelineRange)
|
||||
expect(resetState.evidence[0]).toMatchObject({ id: folderId, x: 812, y: 533 })
|
||||
|
||||
const templateResponse = await fetch(`${baseUrl}/api/levels/${state.id}/templates?edit=1`, {
|
||||
const templateResponse = await adminFetch(`${baseUrl}/api/levels/${state.id}/templates?edit=1`, {
|
||||
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'Smoke Template' }),
|
||||
})
|
||||
expect(templateResponse.status).toBe(201)
|
||||
@@ -217,10 +230,10 @@ suite('level persistence API', () => {
|
||||
const changedSource = structuredClone(savedPlayerState)
|
||||
changedSource.title = 'Changed after template freeze'
|
||||
changedSource.evidence[0].x = 999
|
||||
await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
|
||||
method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(changedSource),
|
||||
})
|
||||
const cloneResponse = await fetch(`${baseUrl}/api/templates/smoke-template/levels?edit=1`, {
|
||||
const cloneResponse = await adminFetch(`${baseUrl}/api/templates/smoke-template/levels?edit=1`, {
|
||||
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: 'smoke-template-copy', title: 'Playable copy' }),
|
||||
})
|
||||
expect(cloneResponse.status).toBe(201)
|
||||
@@ -242,7 +255,7 @@ suite('level persistence API', () => {
|
||||
expect(clone.evidence.filter(item => item.type === 'party')).toHaveLength(2)
|
||||
expect(clone.brief.concepts.every(concept => Boolean(concept.resolvedPartyExhibitId))).toBe(true)
|
||||
expect(clone.brief.concepts.map(concept => concept.resolvedPartyExhibitId)).not.toContain(personPartyId)
|
||||
const authoredClone = await (await fetch(`${baseUrl}/api/levels/${clone.id}?edit=1`)).json() as CaseState
|
||||
const authoredClone = await (await adminFetch(`${baseUrl}/api/levels/${clone.id}?edit=1`)).json() as CaseState
|
||||
expect(authoredClone.brief.concepts.map(concept => concept.expectedPartyKind).sort()).toEqual(['organization', 'person'])
|
||||
|
||||
const clonedFolder = clone.evidence.find(item => item.type === 'folder')!
|
||||
@@ -256,14 +269,14 @@ suite('level persistence API', () => {
|
||||
expect(cloneReset.evidence.find(item => item.type === 'folder')).toMatchObject({ x: 812, y: 533 })
|
||||
expect(cloneReset.evidence.map(item => item.id)).not.toContain(clonedFolder.id)
|
||||
|
||||
const versionTwoResponse = await fetch(`${baseUrl}/api/levels/${state.id}/templates?edit=1`, {
|
||||
const versionTwoResponse = await adminFetch(`${baseUrl}/api/levels/${state.id}/templates?edit=1`, {
|
||||
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'Smoke Template' }),
|
||||
})
|
||||
expect(await versionTwoResponse.json()).toMatchObject({ currentVersion: 2, versionCount: 2 })
|
||||
const oldVersion = await (await fetch(`${baseUrl}/api/templates/smoke-template/levels?edit=1`, {
|
||||
const oldVersion = await (await adminFetch(`${baseUrl}/api/templates/smoke-template/levels?edit=1`, {
|
||||
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: 'old-version-copy', version: 1 }),
|
||||
})).json() as CaseState
|
||||
const currentVersion = await (await fetch(`${baseUrl}/api/templates/smoke-template/levels?edit=1`, {
|
||||
const currentVersion = await (await adminFetch(`${baseUrl}/api/templates/smoke-template/levels?edit=1`, {
|
||||
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: 'current-version-copy' }),
|
||||
})).json() as CaseState
|
||||
expect(oldVersion.evidence.find(item => item.type === 'folder')).toMatchObject({ x: 812 })
|
||||
|
||||
Reference in New Issue
Block a user