mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-21 13:20:33 +08:00
Resolve #448 — one upload entry point and an AIP-164 soft delete. Upload: POST /objects now returns size-decided upload instructions { sessionId, partSize, urls }; the server picks single PutObject (<=5 GiB) vs 5 GiB-part multipart (>5 GiB) and rejects >5 TiB. The client PUTs each slice, reads its ETag, then POSTs them to POST /objects/{id}/uploads/{sid}/completions (returns the live object). DELETE /objects/{id}/uploads/{sid} aborts and discards the draft. Trash: matters.status drops 'trashed' (enum is {draft,active}); trash is tracked by the existing trashedAt timestamp. DELETE /objects/{id} now soft-deletes; the recycle bin lives under /trash/objects (list roots, get, restorations, purge). Empty-trash is a frontend loop over roots. BREAKING CHANGE: - removes PUT /objects/{id}/status and POST /objects/{id}/uploads - PUT .../uploads/{sid}/status -> POST .../uploads/{sid}/completions {parts} - DELETE /objects/{id} flips hard-purge -> soft-delete; permanent purge moves to DELETE /trash/objects/{id} - DELETE /trash removed; restore is POST /trash/objects/{id}/restorations - matters.status enum loses 'trashed' (migration backfills to trashedAt) The migration swaps the matters_active_name_uniq partial index to exclude trashed rows (WHERE status='active' AND trashed_at IS NULL). The single-PUT presign is header-free so the uniform slice uploader's raw PUT matches the S3 signature. Go downloader client + agent reworked to the unified flow. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2.8 KiB
TypeScript
65 lines
2.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { createTestApp } from './test/setup'
|
|
|
|
describe('global OpenAPI document', () => {
|
|
it('aggregates every OpenAPIHono route at /api/openapi.json', async () => {
|
|
const { app } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
|
|
const res = await app.request('/api/openapi.json')
|
|
|
|
expect(res.status).toBe(200)
|
|
const doc = (await res.json()) as {
|
|
openapi: string
|
|
paths: Record<string, { get?: { tags?: string[] } }>
|
|
tags?: { name: string }[]
|
|
}
|
|
expect(doc.openapi).toBe('3.1.0')
|
|
// Operations are tagged so Scalar groups them (not all under "default").
|
|
expect(doc.paths['/api/objects']?.get?.tags).toContain('Objects')
|
|
expect(doc.paths['/api/events']?.get?.tags).toContain('Events')
|
|
expect((doc.tags ?? []).map((t) => t.name)).toEqual(
|
|
expect.arrayContaining(['Objects', 'Events', 'Download Tasks', 'Downloaders']),
|
|
)
|
|
// Every resource already converted to `.openapi()` shows up automatically.
|
|
expect(Object.keys(doc.paths)).toEqual(
|
|
expect.arrayContaining([
|
|
'/api/downloads/tasks',
|
|
'/api/downloads/tasks/{id}',
|
|
'/api/downloads/tasks/{id}/status',
|
|
'/api/downloads/tasks/{id}/attempts',
|
|
'/api/downloads/downloaders',
|
|
'/api/downloads/downloaders/{id}',
|
|
'/api/events',
|
|
'/api/objects',
|
|
'/api/objects/{id}',
|
|
'/api/objects/{id}/uploads/{uploadSessionId}/parts',
|
|
'/api/objects/{id}/uploads/{uploadSessionId}/completions',
|
|
'/api/objects/{id}/uploads/{uploadSessionId}',
|
|
'/api/trash/objects',
|
|
'/api/trash/objects/{id}',
|
|
'/api/trash/objects/{id}/restorations',
|
|
]),
|
|
)
|
|
})
|
|
|
|
it('serves the Scalar reference UI at /api/docs pointing at the spec', async () => {
|
|
const { app } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
|
|
const res = await app.request('/api/docs')
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toContain('text/html')
|
|
const html = await res.text()
|
|
expect(html).toContain('/api/openapi.json')
|
|
})
|
|
|
|
it("merges better-auth's auto-generated schema (incl. the device flow) into the same doc", async () => {
|
|
const { app } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
|
|
const res = await app.request('/api/openapi.json')
|
|
const doc = (await res.json()) as { paths: Record<string, unknown> }
|
|
// better-auth's device-authorization endpoints come from its openAPI plugin,
|
|
// not hand-written stubs — prefixed under /api/auth.
|
|
const authPaths = Object.keys(doc.paths).filter((p) => p.startsWith('/api/auth/'))
|
|
expect(authPaths.length).toBeGreaterThan(0)
|
|
expect(authPaths.some((p) => p.includes('/device/'))).toBe(true)
|
|
})
|
|
})
|