This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type {
|
||||
GeneratedArtifactMetadata,
|
||||
GeneratedArtifactMetadataStore,
|
||||
ImmutableArtifactStorage,
|
||||
StoreGeneratedArtifactResult,
|
||||
WorkspaceAuthorizationRecord,
|
||||
} from '..'
|
||||
import {
|
||||
createGeneratedArtifact,
|
||||
downloadGeneratedArtifact,
|
||||
} from './generated-artifact'
|
||||
|
||||
const workspaceId = '00000000-0000-4000-8000-000000000101'
|
||||
const userId = '00000000-0000-4000-8000-000000000102'
|
||||
const runId = '00000000-0000-4000-8000-000000000103'
|
||||
const artifactId = '00000000-0000-4000-8000-000000000104'
|
||||
|
||||
class MemoryMetadata implements GeneratedArtifactMetadataStore {
|
||||
artifact: GeneratedArtifactMetadata | null = null
|
||||
|
||||
async createIdempotently(
|
||||
artifact: GeneratedArtifactMetadata,
|
||||
): Promise<StoreGeneratedArtifactResult> {
|
||||
if (!this.artifact) {
|
||||
this.artifact = artifact
|
||||
return { artifact, created: true }
|
||||
}
|
||||
return { artifact: this.artifact, created: false }
|
||||
}
|
||||
|
||||
async findByIdInWorkspace(id: string, workspace: string) {
|
||||
return this.artifact?.id === id && this.artifact.workspaceId === workspace
|
||||
? this.artifact
|
||||
: null
|
||||
}
|
||||
|
||||
async listByRunInWorkspace(runId: string, workspace: string) {
|
||||
return this.artifact?.runId === runId &&
|
||||
this.artifact.workspaceId === workspace
|
||||
? [this.artifact]
|
||||
: []
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryStorage implements ImmutableArtifactStorage {
|
||||
readonly bytes = new Map<string, Uint8Array>()
|
||||
|
||||
async putImmutable(key: string, content: Uint8Array) {
|
||||
const created = !this.bytes.has(key)
|
||||
if (created) this.bytes.set(key, new Uint8Array(content))
|
||||
return { created }
|
||||
}
|
||||
|
||||
async read(key: string) {
|
||||
const content = this.bytes.get(key)
|
||||
if (!content) throw new Error('missing')
|
||||
return new Uint8Array(content)
|
||||
}
|
||||
}
|
||||
|
||||
function authorization(role: 'viewer' | 'editor' | 'owner') {
|
||||
return {
|
||||
async findWorkspaceAuthorization(): Promise<WorkspaceAuthorizationRecord> {
|
||||
return {
|
||||
userId,
|
||||
workspaceId,
|
||||
instanceRole: 'user',
|
||||
workspaceRole: role,
|
||||
userStatus: 'active',
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function request(content = new TextEncoder().encode('# Run\n')) {
|
||||
return {
|
||||
actor: { userId },
|
||||
workspaceId,
|
||||
artifactId,
|
||||
runId,
|
||||
artifactType: 'markdown' as const,
|
||||
filename: 'run.md',
|
||||
mediaType: 'text/markdown; charset=utf-8',
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
describe('generated artifact use cases', () => {
|
||||
it('creates immutable bytes and metadata idempotently', async () => {
|
||||
const metadata = new MemoryMetadata()
|
||||
const storage = new MemoryStorage()
|
||||
const dependencies = {
|
||||
authorization: authorization('editor'),
|
||||
metadata,
|
||||
storage,
|
||||
now: () => new Date('2026-07-27T12:00:00.000Z'),
|
||||
}
|
||||
|
||||
await expect(
|
||||
createGeneratedArtifact(dependencies, request()),
|
||||
).resolves.toMatchObject({ created: true })
|
||||
await expect(
|
||||
createGeneratedArtifact(dependencies, request()),
|
||||
).resolves.toMatchObject({ created: false })
|
||||
expect(metadata.artifact?.sha256).toHaveLength(64)
|
||||
expect(metadata.artifact?.sizeBytes).toBe(6n)
|
||||
expect(metadata.artifact?.storageKey).toMatch(/^[0-9a-f]{64}$/)
|
||||
expect(storage.bytes).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('allows a viewer to download only from the authorized workspace', async () => {
|
||||
const metadata = new MemoryMetadata()
|
||||
const storage = new MemoryStorage()
|
||||
await createGeneratedArtifact(
|
||||
{
|
||||
authorization: authorization('editor'),
|
||||
metadata,
|
||||
storage,
|
||||
now: () => new Date('2026-07-27T12:00:00.000Z'),
|
||||
},
|
||||
request(),
|
||||
)
|
||||
|
||||
const download = await downloadGeneratedArtifact(
|
||||
{ authorization: authorization('viewer'), metadata, storage },
|
||||
{ actor: { userId }, workspaceId, artifactId },
|
||||
)
|
||||
expect(new TextDecoder().decode(download.content)).toBe('# Run\n')
|
||||
await expect(
|
||||
downloadGeneratedArtifact(
|
||||
{ authorization: authorization('viewer'), metadata, storage },
|
||||
{
|
||||
actor: { userId },
|
||||
workspaceId: '00000000-0000-4000-8000-000000000999',
|
||||
artifactId,
|
||||
},
|
||||
),
|
||||
).rejects.toMatchObject({ code: 'workspace_access_denied' })
|
||||
})
|
||||
|
||||
it('rejects unsafe filenames, viewer creation, and corrupted bytes', async () => {
|
||||
const metadata = new MemoryMetadata()
|
||||
const storage = new MemoryStorage()
|
||||
const base = {
|
||||
authorization: authorization('editor'),
|
||||
metadata,
|
||||
storage,
|
||||
now: () => new Date('2026-07-27T12:00:00.000Z'),
|
||||
}
|
||||
await expect(
|
||||
createGeneratedArtifact(base, { ...request(), filename: '../run.md' }),
|
||||
).rejects.toMatchObject({ code: 'generated_artifact_filename_invalid' })
|
||||
await expect(
|
||||
createGeneratedArtifact(
|
||||
{ ...base, authorization: authorization('viewer') },
|
||||
request(),
|
||||
),
|
||||
).rejects.toMatchObject({ code: 'workspace_access_denied' })
|
||||
|
||||
await createGeneratedArtifact(base, request())
|
||||
const key = metadata.artifact?.storageKey
|
||||
if (!key) throw new Error('expected storage key')
|
||||
storage.bytes.set(key, new TextEncoder().encode('tampered'))
|
||||
await expect(
|
||||
downloadGeneratedArtifact(
|
||||
{ authorization: authorization('viewer'), metadata, storage },
|
||||
{ actor: { userId }, workspaceId, artifactId },
|
||||
),
|
||||
).rejects.toMatchObject({ code: 'generated_artifact_integrity_failed' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user