Files
zpan/server/http/objects.integration.test.ts
T
jarvis 82f5988d56 fix(objects): load creator profiles on demand
Keep object list responses limited to recorded creator identities and resolve full profiles through the creator subresource. Normalize list avatar sizing and avoid retrying terminal creator lookup errors.
2026-08-16 21:06:09 -04:00

3321 lines
137 KiB
TypeScript

import { createHash, randomUUID } from 'node:crypto'
import { createServer, type IncomingMessage, type Server } from 'node:http'
import { eq, sql } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { S3Service } from '../adapters/gateways/s3.js'
import { createDownloadTokenGateway } from '../adapters/repos/download-tokens.js'
import { createMatterRepo } from '../adapters/repos/matter.js'
import { createQuotaRepo } from '../adapters/repos/quota.js'
import { createStorageUsageRepo } from '../adapters/repos/storage-usage.js'
import { cloudTrafficReports, downloadTasks, orgQuotaEntitlements, orgQuotas } from '../db/schema.js'
import { currentTrafficPeriod } from '../domain/quota.js'
import { adminHeaders, authedHeaders, createTestApp, seedBusinessLicense, seedProLicense } from '../test/setup.js'
import { type ConfirmUploadOptions, confirmUpload as confirmUploadUsecase } from '../usecases/object.js'
import type {
ActorIdentity,
CopyMatterOptions,
CreateMatterInput,
Matter,
MatterListFilters,
UpdateMatterInput,
} from '../usecases/ports.js'
import { actorIdentityKey } from '../usecases/ports.js'
import { createCapacityRequestHash } from './objects.js'
type TestDbForMatter = Awaited<ReturnType<typeof createTestApp>>['db']
// Thin adapters preserving the former matter service signatures so these
// behavioral tests exercise the migrated MatterRepo + confirmUpload usecase
// unchanged.
function createMatter(db: TestDbForMatter, input: CreateMatterInput): Promise<Matter> {
return createMatterRepo(db).create(input)
}
function getMatter(db: TestDbForMatter, id: string, orgId: string) {
return createMatterRepo(db).get(id, orgId)
}
function getMatters(db: TestDbForMatter, orgId: string, ids: string[]) {
return createMatterRepo(db).getMany(orgId, ids)
}
function listMatters(db: TestDbForMatter, orgId: string, filters: MatterListFilters) {
return createMatterRepo(db).list(orgId, filters)
}
function updateMatter(db: TestDbForMatter, id: string, orgId: string, input: UpdateMatterInput) {
return createMatterRepo(db).update(id, orgId, input)
}
function copyMatter(
db: TestDbForMatter,
source: Matter,
targetParent: string,
newObject: string,
opts?: CopyMatterOptions,
) {
return createMatterRepo(db).copy(source, targetParent, newObject, opts)
}
async function deleteMatter(db: TestDbForMatter, id: string, orgId: string) {
const repo = createMatterRepo(db)
const existing = await repo.get(id, orgId)
if (!existing) return null
await repo.purge(orgId, [id])
return existing
}
function confirmUpload(db: TestDbForMatter, id: string, orgId: string, opts: ConfirmUploadOptions = {}) {
return confirmUploadUsecase(
{
matter: createMatterRepo(db),
quota: createQuotaRepo(db),
storageUsage: createStorageUsageRepo(db),
},
id,
orgId,
opts,
)
}
beforeEach(() => {
vi.restoreAllMocks()
vi.spyOn(S3Service.prototype, 'deleteObject').mockResolvedValue(undefined)
vi.spyOn(S3Service.prototype, 'deleteObjects').mockResolvedValue(undefined)
vi.spyOn(S3Service.prototype, 'presignUpload').mockResolvedValue('https://presigned-upload.example.com')
vi.spyOn(S3Service.prototype, 'presignDownload').mockResolvedValue('https://presigned-download.example.com')
vi.spyOn(S3Service.prototype, 'copyObject').mockResolvedValue(undefined)
// Single-PUT completion HEADs the object and matches the client's reported ETag.
// Tests that finalize a ≤5 GiB upload send parts:[{partNumber:1, etag:'abc'}].
vi.spyOn(S3Service.prototype, 'headObject').mockResolvedValue({
size: 100,
contentType: 'text/plain',
etag: 'abc',
})
vi.spyOn(S3Service.prototype, 'completeMultipartUpload').mockResolvedValue(undefined)
})
describe('capacity purchase challenges', () => {
it('creates a fresh request hash for repeated identical uploads', async () => {
const input = { name: 'same.bin', type: 'application/octet-stream', size: 1024 }
const first = await createCapacityRequestHash('Org1', input)
const second = await createCapacityRequestHash('Org1', input)
expect(first).toMatch(/^[0-9a-f]{64}$/)
expect(second).toMatch(/^[0-9a-f]{64}$/)
expect(second).not.toBe(first)
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
const validStorage = {
id: 'St1',
bucket: 'test-bucket',
endpoint: 'https://s3.amazonaws.com',
region: 'us-east-1',
accessKey: 'AKIAIOSFODNN7EXAMPLE',
secretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
}
async function insertStorage(
db: Awaited<ReturnType<typeof createTestApp>>['db'],
opts: { id?: string; metered?: boolean; capacity?: number; used?: number; enabled?: boolean } = {},
) {
const now = Date.now()
const metered = opts.metered ? 1 : 0
const id = opts.id ?? validStorage.id
await db.run(sql`
INSERT INTO storages (
id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host,
capacity, used, enabled, status, egress_credit_billing_enabled, egress_credit_unit_bytes,
egress_credit_per_unit, created_at, updated_at
)
VALUES (
${id}, ${validStorage.bucket},
${validStorage.endpoint}, ${validStorage.region}, ${validStorage.accessKey}, ${validStorage.secretKey},
'', '', ${opts.capacity ?? 0}, ${opts.used ?? 0}, ${(opts.enabled ?? true) ? 1 : 0}, 'untested', ${metered}, ${100 * 1024 ** 2}, 1, ${now}, ${now}
)
`)
}
async function insertFolder(
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
opts: { id: string; name: string; parent?: string; trashedAt?: number },
) {
const now = Date.now()
await db.run(sql`
INSERT INTO matters (id, org_id, alias, name, type, size, dirtype, parent, object, storage_id, status, trashed_at, created_at, updated_at)
VALUES (${opts.id}, ${orgId}, ${`${opts.id}-alias`}, ${opts.name}, 'folder', 0, 1, ${opts.parent ?? ''}, '', ${validStorage.id}, 'active', ${opts.trashedAt ?? null}, ${now}, ${now})
`)
}
// Inserts a matter row. A "trashed" object is status='active' with trashedAt set;
// a "draft" is status='draft'. Pass `trashedAt` to drop the row in the recycle bin.
async function insertFile(
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
opts: {
id: string
name: string
parent?: string
status?: string
size?: number
trashedAt?: number
createdBy?: ActorIdentity
},
) {
const now = Date.now()
const status = opts.status ?? 'active'
const size = opts.size ?? 100
await db.run(sql`
INSERT INTO matters (
id, org_id, alias, name, type, size, dirtype, parent, object, storage_id, status, trashed_at,
created_by_actor_type, created_by_actor_ref, created_by_actor_issuer, created_at, updated_at
)
VALUES (
${opts.id}, ${orgId}, ${`${opts.id}-alias`}, ${opts.name}, 'text/plain', ${size}, 0,
${opts.parent ?? ''}, 'some/key.txt', ${validStorage.id}, ${status}, ${opts.trashedAt ?? null},
${opts.createdBy?.type ?? null}, ${opts.createdBy?.ref ?? null}, ${opts.createdBy?.issuer ?? null}, ${now}, ${now}
)
`)
}
// Drives the full file-upload flow against the in-memory app: POST /api/objects
// creates a draft + upload session, then POST .../completions finalizes it to a
// live object. Returns the created object id. The S3 spies make the single-PUT
// HEAD return etag 'abc', so completions sends a matching part.
async function _uploadFile(
app: Awaited<ReturnType<typeof createTestApp>>['app'],
headers: Record<string, string>,
body: { name: string; type?: string; size?: number; parent?: string; onConflict?: string },
): Promise<string> {
const createRes = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'text/plain', size: 100, parent: '', dirtype: 0, ...body }),
})
if (createRes.status !== 201) throw new Error(`create failed: ${createRes.status}`)
const created = (await createRes.json()) as { id: string; upload: { sessionId: string } }
const completeRes = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}/completions`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parts: [{ partNumber: 1, etag: 'abc' }] }),
})
if (completeRes.status !== 200) throw new Error(`complete failed: ${completeRes.status}`)
return created.id
}
async function getOrgQuota(db: Awaited<ReturnType<typeof createTestApp>>['db'], orgId: string) {
const rows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId} LIMIT 1`)
return rows[0] ?? null
}
async function getOrgId(db: Awaited<ReturnType<typeof createTestApp>>['db']): Promise<string> {
const rows = await db.all<{ id: string }>(sql`
SELECT id FROM organization WHERE metadata LIKE '%"type":"personal"%' LIMIT 1
`)
return rows[0].id
}
describe('Objects API', () => {
it('returns 401 without auth [spec: objects/auth-required]', async () => {
const { app } = await createTestApp()
const res = await app.request('/api/objects')
expect(res.status).toBe(401)
})
it('GET /api/objects returns empty list [spec: objects/list-empty]', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects', { headers })
expect(res.status).toBe(200)
const body = await res.json()
expect(body).toEqual({ items: [], nextPageToken: null })
})
it('GET /api/objects returns the cursor page contract [spec: objects/list-pagination]', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects?page=2&pageSize=10', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as { items: unknown[]; nextPageToken: string | null }
expect(body).toEqual({ items: [], nextPageToken: null })
})
it('GET /api/objects does not wait for external creator profiles [spec: objects/creator-attribution]', async () => {
const { app, db, deps } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const identity = { type: 'oauth', ref: 'agent-1', issuer: 'https://id.realmroot.dev/api/auth' } as const
await insertFile(db, orgId, { id: 'LazyCreatorFile', name: 'lazy.txt', createdBy: identity })
vi.spyOn(deps.auditActorDirectory, 'listTrustedAgentIssuerOrigins').mockResolvedValue(
new Set(['https://id.realmroot.dev']),
)
vi.spyOn(deps.agentInfo, 'resolve').mockImplementation(() => new Promise(() => {}))
const res = await Promise.race([
app.request('/api/objects?path=&pageSize=100', { headers }),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('object_list_waited_for_creator')), 100)),
])
expect(res.status).toBe(200)
const body = (await res.json()) as { items: Array<{ id: string; createdBy: unknown }> }
expect(body.items).toHaveLength(1)
expect(body.items[0]).toMatchObject({ id: 'LazyCreatorFile' })
expect(body.items[0]?.createdBy).toEqual(identity)
expect(deps.agentInfo.resolve).not.toHaveBeenCalled()
})
it('GET /api/objects/{id}/creator resolves the creator profile on demand', async () => {
const { app, db, deps } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const identity = { type: 'oauth', ref: 'agent-1', issuer: 'https://id.realmroot.dev/api/auth' } as const
await insertFile(db, orgId, { id: 'CreatorProfileFile', name: 'creator.txt', createdBy: identity })
vi.spyOn(deps.auditActorDirectory, 'listTrustedAgentIssuerOrigins').mockResolvedValue(
new Set(['https://id.realmroot.dev']),
)
vi.spyOn(deps.agentInfo, 'resolve').mockResolvedValue(
new Map([
[
actorIdentityKey(identity),
{
name: 'Jarvis',
image: 'https://id.realmroot.dev/agent-picture-v1.svg',
profileUrl: 'https://id.realmroot.dev/agents/agent-1',
resolved: true,
},
],
]),
)
const res = await app.request('/api/objects/CreatorProfileFile/creator', { headers })
expect(res.status).toBe(200)
await expect(res.json()).resolves.toEqual({
...identity,
name: 'Jarvis',
image: 'https://id.realmroot.dev/agent-picture-v1.svg',
profileUrl: 'https://id.realmroot.dev/agents/agent-1',
})
})
it('GET /api/objects/{id}/creator does not return a fallback profile', async () => {
const { app, db, deps } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const identity = { type: 'oauth', ref: 'missing-agent', issuer: 'https://id.realmroot.dev/api/auth' } as const
await insertFile(db, orgId, { id: 'MissingCreatorProfile', name: 'missing.txt', createdBy: identity })
vi.spyOn(deps.auditActorDirectory, 'listTrustedAgentIssuerOrigins').mockResolvedValue(
new Set(['https://id.realmroot.dev']),
)
vi.spyOn(deps.agentInfo, 'resolve').mockResolvedValue(new Map())
const res = await app.request('/api/objects/MissingCreatorProfile/creator', { headers })
expect(res.status).toBe(404)
})
it('GET /api/objects enforces the shared page-size limit', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects?pageSize=500', { headers })
expect(res.status).toBe(400)
})
it('GET /api/objects lists folders with hasChildren', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'ParentFolder', name: 'Parent' })
await insertFolder(db, orgId, { id: 'LeafFolder', name: 'Leaf' })
await insertFolder(db, orgId, { id: 'ChildFolder', name: 'Child', parent: 'Parent' })
await insertFile(db, orgId, { id: 'RootFile', name: 'root.txt' })
await insertFile(db, orgId, { id: 'ChildFile', name: 'child.txt', parent: 'Leaf' })
const res = await app.request('/api/objects?path=&type=folder&page=1&pageSize=100', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as {
items: Array<{ id: string; hasChildren: boolean }>
nextPageToken: string | null
}
expect(body.nextPageToken).toBeNull()
expect(body.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: 'ParentFolder', hasChildren: true }),
expect.objectContaining({ id: 'LeafFolder', hasChildren: false }),
]),
)
})
it('GET /api/objects composes folder and search filters', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'ReportsFolder', name: 'Reports' })
await insertFile(db, orgId, { id: 'ReportsFile', name: 'Reports.txt' })
const res = await app.request('/api/objects?type=folder&search=Reports', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as { items: Array<{ id: string }> }
expect(body.items).toHaveLength(1)
expect(body.items.map((item) => item.id)).toEqual(['ReportsFolder'])
})
it('POST /api/objects creates a folder [spec: objects/create-folder]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'My Folder', type: 'folder', dirtype: 1 }),
})
expect(res.status).toBe(201)
const body = (await res.json()) as Record<string, unknown>
expect(body.name).toBe('My Folder')
expect(body.dirtype).toBe(1)
expect(body.status).toBe('active')
expect(body.object).toBe('')
expect(body.id).toBeTruthy()
})
it('POST /api/objects returns 400 for invalid input [spec: objects/create-invalid]', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '' }),
})
expect(res.status).toBe(400)
})
it('POST /api/objects returns 503 when no storage available [spec: objects/create-no-storage]', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test.txt', type: 'text/plain' }),
})
expect(res.status).toBe(503)
const body = (await res.json()) as { error: { message: string; details: Array<{ reason: string }> } }
expect(body.error.message).toBe('No storage configured')
expect(body.error.details[0].reason).toBe('NO_STORAGE_CONFIGURED')
})
it('GET /api/objects lists active objects in root', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, { metered: true })
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'f1', name: 'Folder A' })
await insertFile(db, orgId, { id: 'm1', name: 'file.txt' })
const res = await app.request('/api/objects', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items).toHaveLength(2)
// Folders sort before files (dirtype DESC)
expect(body.items[0].name).toBe('Folder A')
expect(body.items[1].name).toBe('file.txt')
})
it('GET /api/objects filters by parent [spec: objects/list-by-parent]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'f1', name: 'Folder A' })
await insertFile(db, orgId, { id: 'm1', name: 'nested.txt', parent: 'Folder A' })
await insertFile(db, orgId, { id: 'm2', name: 'root.txt' })
const res = await app.request(`/api/objects?parent=${encodeURIComponent('Folder A')}`, { headers })
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items).toHaveLength(1)
expect(body.items[0].name).toBe('nested.txt')
})
it('GET /api/objects lists live objects only — excludes drafts and trashed [spec: objects/list-live-only]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'active.txt', status: 'active' })
await insertFile(db, orgId, { id: 'm2', name: 'draft.txt', status: 'draft' })
await insertFile(db, orgId, { id: 'm3', name: 'trashed.txt', trashedAt: Date.now() })
const res = await app.request('/api/objects', { headers })
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items).toHaveLength(1)
expect(body.items[0].name).toBe('active.txt')
})
it('GET /api/objects/:id returns folder detail [spec: objects/detail]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'f1', name: 'My Folder' })
const res = await app.request('/api/objects/f1', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.id).toBe('f1')
expect(body.name).toBe('My Folder')
// Folder should not have downloadUrl
expect(body).not.toHaveProperty('downloadUrl')
})
it('GET /api/objects/:id returns 404 for missing object [spec: objects/detail-missing]', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', { headers })
expect(res.status).toBe(404)
})
it('PATCH /api/objects/:id renames an object [spec: objects/rename]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'f1', name: 'Old Name' })
const res = await app.request('/api/objects/f1', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'New Name' }),
})
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.name).toBe('New Name')
})
it('PATCH /api/objects/:id moves an object [spec: objects/move]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'f1', name: 'Target Folder' })
await insertFile(db, orgId, { id: 'm1', name: 'moveme.txt' })
const res = await app.request('/api/objects/m1', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: 'Target Folder' }),
})
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.parent).toBe('Target Folder')
})
it('PATCH /api/objects/:id returns 404 for missing object', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Nope' }),
})
expect(res.status).toBe(404)
})
it('POST /api/objects/:id/uploads/:sid/completions finalizes a draft to a live object [spec: objects/complete-upload]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const createRes = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'uploading.txt', type: 'text/plain', size: 100, parent: '', dirtype: 0 }),
})
expect(createRes.status).toBe(201)
const created = (await createRes.json()) as { id: string; status: string; upload: { sessionId: string } }
expect(created.status).toBe('draft')
const res = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}/completions`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parts: [{ partNumber: 1, etag: 'abc' }] }),
})
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.status).toBe('active')
expect(body.trashedAt).toBeNull()
// The live object now appears in the listing.
const list = await app.request('/api/objects', { headers })
expect(((await list.json()) as { items: unknown[] }).items).toHaveLength(1)
void db
})
it('POST .../completions rejects a single-PUT whose ETag does not match the HEAD [spec: objects/complete-etag-mismatch]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
void db
const createRes = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'mismatch.txt', type: 'text/plain', size: 100, parent: '', dirtype: 0 }),
})
const created = (await createRes.json()) as { id: string; upload: { sessionId: string } }
const res = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}/completions`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parts: [{ partNumber: 1, etag: 'wrong' }] }),
})
// ETag mismatch surfaces as an invalid upload-session state → 409.
expect(res.status).toBe(409)
const events = await db.all<{ metadata: string }>(sql`
SELECT metadata FROM audit_events
WHERE action = 'upload_failed' AND target_id = ${created.id}
`)
expect(events).toHaveLength(1)
expect(JSON.parse(events[0].metadata)).toMatchObject({
bytes: 100,
source: 'upload',
status: 'failed',
reason: 'etag_mismatch',
})
})
it('DELETE /api/objects/:id/uploads/:sid aborts the upload and discards the draft [spec: objects/abort-upload]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const createRes = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'cancel.txt', type: 'text/plain', size: 100, parent: '', dirtype: 0 }),
})
const created = (await createRes.json()) as { id: string; upload: { sessionId: string } }
const res = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}`, {
method: 'DELETE',
headers,
})
expect(res.status).toBe(204)
// Single-PUT abort best-effort deletes the S3 object and removes the draft row.
expect(S3Service.prototype.deleteObject).toHaveBeenCalled()
const check = await app.request(`/api/objects/${created.id}`, { headers })
expect(check.status).toBe(404)
const repeated = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}`, {
method: 'DELETE',
headers,
})
expect(repeated.status).toBe(204)
void db
})
it('DELETE /api/objects/:id soft-deletes a live object → 204 and moves it to trash [spec: objects/trash]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'a.txt' })
const res = await app.request('/api/objects/m1', { method: 'DELETE', headers })
expect(res.status).toBe(204)
// Gone from the live listing…
const list = await app.request('/api/objects', { headers })
expect(((await list.json()) as { items: unknown[] }).items).toHaveLength(0)
// …and present in the recycle bin with trashedAt set.
const trash = await app.request('/api/trash/objects', { headers })
const trashBody = (await trash.json()) as { items: Array<{ id: string; trashedAt: number }> }
expect(trashBody.items).toHaveLength(1)
expect(trashBody.items[0].id).toBe('m1')
expect(trashBody.items[0].trashedAt).toBeTruthy()
})
it('DELETE /api/objects/:id soft-deletes a folder, cascading its subtree to trash [spec: objects/trash-cascade]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'f1', name: 'Parent' })
await insertFile(db, orgId, { id: 'm1', name: 'child.txt', parent: 'Parent' })
await insertFolder(db, orgId, { id: 'f2', name: 'Sub', parent: 'Parent' })
await insertFile(db, orgId, { id: 'm2', name: 'deep.txt', parent: 'f2' })
const res = await app.request('/api/objects/f1', { method: 'DELETE', headers })
expect(res.status).toBe(204)
// Only the root folder shows in the trash root listing…
const trash = await app.request('/api/trash/objects', { headers })
expect(((await trash.json()) as { items: unknown[] }).items).toHaveLength(1)
// …but every descendant is flagged trashed: restore brings them all back.
const restoreRes = await app.request('/api/trash/objects/f1/restorations', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
expect(restoreRes.status).toBe(200)
const childRes = await app.request('/api/objects/m2', { headers })
expect(((await childRes.json()) as { status: string }).status).toBe('active')
})
it('POST /api/trash/objects/:id/restorations restores a trashed object [spec: objects/restore]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'a.txt', trashedAt: Date.now() })
const res = await app.request('/api/trash/objects/m1/restorations', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.status).toBe('active')
expect(body.trashedAt).toBeNull()
})
it('GET /api/trash/objects/:id returns a trashed object; 404 for a live one [spec: objects/get-trashed]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'gone', name: 'gone.txt', trashedAt: Date.now() })
await insertFile(db, orgId, { id: 'live', name: 'live.txt' })
const trashedRes = await app.request('/api/trash/objects/gone', { headers })
expect(trashedRes.status).toBe(200)
expect(((await trashedRes.json()) as { id: string }).id).toBe('gone')
const liveRes = await app.request('/api/trash/objects/live', { headers })
expect(liveRes.status).toBe(404)
})
it('GET /api/trash/objects lists trashed folder roots only [spec: objects/list-trashed]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'media', name: 'Media' })
await insertFolder(db, orgId, { id: 'music', name: 'Music', parent: 'Media' })
await insertFolder(db, orgId, { id: 'album', name: 'Album', parent: 'Media/Music' })
await insertFile(db, orgId, { id: 'track', name: 'track.flac', parent: 'Media/Music/Album' })
const trashRes = await app.request('/api/objects/album', { method: 'DELETE', headers })
expect(trashRes.status).toBe(204)
const res = await app.request('/api/trash/objects', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as { items: Array<{ id: string }> }
expect(body.items).toHaveLength(1)
expect(body.items.map((item) => item.id)).toEqual(['album'])
})
it('DELETE /api/trash/objects/:id permanently purges a trashed folder [spec: objects/purge-folder]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'f1', name: 'Delete Me', trashedAt: Date.now() })
const res = await app.request('/api/trash/objects/f1', { method: 'DELETE', headers })
expect(res.status).toBe(204)
expect(await getMatter(db, 'f1', orgId)).toBeNull()
})
it('DELETE /api/trash/objects/:id purges a trashed folder with spaces and bracketed tags', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const folderName = 'Project Hail Mary (2026) [IMAX] [1080p] [WEBRip] [5.1] [YTS.BZ]'
const trashedAt = Date.now()
await insertFolder(db, orgId, { id: 'MovieFolder', name: folderName, trashedAt })
await insertFile(db, orgId, { id: 'MovieFile', name: 'movie.mkv', parent: folderName, trashedAt })
const res = await app.request('/api/trash/objects/MovieFolder', { method: 'DELETE', headers })
expect(res.status).toBe(204)
expect(await getMatter(db, 'MovieFolder', orgId)).toBeNull()
expect(await getMatter(db, 'MovieFile', orgId)).toBeNull()
const tombstones = await db.all<{ id: string; purgedAt: number | null }>(sql`
SELECT id, purged_at AS purgedAt FROM matters
WHERE id IN ('MovieFolder', 'MovieFile')
ORDER BY id
`)
expect(tombstones).toHaveLength(2)
expect(tombstones.every((row) => typeof row.purgedAt === 'number')).toBe(true)
const ledger = await db.all<{ bytes: number }>(sql`
SELECT COALESCE(SUM(delta_bytes), 0) AS bytes
FROM storage_usage_ledger
WHERE org_id = ${orgId} AND storage_id = ${validStorage.id}
`)
expect(ledger[0].bytes).toBe(0)
})
it('DELETE /api/objects/:id is idempotent for an already-trashed object → 204', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'a.txt', trashedAt: Date.now() })
const res = await app.request('/api/objects/m1', { method: 'DELETE', headers })
expect(res.status).toBe(204)
})
it('DELETE /api/objects/:id returns 404 for missing object', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', {
method: 'DELETE',
headers,
})
expect(res.status).toBe(404)
})
it('POST /api/objects/copy copies a folder [spec: objects/copy-folder]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'target', name: 'Dest' })
await insertFolder(db, orgId, { id: 'f1', name: 'Original' })
const res = await app.request('/api/objects/f1/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: 'Dest' }),
})
expect(res.status).toBe(201)
const body = (await res.json()) as Record<string, unknown>
expect(body.name).toBe('Original')
expect(body.id).not.toBe('f1')
expect(body.status).toBe('active')
expect(body.parent).toBe('Dest')
})
it('POST /api/objects/copy returns 404 for missing source', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
expect(res.status).toBe(404)
})
it('POST .../completions returns 404 for a missing upload session', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent/uploads/NoSession/completions', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parts: [{ partNumber: 1, etag: 'abc' }] }),
})
expect(res.status).toBe(404)
})
it('POST /api/objects creates a file draft with single-PUT upload instructions [spec: objects/create-file-presign]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'photo.jpg', type: 'image/jpeg', size: 2048 }),
})
expect(res.status).toBe(201)
const body = (await res.json()) as {
status: string
object: string
upload: {
sessionId: string
partSize: number
partCount: number
mode: string
urls: string[]
parts: Array<{ url: string }>
}
}
expect(body.status).toBe('draft')
expect(body.object).toBeTruthy()
// Small upload → single PutObject: one explicit part, partSize equals the file size.
expect(body.upload.sessionId).toBeTruthy()
expect(body.upload.mode).toBe('single')
expect(body.upload.partSize).toBe(2048)
expect(body.upload.partCount).toBe(1)
expect(body.upload.urls).toEqual(['https://presigned-upload.example.com'])
expect(body.upload.parts.map((part) => part.url)).toEqual(['https://presigned-upload.example.com'])
})
it('POST /api/objects/:id/uploads/:sid/parts re-signs a single-PUT session [spec: objects/presign-upload-parts-single]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const createRes = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'resume.txt', type: 'text/plain', size: 100, parent: '', dirtype: 0 }),
})
expect(createRes.status).toBe(201)
const created = (await createRes.json()) as {
id: string
upload: { sessionId: string; mode: string; partCount: number; partSize: number }
}
expect(created.upload.mode).toBe('single')
expect(created.upload.partCount).toBe(1)
const res = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}/parts`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ partNumbers: [1] }),
})
expect(res.status).toBe(200)
const body = (await res.json()) as {
uploadId: string | null
mode: string
partSize: number
partCount: number
requiredHeaders: Record<string, string>
parts: Array<{ partNumber: number; url: string; headers: Record<string, string> }>
}
expect(body).toMatchObject({
uploadId: null,
mode: 'single',
partSize: 100,
partCount: 1,
requiredHeaders: { 'content-type': 'text/plain' },
parts: [
{ partNumber: 1, url: 'https://presigned-upload.example.com', headers: { 'content-type': 'text/plain' } },
],
})
})
it('POST /api/objects with storageId uses that exact eligible storage', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await insertStorage(db, { id: 'Stoldest' })
await insertStorage(db, { id: 'Sttarget' })
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'target.txt', type: 'text/plain', size: 1, storageId: 'Sttarget' }),
})
expect(res.status).toBe(201)
const body = (await res.json()) as { id: string; storageId: string }
expect(body.storageId).toBe('Sttarget')
const rows = await db.all<{ storageId: string }>(
sql`SELECT storage_id as storageId FROM matters WHERE id = ${body.id}`,
)
expect(rows[0].storageId).toBe('Sttarget')
})
it('POST /api/objects with ineligible storageId fails before draft/session creation', async () => {
for (const storage of [
{ id: 'missing' },
{ id: 'inactive', enabled: false },
{ id: 'full', capacity: 1, used: 1 },
]) {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
if (storage.id !== 'missing') await insertStorage(db, storage)
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: `${storage.id}.txt`, type: 'text/plain', size: 1, storageId: storage.id }),
})
expect(res.status).toBe(503)
const matters = await db.all<{ count: number }>(sql`SELECT COUNT(*) as count FROM matters`)
const sessions = await db.all<{ count: number }>(sql`SELECT COUNT(*) as count FROM object_upload_sessions`)
expect(matters[0].count).toBe(0)
expect(sessions[0].count).toBe(0)
}
})
it('POST /api/objects with storageId is admin-only and fails before draft/session creation', async () => {
const { app, db } = await createTestApp()
await adminHeaders(app)
const headers = await authedHeaders(app, 'editor@example.com')
await insertStorage(db, { id: 'Sttarget' })
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'target.txt', type: 'text/plain', size: 1, storageId: 'Sttarget' }),
})
expect(res.status).toBe(403)
const matters = await db.all<{ count: number }>(sql`SELECT COUNT(*) as count FROM matters`)
const sessions = await db.all<{ count: number }>(sql`SELECT COUNT(*) as count FROM object_upload_sessions`)
expect(matters[0].count).toBe(0)
expect(sessions[0].count).toBe(0)
})
it('POST /api/objects rejects a file larger than 5 TiB [spec: objects/create-file-too-large]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'huge.bin', type: 'application/octet-stream', size: 5 * 1024 ** 4 + 1 }),
})
expect(res.status).toBe(400)
const body = (await res.json()) as { error: { details: Array<{ reason: string }> } }
expect(body.error.details[0].reason).toBe('FILE_TOO_LARGE')
})
it('POST /api/objects/copy copies a file with S3 [spec: objects/copy-file]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'doc.txt' })
const res = await app.request('/api/objects/m1/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '' }),
})
expect(res.status).toBe(201)
expect(S3Service.prototype.copyObject).toHaveBeenCalled()
})
it('DELETE /api/trash/objects/:id permanently deletes a trashed file with S3 cleanup [spec: objects/purge-file-s3]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'file.txt', trashedAt: Date.now() })
const res = await app.request('/api/trash/objects/m1', { method: 'DELETE', headers })
expect(res.status).toBe(204)
expect(S3Service.prototype.deleteObjects).toHaveBeenCalled()
expect(await getMatter(db, 'm1', orgId)).toBeNull()
})
it('DELETE /api/trash/objects/:id purges a folder with file children from S3', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const trashedAt = Date.now()
await insertFolder(db, orgId, { id: 'f1', name: 'Folder', trashedAt })
await insertFile(db, orgId, { id: 'm1', name: 'a.txt', parent: 'Folder', trashedAt })
await insertFile(db, orgId, { id: 'm2', name: 'b.txt', parent: 'Folder', trashedAt })
const res = await app.request('/api/trash/objects/f1', { method: 'DELETE', headers })
expect(res.status).toBe(204)
expect(S3Service.prototype.deleteObjects).toHaveBeenCalled()
expect(await getMatter(db, 'f1', orgId)).toBeNull()
expect(await getMatter(db, 'm1', orgId)).toBeNull()
expect(await getMatter(db, 'm2', orgId)).toBeNull()
})
it('DELETE /api/objects/:id returns 404 for a missing object (soft-delete)', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent', { method: 'DELETE', headers })
expect(res.status).toBe(404)
})
it('DELETE /api/trash/objects/:id returns 404 for a live (non-trashed) object', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'a.txt' })
const res = await app.request('/api/trash/objects/m1', { method: 'DELETE', headers })
expect(res.status).toBe(404)
})
it('POST /api/trash/objects/:id/restorations returns 404 for a missing object', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/trash/objects/nonexistent/restorations', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
expect(res.status).toBe(404)
})
it('POST /api/trash/objects/:id/restorations on a live object is a no-op (stays active)', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'a.txt' })
const res = await app.request('/api/trash/objects/m1/restorations', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.status).toBe('active')
expect(body.trashedAt).toBeNull()
})
it('GET /api/objects/:id returns downloadUrl for files [spec: objects/download-url]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'doc.txt' })
const res = await app.request('/api/objects/m1', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.downloadUrl).toBe('https://presigned-download.example.com')
const events = await db.all<{
userId: string
actorType: string
bytes: number
source: string
trafficEventId: string
}>(sql`
SELECT
user_id AS userId,
actor_type AS actorType,
json_extract(metadata, '$.bytes') AS bytes,
json_extract(metadata, '$.source') AS source,
json_extract(metadata, '$.trafficEventId') AS trafficEventId
FROM audit_events
WHERE action = 'object_download' AND target_id = 'm1'
`)
expect(events).toEqual([
{
userId: expect.any(String),
actorType: 'user',
bytes: 100,
source: 'object_download',
trafficEventId: expect.any(String),
},
])
})
it('GET /api/objects/:id reports Cloud traffic for bound instances before returning the URL [spec: objects/download-traffic]', async () => {
const { app, db } = await createTestApp({ ZPAN_CLOUD_URL: 'https://cloud.example' })
const headers = await authedHeaders(app)
await insertStorage(db, { metered: true })
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'doc.txt' })
await seedBusinessLicense(db)
vi.stubGlobal(
'fetch',
vi.fn(async (_url, init) => {
const body = JSON.parse(String(init?.body)) as { eventId: string }
return {
ok: true,
status: 200,
json: async () => ({ data: { accepted: true, duplicate: false, eventId: body.eventId } }),
} as Response
}),
)
const res = await app.request('/api/objects/m1', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.downloadUrl).toBe('https://presigned-download.example.com')
expect(fetch).toHaveBeenCalledTimes(1)
await expect(
db.select().from(cloudTrafficReports).where(eq(cloudTrafficReports.orgId, orgId)),
).resolves.toMatchObject([{ orgId, source: 'object_download', sourceId: 'm1', bytes: 100, status: 'reported' }])
})
})
describe('Matter service', () => {
it('createMatter applies defaults for optional fields', async () => {
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
VALUES ('s1', 'b', 'https://s3.example.com', 'us-east-1', 'k', 's', '$UID/$RAW_NAME', '', 0, 0, 'active', ${now}, ${now})
`)
const matter = await createMatter(db, {
orgId: 'Org1',
name: 'test.txt',
type: 'text/plain',
object: 'key.txt',
storageId: 's1',
status: 'draft',
})
expect(matter.size).toBe(0)
expect(matter.dirtype).toBe(0)
expect(matter.parent).toBe('')
expect(matter.id).toBeTruthy()
expect(matter.alias).toBeTruthy()
})
it('createMatter uses provided optional fields', async () => {
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
VALUES ('s1', 'b', 'https://s3.example.com', 'us-east-1', 'k', 's', '$UID/$RAW_NAME', '', 0, 0, 'active', ${now}, ${now})
`)
const matter = await createMatter(db, {
orgId: 'Org1',
name: 'doc.pdf',
type: 'application/pdf',
size: 1024,
dirtype: 1,
parent: 'folder-1',
object: '',
storageId: 's1',
status: 'active',
})
expect(matter.size).toBe(1024)
expect(matter.dirtype).toBe(1)
expect(matter.parent).toBe('folder-1')
})
it('listMatters returns paginated results', async () => {
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
VALUES ('s1', 'b', 'https://s3.example.com', 'us-east-1', 'k', 's', '$UID/$RAW_NAME', '', 0, 0, 'active', ${now}, ${now})
`)
await createMatter(db, {
orgId: 'Org1',
name: 'a.txt',
type: 'text/plain',
object: 'a',
storageId: 's1',
status: 'active',
})
await createMatter(db, {
orgId: 'Org1',
name: 'b.txt',
type: 'text/plain',
object: 'b',
storageId: 's1',
status: 'active',
})
const page1 = await listMatters(db, 'Org1', { parent: '', pageSize: 1 })
expect(page1.items).toHaveLength(1)
expect(page1.nextBoundary).not.toBeNull()
const page2 = await listMatters(db, 'Org1', {
parent: '',
pageSize: 1,
after: page1.nextBoundary ?? undefined,
})
expect(page2.items).toHaveLength(1)
})
it('getMatter returns null for missing record', async () => {
const { db } = await createTestApp()
const result = await getMatter(db, 'nonexistent', 'Org1')
expect(result).toBeNull()
})
it('updateMatter returns null for missing record', async () => {
const { db } = await createTestApp()
const result = await updateMatter(db, 'nonexistent', 'Org1', { name: 'new' })
expect(result).toBeNull()
})
it('confirmUpload returns null for missing record', async () => {
const { db } = await createTestApp()
const { matter } = await confirmUpload(db, 'nonexistent', 'Org1')
expect(matter).toBeNull()
})
it('confirmUpload returns null for non-draft status', async () => {
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
VALUES ('s1', 'b', 'https://s3.example.com', 'us-east-1', 'k', 's', '$UID/$RAW_NAME', '', 0, 0, 'active', ${now}, ${now})
`)
const matter = await createMatter(db, {
orgId: 'Org1',
name: 'a.txt',
type: 'text/plain',
object: 'a',
storageId: 's1',
status: 'active',
})
const { matter: result } = await confirmUpload(db, matter.id, 'Org1')
expect(result).toBeNull()
})
it('deleteMatter hides and retains the purged record', async () => {
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
VALUES ('s1', 'b', 'https://s3.example.com', 'us-east-1', 'k', 's', '$UID/$RAW_NAME', '', 0, 0, 'active', ${now}, ${now})
`)
const matter = await createMatter(db, {
orgId: 'Org1',
name: 'a.txt',
type: 'text/plain',
object: 'a',
storageId: 's1',
status: 'active',
})
const deleted = await deleteMatter(db, matter.id, 'Org1')
expect(deleted).not.toBeNull()
expect(deleted!.id).toBe(matter.id)
const check = await getMatter(db, matter.id, 'Org1')
expect(check).toBeNull()
const retained = await db.all<{ purgedAt: number | null }>(
sql`SELECT purged_at AS purgedAt FROM matters WHERE id = ${matter.id}`,
)
expect(retained[0]?.purgedAt).toEqual(expect.any(Number))
})
it('deleteMatter returns null for missing record', async () => {
const { db } = await createTestApp()
const result = await deleteMatter(db, 'nonexistent', 'Org1')
expect(result).toBeNull()
})
it('copyMatter creates a new record from source', async () => {
const { db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
VALUES ('s1', 'b', 'https://s3.example.com', 'us-east-1', 'k', 's', '$UID/$RAW_NAME', '', 0, 0, 'active', ${now}, ${now})
`)
const source = await createMatter(db, {
orgId: 'Org1',
name: 'a.txt',
type: 'text/plain',
size: 42,
object: 'original/key',
storageId: 's1',
status: 'active',
})
const copy = await copyMatter(db, source, 'target-folder', 'copy/key')
expect(copy.id).not.toBe(source.id)
expect(copy.alias).not.toBe(source.alias)
expect(copy.name).toBe('a.txt')
expect(copy.size).toBe(42)
expect(copy.parent).toBe('target-folder')
expect(copy.object).toBe('copy/key')
expect(copy.status).toBe('active')
})
it('getMatters returns empty array for empty ids list', async () => {
const { db } = await createTestApp()
const result = await getMatters(db, 'Org1', [])
expect(result).toEqual([])
})
})
// ─── Name-conflict route layer ────────────────────────────────────────────────
describe('Objects API — name conflict (409 responses)', () => {
it('POST /api/objects returns 409 with NAME_CONFLICT code when folder name is already taken [spec: objects/create-conflict]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'FExist', name: 'Duplicates' })
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Duplicates', type: 'folder', dirtype: 1 }),
})
expect(res.status).toBe(409)
const body = (await res.json()) as {
error: { details: Array<{ reason: string; metadata: Record<string, string> }> }
}
expect(body.error.details[0].reason).toBe('NAME_CONFLICT')
expect(body.error.details[0].metadata.conflictingName).toBe('Duplicates')
expect(typeof body.error.details[0].metadata.conflictingId).toBe('string')
})
it('POST /api/objects with onConflict: rename succeeds and returns auto-renamed folder [spec: objects/create-conflict-rename]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFolder(db, orgId, { id: 'FExist2', name: 'Reports' })
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Reports', type: 'folder', dirtype: 1, onConflict: 'rename' }),
})
expect(res.status).toBe(201)
const body = (await res.json()) as Record<string, unknown>
expect(body.name).toBe('Reports (1)')
})
it('PATCH /api/objects/:id rename conflict returns 409 with NAME_CONFLICT code [spec: objects/rename-conflict]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'alpha.txt' })
await insertFile(db, orgId, { id: 'm2', name: 'beta.txt' })
const res = await app.request('/api/objects/m1', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'beta.txt' }),
})
expect(res.status).toBe(409)
const body = (await res.json()) as {
error: { details: Array<{ reason: string; metadata: Record<string, string> }> }
}
expect(body.error.details[0].reason).toBe('NAME_CONFLICT')
expect(body.error.details[0].metadata.conflictingName).toBe('beta.txt')
})
it('PATCH /api/objects/:id rename with onConflict: rename succeeds', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'alpha.txt' })
await insertFile(db, orgId, { id: 'm2', name: 'beta.txt' })
const res = await app.request('/api/objects/m1', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'beta.txt', onConflict: 'rename' }),
})
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.name).toBe('beta (1).txt')
})
it('PATCH /api/objects/:id move with collision and no onConflict returns 409 [spec: objects/move-conflict]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'file.txt' })
await insertFile(db, orgId, { id: 'm2', name: 'file.txt', parent: 'Dest' })
const res = await app.request('/api/objects/m1', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: 'Dest' }),
})
expect(res.status).toBe(409)
const body = (await res.json()) as { error: { details: Array<{ reason: string }> } }
expect(body.error.details[0].reason).toBe('NAME_CONFLICT')
})
it('PATCH /api/objects/:id move with onConflict: rename resolves collision', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'm1', name: 'file.txt' })
await insertFile(db, orgId, { id: 'm2', name: 'file.txt', parent: 'Dest' })
const res = await app.request('/api/objects/m1', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: 'Dest', onConflict: 'rename' }),
})
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.name).toBe('file (1).txt')
expect(body.parent).toBe('Dest')
})
it('POST .../completions returns 409 when an active sibling appeared during upload', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
// Start a real upload (creates the draft + session)…
const createRes = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'upload.txt', type: 'text/plain', size: 100, parent: '', dirtype: 0 }),
})
const created = (await createRes.json()) as { id: string; upload: { sessionId: string } }
// …then a same-named active sibling lands before completion.
await insertFile(db, orgId, { id: 'active1', name: 'upload.txt', status: 'active' })
const res = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}/completions`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parts: [{ partNumber: 1, etag: 'abc' }] }),
})
expect(res.status).toBe(409)
const body = (await res.json()) as { error: { details: Array<{ reason: string }> } }
expect(body.error.details[0].reason).toBe('NAME_CONFLICT')
})
it('POST /api/trash/objects/:id/restorations returns 409 when restore name is already taken [spec: objects/restore-conflict]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'trashed1', name: 'note.txt', trashedAt: Date.now() })
await insertFile(db, orgId, { id: 'active2', name: 'note.txt', status: 'active' })
const res = await app.request('/api/trash/objects/trashed1/restorations', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
expect(res.status).toBe(409)
const body = (await res.json()) as { error: { details: Array<{ reason: string }> } }
expect(body.error.details[0].reason).toBe('NAME_CONFLICT')
})
it('POST /api/trash/objects/:id/restorations with onConflict: rename restores with suffix', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'trashed2', name: 'note.txt', trashedAt: Date.now() })
await insertFile(db, orgId, { id: 'active3', name: 'note.txt', status: 'active' })
const res = await app.request('/api/trash/objects/trashed2/restorations', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ onConflict: 'rename' }),
})
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.name).toBe('note (1).txt')
expect(body.status).toBe('active')
})
it('POST /api/objects/copy returns 409 when onConflict: fail and target has same name', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'src1', name: 'doc.txt' })
await insertFile(db, orgId, { id: 'dst1', name: 'doc.txt', parent: 'Dest' })
const res = await app.request('/api/objects/src1/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: 'Dest', onConflict: 'fail' }),
})
expect(res.status).toBe(409)
const body = (await res.json()) as { error: { details: Array<{ reason: string }> } }
expect(body.error.details[0].reason).toBe('NAME_CONFLICT')
})
it('POST /api/objects/copy auto-renames by default when target has same name', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'src2', name: 'photo.jpg' })
await insertFile(db, orgId, { id: 'dst2', name: 'photo.jpg', parent: 'Dest' })
const res = await app.request('/api/objects/src2/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: 'Dest' }),
})
expect(res.status).toBe(201)
const body = (await res.json()) as Record<string, unknown>
expect(body.name).toBe('photo (1).jpg')
})
})
// ─── Cross-space transfer ─────────────────────────────────────────────────────
type TestDb = Awaited<ReturnType<typeof createTestApp>>['db']
type TestApp = Awaited<ReturnType<typeof createTestApp>>['app']
type TestAuth = Awaited<ReturnType<typeof createTestApp>>['auth']
async function createWorkspaceApiKey(
auth: TestAuth,
orgId: string,
userId: string,
permissions: Record<string, string[]>,
): Promise<string> {
// biome-ignore lint/suspicious/noExplicitAny: better-auth plugin API is not fully typed
const result = (await (auth.api as any).createApiKey({
body: {
configId: 'ihost',
organizationId: orgId,
userId,
permissions,
},
})) as { key: string }
return result.key
}
async function getUserIdByEmail(db: TestDb, email: string): Promise<string> {
const rows = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = ${email}`)
return rows[0].id
}
async function insertTeamOrg(db: TestDb, id: string): Promise<void> {
await db.run(sql`
INSERT INTO organization (id, name, slug, metadata)
VALUES (${id}, ${`Team ${id}`}, ${id}, '{"type":"team"}')
`)
await db.run(sql`
INSERT INTO org_quotas (id, org_id, quota, used, traffic_quota, traffic_used, traffic_period)
VALUES (${`quota-${id}`}, ${id}, 0, 0, 0, 0, '1970-01')
`)
}
async function insertMember(db: TestDb, orgId: string, userId: string, role: string): Promise<void> {
await db.run(sql`
INSERT INTO member (id, organization_id, user_id, role)
VALUES (${`member-${orgId}-${userId}`}, ${orgId}, ${userId}, ${role})
`)
}
async function insertStorageEntitlement(db: TestDb, orgId: string, bytes: number): Promise<void> {
const now = Date.now()
await db.run(sql`
INSERT INTO org_quota_entitlements
(id, org_id, resource_type, entitlement_type, source, source_id, bytes, starts_at, status, created_at, updated_at)
VALUES
(${`ent-${orgId}`}, ${orgId}, 'storage', 'grant', 'test', ${`test-${orgId}`}, ${bytes}, ${now}, 'active', ${now}, ${now})
`)
}
function transferRequest(
app: TestApp,
headers: Record<string, string>,
id: string,
body: { targetOrgId: string; targetParent?: string; mode: 'copy' | 'move' },
) {
return app.request(`/api/objects/${id}/transfers`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
describe('POST /api/objects/:id/transfers', () => {
it('allows a workspace API key with object scopes to list, read, and create objects', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
await insertFile(db, orgId, { id: 'ApiKeyRead', name: 'readme.txt' })
const key = await createWorkspaceApiKey(auth, orgId, userId, { objects: ['read', 'create'] })
const headers = { Authorization: `Bearer ${key}` }
const list = await app.request('/api/objects', { headers })
expect(list.status).toBe(200)
const listBody = (await list.json()) as { items: Array<{ id: string }> }
expect(listBody.items.map((item) => item.id)).toContain('ApiKeyRead')
const read = await app.request('/api/objects/ApiKeyRead', { headers })
expect(read.status).toBe(200)
const readBody = (await read.json()) as { id: string; name: string }
expect(readBody).toMatchObject({ id: 'ApiKeyRead', name: 'readme.txt' })
const create = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Api Folder', type: 'folder', dirtype: 1, parent: '' }),
})
expect(create.status).toBe(201)
const createBody = (await create.json()) as { name: string; dirtype: number }
expect(createBody).toMatchObject({ name: 'Api Folder', dirtype: 1 })
})
it('returns 403 when a workspace API key is missing the required object scope', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
const orgId = await getOrgId(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
const key = await createWorkspaceApiKey(auth, orgId, userId, { objects: ['create'] })
const res = await app.request('/api/objects', {
headers: { Authorization: `Bearer ${key}` },
})
expect(res.status).toBe(403)
})
it('rejects orgId override for a fixed-workspace API key', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
await insertTeamOrg(db, 'Teamapioverride')
await insertMember(db, 'Teamapioverride', userId, 'viewer')
await insertFile(db, 'Teamapioverride', { id: 'Teamapifile', name: 'team.txt' })
const key = await createWorkspaceApiKey(auth, orgId, userId, { objects: ['read'] })
const res = await app.request('/api/objects?orgId=Teamapioverride', {
headers: { Authorization: `Bearer ${key}` },
})
expect(res.status).toBe(403)
})
it('rejects cross-space transfer for a fixed-workspace API key', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
await insertTeamOrg(db, 'Teamapitransfer')
await insertMember(db, 'Teamapitransfer', userId, 'editor')
await insertStorageEntitlement(db, 'Teamapitransfer', 10_000_000)
await insertFile(db, orgId, { id: 'Srcapitransfer', name: 'doc.txt' })
const key = await createWorkspaceApiKey(auth, orgId, userId, { objects: ['update'] })
const res = await transferRequest(app, { Authorization: `Bearer ${key}` }, 'Srcapitransfer', {
targetOrgId: 'Teamapitransfer',
mode: 'copy',
})
expect(res.status).toBe(403)
})
it('copies a file into a team space the user can edit [spec: objects/transfer-copy]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
await insertTeamOrg(db, 'Teama')
await insertMember(db, 'Teama', userId, 'editor')
await insertStorageEntitlement(db, 'Teama', 10_000_000)
await insertFile(db, orgId, { id: 'Srccopy', name: 'doc.txt' })
const res = await transferRequest(app, headers, 'Srccopy', { targetOrgId: 'Teama', mode: 'copy' })
expect(res.status).toBe(201)
const body = (await res.json()) as { saved: Array<{ orgId: string; name: string }>; sourceDeleted: boolean }
expect(body.saved).toHaveLength(1)
expect(body.saved[0].orgId).toBe('Teama')
expect(body.sourceDeleted).toBe(false)
expect(S3Service.prototype.copyObject).toHaveBeenCalled()
const source = await getMatter(db, 'Srccopy', orgId)
expect(source?.status).toBe('active')
})
it('moves a file into a team space, deleting the source and releasing its quota [spec: objects/transfer-move]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
await insertTeamOrg(db, 'Teamb')
await insertMember(db, 'Teamb', userId, 'owner')
await insertStorageEntitlement(db, 'Teamb', 10_000_000)
await insertFile(db, orgId, { id: 'Srcmove', name: 'photo.jpg', size: 1024 })
await db.run(sql`
UPDATE org_quotas
SET quota = ${1024 * 1024}, used = 1024, traffic_quota = 0, traffic_used = 0, traffic_period = '1970-01'
WHERE org_id = ${orgId}
`)
const res = await transferRequest(app, headers, 'Srcmove', { targetOrgId: 'Teamb', mode: 'move' })
expect(res.status).toBe(201)
const body = (await res.json()) as { saved: Array<{ orgId: string }>; sourceDeleted: boolean }
expect(body.sourceDeleted).toBe(true)
// Source is purged, not trashed — its quota must be released, not double-counted.
const source = await getMatter(db, 'Srcmove', orgId)
expect(source).toBeNull()
expect((await getOrgQuota(db, orgId))?.used ?? 0).toBe(0)
const targetList = await listMatters(db, 'Teamb', { parent: '', pageSize: 10 })
expect(targetList.items.map((m) => m.name)).toContain('photo.jpg')
})
it('copies a folder recursively into the target space [spec: objects/transfer-folder]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
await insertTeamOrg(db, 'Teamc')
await insertMember(db, 'Teamc', userId, 'editor')
await insertStorageEntitlement(db, 'Teamc', 10_000_000)
await insertFolder(db, orgId, { id: 'Fold1', name: 'Album' })
await insertFile(db, orgId, { id: 'InFold', name: 'pic.png', parent: 'Album' })
const res = await transferRequest(app, headers, 'Fold1', { targetOrgId: 'Teamc', mode: 'copy' })
expect(res.status).toBe(201)
const body = (await res.json()) as { saved: Array<{ name: string }> }
expect(body.saved.map((m) => m.name)).toEqual(expect.arrayContaining(['Album', 'pic.png']))
})
it('rejects transfer into a team the user is not a member of [spec: objects/transfer-permission]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertTeamOrg(db, 'Teamstrange')
await insertFile(db, orgId, { id: 'Src403', name: 'doc.txt' })
const res = await transferRequest(app, headers, 'Src403', { targetOrgId: 'Teamstrange', mode: 'copy' })
expect(res.status).toBe(403)
})
it("rejects transfer into another user's personal space", async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await authedHeaders(app, 'victim@example.com')
await insertStorage(db)
const orgId = await getOrgId(db)
const victimId = await getUserIdByEmail(db, 'victim@example.com')
const victimOrgs = await db.all<{ id: string }>(sql`
SELECT o.id
FROM organization o
INNER JOIN member m ON m.organization_id = o.id
WHERE m.user_id = ${victimId}
AND (o.slug LIKE 'personal-%' OR COALESCE(o.metadata, '') LIKE '%"type":"personal"%')
LIMIT 1
`)
await insertFile(db, orgId, { id: 'Srcvictim', name: 'doc.txt' })
const res = await transferRequest(app, headers, 'Srcvictim', { targetOrgId: victimOrgs[0].id, mode: 'copy' })
expect(res.status).toBe(403)
})
it('rejects transfer when the target space quota is exceeded [spec: objects/transfer-quota]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
await insertTeamOrg(db, 'Teamsmall')
await insertMember(db, 'Teamsmall', userId, 'editor')
await insertStorageEntitlement(db, 'Teamsmall', 10)
await insertFile(db, orgId, { id: 'Srcbig', name: 'big.bin' })
const res = await transferRequest(app, headers, 'Srcbig', { targetOrgId: 'Teamsmall', mode: 'copy' })
expect(res.status).toBe(422)
const body = (await res.json()) as { error: { details: Array<{ reason: string }> } }
expect(body.error.details[0].reason).toBe('QUOTA_EXCEEDED')
})
it('rejects transfer to the same space [spec: objects/transfer-same-space]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'Srcsame', name: 'doc.txt' })
const res = await transferRequest(app, headers, 'Srcsame', { targetOrgId: orgId, mode: 'copy' })
expect(res.status).toBe(400)
})
})
// ─── Quota enforcement ────────────────────────────────────────────────────────
// Scoped helpers below shadow the top-level fixtures (distinct storage id and a
// quota-focused insertStorage/insertFile signature) so they stay isolated.
describe('Objects API — quota enforcement', () => {
const validStorage = {
id: 'Stquota',
bucket: 'test-bucket',
endpoint: 'https://s3.amazonaws.com',
region: 'us-east-1',
accessKey: 'AKIAIOSFODNN7EXAMPLE',
secretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
}
async function insertStorage(db: Awaited<ReturnType<typeof createTestApp>>['db'], used = 0) {
const now = Date.now()
await db.run(sql`
INSERT INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
VALUES (${validStorage.id}, ${validStorage.bucket},
${validStorage.endpoint}, ${validStorage.region}, ${validStorage.accessKey},
${validStorage.secretKey}, '', '', 0, ${used}, 'active', ${now}, ${now})
`)
}
async function insertFile(
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
opts: { id: string; name: string; size?: number; status?: string; trashedAt?: number },
) {
const now = Date.now()
const size = opts.size ?? 100
const status = opts.status ?? 'active'
await db.run(sql`
INSERT INTO matters (id, org_id, alias, name, type, size, dirtype, parent, object, storage_id, status, trashed_at, created_at, updated_at)
VALUES (${opts.id}, ${orgId}, ${`${opts.id}-alias`}, ${opts.name}, 'text/plain', ${size}, 0, '',
'some/key.txt', ${validStorage.id}, ${status}, ${opts.trashedAt ?? null}, ${now}, ${now})
`)
}
async function getOrgId(db: Awaited<ReturnType<typeof createTestApp>>['db']): Promise<string> {
const rows = await db.all<{ id: string }>(sql`
SELECT id FROM organization WHERE metadata LIKE '%"type":"personal"%' LIMIT 1
`)
return rows[0].id
}
async function setOrgQuota(
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
quota: number,
used = 0,
) {
const existing = await db.select().from(orgQuotas).where(eq(orgQuotas.orgId, orgId))
if (existing.length > 0) {
await db.update(orgQuotas).set({ quota, used }).where(eq(orgQuotas.orgId, orgId))
} else {
await db.insert(orgQuotas).values({
id: nanoid(),
orgId,
quota,
used,
trafficQuota: 0,
trafficUsed: 0,
trafficPeriod: currentTrafficPeriod(),
})
}
const now = Date.now()
await db.run(sql`
UPDATE org_quota_entitlements
SET status = 'revoked', updated_at = ${now}
WHERE org_id = ${orgId}
AND resource_type = 'storage'
AND entitlement_type = 'plan'
AND status = 'active'
`)
if (quota > 0) {
await db.run(sql`
INSERT INTO org_quota_entitlements
(id, org_id, resource_type, entitlement_type, source, source_id, bytes, starts_at, expires_at, status, metadata, created_at, updated_at)
VALUES
(${nanoid()}, ${orgId}, 'storage', 'plan', 'test', ${`test-storage-plan:${orgId}:${nanoid()}`}, ${quota}, ${now}, NULL, 'active', '{"packageName":"Test Plan"}', ${now}, ${now})
`)
}
}
async function addStorageEntitlement(
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
bytes: number,
) {
const now = new Date()
await db.insert(orgQuotaEntitlements).values({
id: nanoid(),
orgId,
resourceType: 'storage',
source: 'test',
sourceId: nanoid(),
bytes,
startsAt: now,
expiresAt: null,
status: 'active',
metadata: null,
createdAt: now,
updatedAt: now,
})
}
function stubCapacityStoreWithoutOffers() {
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
const url = String(input instanceof Request ? input.url : input)
if (url.includes('/products')) {
return Response.json({ items: [], total: 0, limit: 100, offset: 0 })
}
if (url.endsWith('/publication')) {
return Response.json({
storeId: 'store-test-binding',
mode: 'directory',
listingStatus: 'listed',
displayName: 'ZPan',
summary: null,
publicMetadata: {},
skillUrl: null,
termsUrl: null,
healthUrl: 'https://files.example/api/health',
healthStatus: 'healthy',
resources: [],
createdAt: '2026-07-30T00:00:00.000Z',
updatedAt: '2026-07-30T00:00:00.000Z',
})
}
throw new Error(`Unexpected Cloud request: ${url}`)
}),
)
}
function stubCapacityStoreWithOffer() {
const product = {
id: 'capacity-pro',
storeId: 'store-test-binding',
type: 'store_item',
name: 'Pro',
description: 'More storage',
metadata: { deliverable: { type: 'zpan.plan', storageBytes: 4096, includedCredits: 0 } },
prices: [
{
id: 'price-monthly',
currency: 'usd',
amount: 200,
recurring: { interval: 'month', intervalCount: 1 },
},
],
active: true,
sortOrder: 2,
createdAt: '2026-07-30T00:00:00.000Z',
updatedAt: '2026-07-30T00:00:00.000Z',
}
const resource = {
id: 'publication-resource-pro',
storeId: 'store-test-binding',
resourceId: 'capacity-pro:price-monthly',
offerId: 'pro-monthly',
title: 'Pro',
description: 'More storage',
productId: product.id,
priceId: product.prices[0].id,
postResourceUrl: 'https://files.example/api/store/capacity-purchases/capacity-pro%3Aprice-monthly',
status: 'active',
tags: [],
capabilities: ['storage.capacity.purchase'],
publicDeliverable: { type: 'zpan.plan', storageBytes: 4096 },
productSnapshot: null,
bazaarRequestMethod: 'POST',
bazaarBodyType: 'json',
bazaarInput: null,
bazaarInputSchema: null,
bazaarOutput: null,
bazaarValidationStatus: 'unknown',
bazaarValidationDiagnostic: null,
bazaarValidatedAt: null,
createdAt: '2026-07-30T00:00:00.000Z',
updatedAt: '2026-07-30T00:00:00.000Z',
}
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
const url = String(input instanceof Request ? input.url : input)
if (url.includes('/products')) {
return Response.json({ items: [product], total: 1, limit: 100, offset: 0 })
}
if (url.endsWith('/publication')) {
return Response.json({
storeId: 'store-test-binding',
mode: 'directory',
listingStatus: 'listed',
displayName: 'ZPan',
summary: null,
publicMetadata: {},
skillUrl: null,
termsUrl: null,
healthUrl: 'https://files.example/api/health',
healthStatus: 'healthy',
resources: [resource],
createdAt: '2026-07-30T00:00:00.000Z',
updatedAt: '2026-07-30T00:00:00.000Z',
})
}
throw new Error(`Unexpected Cloud request: ${url}`)
}),
)
}
it('surfaces capacity-offer lookup failures instead of reporting quota exhaustion', async () => {
const { app, db } = await createTestApp({ ZPAN_CLOUD_URL: 'https://cloud.example' })
await seedBusinessLicense(db)
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 100, 90)
vi.stubGlobal(
'fetch',
vi.fn(async () => Response.json({ error: { code: 'cloud_unavailable' } }, { status: 503 })),
)
const res = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
dirtype: 0,
name: 'toobig.txt',
parent: '',
size: 50,
type: 'text/plain',
}),
})
expect(res.status).toBe(502)
await expect(res.json()).resolves.toMatchObject({
error: {
message: 'cloud_unavailable',
details: [{ reason: 'UNAVAILABLE' }],
},
})
})
// ─── POST /api/objects/copy — quota enforcement ──────────────────────────
describe('POST /api/objects/copy — quota enforcement', () => {
it('returns 422 when copying a file would exceed quota', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
// quota = 500, used = 450, file size = 100 → copy would exceed
await setOrgQuota(db, orgId, 500, 450)
await insertFile(db, orgId, { id: 'Mcopyover', name: 'big.txt', size: 100 })
const res = await app.request('/api/objects/Mcopyover/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '' }),
})
expect(res.status).toBe(422)
const body = (await res.json()) as { error: { message: string; details: Array<{ reason: string }> } }
expect(body.error.message).toBe('Quota exceeded')
expect(body.error.details[0].reason).toBe('QUOTA_EXCEEDED')
})
it('returns 201 and increments orgQuotas.used when copy succeeds within quota', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
// quota = 1000, used = 100, file size = 100 → copy is fine
await setOrgQuota(db, orgId, 1000, 100)
await insertFile(db, orgId, { id: 'Mcopyok', name: 'doc.txt', size: 100 })
const res = await app.request('/api/objects/Mcopyok/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '' }),
})
expect(res.status).toBe(201)
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(quotaRows[0].used).toBe(200)
})
it('returns 201 and increments storages.used when copy succeeds', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 50)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 10000, 50)
await insertFile(db, orgId, { id: 'Mcopyst', name: 'img.png', size: 150 })
await app.request('/api/objects/Mcopyst/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '' }),
})
const storageRows = await db.all<{ used: number }>(sql`SELECT used FROM storages WHERE id = ${validStorage.id}`)
expect(storageRows[0].used).toBe(200)
})
it('rolls back usage when S3 copy fails after quota reservation', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 100)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 1000, 100)
await insertFile(db, orgId, { id: 'Mcopys3fail', name: 'fail.txt', size: 200 })
vi.mocked(S3Service.prototype.copyObject).mockRejectedValueOnce(new Error('copy failed'))
const res = await app.request('/api/objects/Mcopys3fail/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: 'Archive' }),
})
expect(res.status).toBe(500)
const storageRows = await db.all<{ used: number }>(sql`SELECT used FROM storages WHERE id = ${validStorage.id}`)
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(storageRows[0].used).toBe(100)
expect(quotaRows[0].used).toBe(100)
})
it('rolls back usage when copy fails on name conflict after quota reservation', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 100)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 1000, 100)
await insertFile(db, orgId, { id: 'Mcopyconflict', name: 'conflict.txt', size: 200 })
const res = await app.request('/api/objects/Mcopyconflict/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '', onConflict: 'fail' }),
})
expect(res.status).toBe(409)
const storageRows = await db.all<{ used: number }>(sql`SELECT used FROM storages WHERE id = ${validStorage.id}`)
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(storageRows[0].used).toBe(100)
expect(quotaRows[0].used).toBe(100)
})
it('returns 201 without incrementing usage when copying a zero-size file', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
// quota is fully consumed, but zero-size should still pass
await setOrgQuota(db, orgId, 500, 500)
const now = Date.now()
await db.run(sql`
INSERT INTO matters (id, org_id, alias, name, type, size, dirtype, parent, object, storage_id, status, created_at, updated_at)
VALUES ('Mzero', ${orgId}, 'Mzeroalias', 'empty.txt', 'text/plain', 0, 0, '', '',
${validStorage.id}, 'active', ${now}, ${now})
`)
const res = await app.request('/api/objects/Mzero/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '' }),
})
expect(res.status).toBe(201)
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(quotaRows[0].used).toBe(500) // unchanged
})
it('returns 422 when no quota row or entitlement exists', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await db.delete(orgQuotaEntitlements).where(eq(orgQuotaEntitlements.orgId, orgId))
await db.delete(orgQuotas).where(eq(orgQuotas.orgId, orgId))
await insertFile(db, orgId, { id: 'Mcopynolimit', name: 'nolimit.txt', size: 100 })
const res = await app.request('/api/objects/Mcopynolimit/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '' }),
})
expect(res.status).toBe(422)
})
it('returns 422 when effective quota is zero', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 0, 99999)
await insertFile(db, orgId, { id: 'Mcopyqlimit', name: 'large.bin', size: 1000000 })
const res = await app.request('/api/objects/Mcopyqlimit/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '' }),
})
expect(res.status).toBe(422)
})
it('returns 404 when source file does not exist', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/objects/nonexistent/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '' }),
})
expect(res.status).toBe(404)
})
})
describe('trash and purge — storage usage accounting', () => {
it('does not change usage when an active file is moved to trash', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 300)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 1000, 300)
await insertFile(db, orgId, { id: 'Mtrashusage', name: 'keep-accounted.txt', size: 300 })
// Soft delete keeps trashed bytes counted (they still occupy storage).
const res = await app.request('/api/objects/Mtrashusage', { method: 'DELETE', headers })
expect(res.status).toBe(204)
const storageRows = await db.all<{ used: number }>(sql`SELECT used FROM storages WHERE id = ${validStorage.id}`)
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(storageRows[0].used).toBe(300)
expect(quotaRows[0].used).toBe(300)
})
it('purging a trashed root releases only its bytes and keeps active file usage', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 500)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 1000, 500)
await insertFile(db, orgId, { id: 'Mactiveafterempty', name: 'active.txt', size: 300 })
await insertFile(db, orgId, { id: 'Mtrashedempty', name: 'trashed.txt', size: 200, trashedAt: Date.now() })
const res = await app.request('/api/trash/objects/Mtrashedempty', { method: 'DELETE', headers })
expect(res.status).toBe(204)
const storageRows = await db.all<{ used: number }>(sql`SELECT used FROM storages WHERE id = ${validStorage.id}`)
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(storageRows[0].used).toBe(300)
expect(quotaRows[0].used).toBe(300)
})
it('purging a trashed root recalculates usage when counters had drifted below active file bytes', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 200)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 1000, 200)
await insertFile(db, orgId, { id: 'Mactivedrift', name: 'active.txt', size: 300 })
await insertFile(db, orgId, { id: 'Mtrasheddrift', name: 'trashed.txt', size: 200, trashedAt: Date.now() })
const res = await app.request('/api/trash/objects/Mtrasheddrift', { method: 'DELETE', headers })
expect(res.status).toBe(204)
const storageRows = await db.all<{ used: number }>(sql`SELECT used FROM storages WHERE id = ${validStorage.id}`)
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(storageRows[0].used).toBe(300)
expect(quotaRows[0].used).toBe(300)
})
})
describe('GET /api/objects/:id — traffic quota enforcement', () => {
it('returns download URL and consumes traffic quota when allowed', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'Mdownloadok', name: 'download.txt', size: 100 })
const trafficPeriod = currentTrafficPeriod()
await db.run(sql`
UPDATE org_quotas
SET traffic_quota = 500, traffic_used = 25, traffic_period = ${trafficPeriod}
WHERE org_id = ${orgId}
`)
const res = await app.request('/api/objects/Mdownloadok', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.downloadUrl).toBe('https://presigned-download.example.com')
const rows = await db.all<{ trafficUsed: number }>(
sql`SELECT traffic_used AS trafficUsed FROM org_quotas WHERE org_id = ${orgId}`,
)
expect(rows[0].trafficUsed).toBe(125)
})
it('resets stale monthly traffic period before consuming traffic', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'Mdownloadreset', name: 'download.txt', size: 100 })
const trafficPeriod = currentTrafficPeriod()
await db.run(sql`
UPDATE org_quotas
SET traffic_quota = 500, traffic_used = 500, traffic_period = '1970-01'
WHERE org_id = ${orgId}
`)
const res = await app.request('/api/objects/Mdownloadreset', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.downloadUrl).toBe('https://presigned-download.example.com')
const rows = await db.all<{ trafficUsed: number; trafficPeriod: string }>(
sql`SELECT traffic_used AS trafficUsed, traffic_period AS trafficPeriod FROM org_quotas WHERE org_id = ${orgId}`,
)
expect(rows[0]).toEqual({ trafficUsed: 100, trafficPeriod })
})
it('refunds traffic when download URL signing fails', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'Mdownloadsignfail', name: 'download.txt', size: 100 })
const trafficPeriod = currentTrafficPeriod()
await db.run(sql`
UPDATE org_quotas
SET traffic_quota = 500, traffic_used = 25, traffic_period = ${trafficPeriod}
WHERE org_id = ${orgId}
`)
vi.mocked(S3Service.prototype.presignDownload).mockRejectedValueOnce(new Error('sign failed'))
const res = await app.request('/api/objects/Mdownloadsignfail', { headers })
expect(res.status).toBe(500)
const rows = await db.all<{ trafficUsed: number }>(
sql`SELECT traffic_used AS trafficUsed FROM org_quotas WHERE org_id = ${orgId}`,
)
expect(rows[0].trafficUsed).toBe(25)
})
it('returns 422 when download traffic quota is exhausted', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'Mdownloadover', name: 'download.txt', size: 100 })
const trafficPeriod = currentTrafficPeriod()
await db.run(sql`
UPDATE org_quotas
SET traffic_quota = 0, traffic_used = 0, traffic_period = ${trafficPeriod}
WHERE org_id = ${orgId}
`)
const now = Date.now()
await db.run(sql`
UPDATE org_quota_entitlements
SET status = 'revoked', updated_at = ${now}
WHERE org_id = ${orgId}
AND resource_type = 'traffic'
AND entitlement_type = 'plan'
AND status = 'active'
`)
await db.run(sql`
INSERT INTO org_quota_entitlements
(id, org_id, resource_type, entitlement_type, source, source_id, bytes, starts_at, expires_at, status, metadata, created_at, updated_at)
VALUES
(${nanoid()}, ${orgId}, 'traffic', 'plan', 'test', ${`test-traffic-plan:${orgId}`}, 50, ${now}, NULL, 'active', '{"packageName":"Test Plan"}', ${now}, ${now})
`)
const res = await app.request('/api/objects/Mdownloadover', { headers })
expect(res.status).toBe(422)
const body = (await res.json()) as {
error: { message: string; status: string; details: Array<{ reason: string }> }
}
expect(body.error.message).toBe('Traffic quota exceeded')
expect(body.error.status).toBe('RESOURCE_EXHAUSTED')
expect(body.error.details[0].reason).toBe('QUOTA_EXCEEDED')
expect(S3Service.prototype.presignDownload).not.toHaveBeenCalled()
const rows = await db.all<{ trafficUsed: number }>(
sql`SELECT traffic_used AS trafficUsed FROM org_quotas WHERE org_id = ${orgId}`,
)
expect(rows[0].trafficUsed).toBe(0)
const failures = await db.all<{ bytes: number; reason: string; source: string }>(sql`
SELECT json_extract(metadata, '$.bytes') AS bytes,
json_extract(metadata, '$.reason') AS reason,
json_extract(metadata, '$.source') AS source
FROM audit_events
WHERE action = 'download_failed' AND target_id = 'Mdownloadover'
`)
expect(failures).toEqual([{ bytes: 100, reason: 'quota_exceeded', source: 'object_download' }])
})
})
// ─── POST .../completions — quota enforcement via the activation path ──────────
// The completions endpoint finalizes a draft (draft → live), reserving quota
// through the same confirmUpload core. These tests drive the real upload flow:
// POST /api/objects (creates the draft + session) then POST .../completions.
describe('POST .../completions — quota enforcement', () => {
// Creates a file draft via the API and returns {id, sessionId}. The S3 spies
// make the single-PUT HEAD return etag 'abc'.
async function createDraft(
app: Awaited<ReturnType<typeof createTestApp>>['app'],
headers: Record<string, string>,
body: { name: string; size: number; onConflict?: string },
): Promise<{ id: string; sessionId: string }> {
const res = await createDraftResponse(app, headers, body)
if (res.status !== 201) throw new Error(`create failed: ${res.status} ${await res.text()}`)
const created = (await res.json()) as { id: string; upload: { sessionId: string } }
return { id: created.id, sessionId: created.upload.sessionId }
}
function createDraftResponse(
app: Awaited<ReturnType<typeof createTestApp>>['app'],
headers: Record<string, string>,
body: { name: string; size: number; onConflict?: string },
) {
return app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'text/plain', parent: '', dirtype: 0, ...body }),
})
}
// The conflict strategy is fixed at create time (stored on the session); the
// completions body carries only the uploaded parts.
function complete(
app: Awaited<ReturnType<typeof createTestApp>>['app'],
headers: Record<string, string>,
ref: { id: string; sessionId: string },
) {
return app.request(`/api/objects/${ref.id}/uploads/${ref.sessionId}/completions`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parts: [{ partNumber: 1, etag: 'abc' }] }),
})
}
it('returns 200 and increments usage when quota allows', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 10000, 0)
const ref = await createDraft(app, headers, { name: 'uploading.txt', size: 350 })
const res = await complete(app, headers, ref)
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body.status).toBe('active')
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(quotaRows[0].used).toBe(350)
})
it('uses active storage entitlements when finalizing upload', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 100, 90)
await addStorageEntitlement(db, orgId, 100)
const ref = await createDraft(app, headers, { name: 'entitled.txt', size: 50 })
const res = await complete(app, headers, ref)
expect(res.status).toBe(200)
const quotaRows = await db.all<{ used: number; quota: number }>(
sql`SELECT used, quota FROM org_quotas WHERE org_id = ${orgId}`,
)
expect(quotaRows[0]).toEqual({ used: 140, quota: 100 })
})
it('returns an eligible capacity offer before upload [spec: objects/create-capacity-offer]', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 0, 90)
await addStorageEntitlement(db, orgId, 100)
stubCapacityStoreWithOffer()
const res = await createDraftResponse(app, headers, { name: 'upgrade.txt', size: 11 })
expect(res.status).toBe(402)
await expect(res.json()).resolves.toMatchObject({
error: 'CAPACITY_REQUIRED',
requestHash: expect.stringMatching(/^[0-9a-f]{64}$/),
requestedBytes: 11,
usedBytes: 90,
quotaBytes: 100,
offers: [
{
resourceId: 'capacity-pro:price-monthly',
productId: 'capacity-pro',
priceId: 'price-monthly',
storageBytes: 4096,
amount: 200,
currency: 'usd',
},
],
})
})
it('rejects upload preparation when no capacity offer can satisfy the request [spec: objects/create-capacity-unavailable]', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 0, 90)
await addStorageEntitlement(db, orgId, 100)
stubCapacityStoreWithoutOffers()
const res = await createDraftResponse(app, headers, { name: 'limited.txt', size: 11 })
expect(res.status).toBe(422)
await expect(res.json()).resolves.toMatchObject({ error: { message: 'Quota exceeded' } })
})
it('returns 200 and increments storages.used when quota allows', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 100)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 10000, 100)
const ref = await createDraft(app, headers, { name: 'photo.jpg', size: 400 })
await complete(app, headers, ref)
const storageRows = await db.all<{ used: number }>(sql`SELECT used FROM storages WHERE id = ${validStorage.id}`)
expect(storageRows[0].used).toBe(500)
})
it('returns 422 before upload when no capacity store is bound', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
// quota = 100, used = 90, file size = 50 → exceeds
await setOrgQuota(db, orgId, 100, 90)
const res = await createDraftResponse(app, headers, { name: 'toobig.txt', size: 50 })
expect(res.status).toBe(422)
const body = (await res.json()) as { error: { message: string; details: Array<{ reason: string }> } }
expect(body.error.message).toBe('Quota exceeded')
expect(body.error.details[0].reason).toBe('QUOTA_EXCEEDED')
})
it('does not change usage when a file with size 0 is finalized', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db, 50)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 10000, 50)
// HEAD returns size 0 for the empty file; the reported etag still matches.
vi.mocked(S3Service.prototype.headObject).mockResolvedValue({ size: 0, contentType: 'text/plain', etag: 'abc' })
const ref = await createDraft(app, headers, { name: 'empty.txt', size: 0 })
await complete(app, headers, ref)
const storageRows = await db.all<{ used: number }>(sql`SELECT used FROM storages WHERE id = ${validStorage.id}`)
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(storageRows[0].used).toBe(50)
expect(quotaRows[0].used).toBe(50)
})
it('returns 422 before upload when no quota row or entitlement exists', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await db.delete(orgQuotaEntitlements).where(eq(orgQuotaEntitlements.orgId, orgId))
await db.delete(orgQuotas).where(eq(orgQuotas.orgId, orgId))
const res = await createDraftResponse(app, headers, { name: 'nolimit.txt', size: 5000 })
expect(res.status).toBe(422)
await expect(res.json()).resolves.toMatchObject({ error: { details: [{ reason: 'QUOTA_EXCEEDED' }] } })
})
it('replaces a same-size file at full quota — net-neutral, incumbent purged', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
// Quota exactly full: the 100-byte incumbent fills the 100-byte quota.
await insertFile(db, orgId, { id: 'incumbent', name: 'doc.txt', size: 100 })
await setOrgQuota(db, orgId, 100, 100)
// Create the replacement draft with onConflict:'replace' (stored on the
// session; incumbent stays active — overwrite deferred to completion).
const ref = await createDraft(app, headers, { name: 'doc.txt', size: 100, onConflict: 'replace' })
// Complete: the incumbent's bytes are freed so this is net-neutral.
const completeRes = await complete(app, headers, ref)
expect(completeRes.status).toBe(200)
// Incumbent content is purged but its tombstone is retained; usage is unchanged.
const incumbent = await db.all<{ purgedAt: number | null }>(
sql`SELECT purged_at AS purgedAt FROM matters WHERE id = 'incumbent'`,
)
expect(incumbent[0]?.purgedAt).toEqual(expect.any(Number))
const quotaRows = await db.all<{ used: number }>(sql`SELECT used FROM org_quotas WHERE org_id = ${orgId}`)
expect(quotaRows[0].used).toBe(100)
})
})
})
// ─── Multipart upload against a live S3-compatible mock ───────────────────────
// Self-contained: spins up an in-process S3 mock and uses its own storage row.
describe('object multipart upload API with S3-compatible storage', () => {
let server: Server | undefined
// Undo the file-level S3Service spies so this test exercises the real gateway
// against its in-process mock instead of the stubbed presign URLs.
beforeEach(() => {
vi.restoreAllMocks()
})
async function insertStorage(db: Awaited<ReturnType<typeof createTestApp>>['db'], endpoint: string) {
const now = Date.now()
await db.run(sql`
INSERT INTO storages (
id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host,
capacity, used, status, egress_credit_billing_enabled, egress_credit_unit_bytes,
egress_credit_per_unit, created_at, updated_at
)
VALUES (
'multipart-live-storage', 'test-bucket',
${endpoint}, 'auto', 'test-access-key', 'test-secret-key',
'$UID/$RAW_NAME', '', 0, 0, 'active', 0, ${100 * 1024 * 1024}, 1, ${now}, ${now}
)
`)
}
async function startMultipartS3Mock(): Promise<{ endpoint: string; server: Server }> {
const objects = new Map<string, Uint8Array>()
const uploads = new Map<string, { bucket: string; key: string; parts: Map<number, Uint8Array> }>()
const server = createServer(async (req, res) => {
try {
const url = new URL(req.url ?? '/', `http://${req.headers.host}`)
const { bucket, key } = parsePath(url.pathname)
const objectKey = `${bucket}/${key}`
if (req.method === 'POST' && url.searchParams.has('uploads')) {
const uploadId = randomUUID()
uploads.set(uploadId, { bucket, key, parts: new Map() })
res.writeHead(200, { 'Content-Type': 'application/xml' })
res.end(`<CreateMultipartUploadResult><UploadId>${uploadId}</UploadId></CreateMultipartUploadResult>`)
return
}
if (req.method === 'PUT' && url.searchParams.has('uploadId') && url.searchParams.has('partNumber')) {
const upload = uploads.get(url.searchParams.get('uploadId') ?? '')
if (!upload) {
res.writeHead(404)
res.end('Upload not found')
return
}
const body = await readBody(req)
upload.parts.set(Number(url.searchParams.get('partNumber')), body)
res.writeHead(200, { etag: etag(body) })
res.end('')
return
}
if (req.method === 'POST' && url.searchParams.has('uploadId')) {
const uploadId = url.searchParams.get('uploadId') ?? ''
const upload = uploads.get(uploadId)
if (!upload) {
res.writeHead(404)
res.end('Upload not found')
return
}
const parts = [...upload.parts.entries()].sort(([left], [right]) => left - right)
const body = concat(parts.map(([, part]) => part))
objects.set(`${upload.bucket}/${upload.key}`, body)
uploads.delete(uploadId)
res.writeHead(200, { 'Content-Type': 'application/xml' })
res.end('<CompleteMultipartUploadResult />')
return
}
// Single PutObject (≤5 GiB upload): store the body and return its ETag.
if (req.method === 'PUT') {
const body = await readBody(req)
objects.set(objectKey, body)
res.writeHead(200, { etag: etag(body) })
res.end('')
return
}
// HeadObject — completions HEADs the object to confirm the single-PUT ETag.
if (req.method === 'HEAD') {
const object = objects.get(objectKey)
if (!object) {
res.writeHead(404)
res.end('')
return
}
res.writeHead(200, { 'Content-Length': object.byteLength, etag: etag(object) })
res.end('')
return
}
if (req.method === 'GET') {
const object = objects.get(objectKey)
if (!object) {
res.writeHead(404)
res.end('Not found')
return
}
res.writeHead(200, { 'Content-Length': object.byteLength, etag: etag(object) })
res.end(object)
return
}
res.writeHead(405)
res.end('Method not allowed')
} catch (error) {
res.writeHead(500)
res.end(error instanceof Error ? error.message : String(error))
}
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (!address || typeof address === 'string') throw new Error('S3 mock did not bind to a TCP port')
return { endpoint: `http://127.0.0.1:${address.port}`, server }
}
function parsePath(pathname: string): { bucket: string; key: string } {
const parts = pathname.split('/').filter(Boolean).map(decodeURIComponent)
return { bucket: parts[0] ?? '', key: parts.slice(1).join('/') }
}
async function readBody(req: IncomingMessage): Promise<Uint8Array> {
const chunks: Buffer[] = []
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
return new Uint8Array(Buffer.concat(chunks))
}
function concat(parts: Uint8Array[]): Uint8Array {
const body = new Uint8Array(parts.reduce((sum, part) => sum + part.byteLength, 0))
let offset = 0
for (const part of parts) {
body.set(part, offset)
offset += part.byteLength
}
return body
}
function etag(body: Uint8Array): string {
return `"${createHash('md5').update(body).digest('hex')}"`
}
afterEach(async () => {
if (!server) return
await new Promise<void>((resolve, reject) => {
server?.close((error) => (error ? reject(error) : resolve()))
})
server = undefined
})
it('uploads a small object end-to-end via the single-PUT flow against a live S3 mock', async () => {
const s3 = await startMultipartS3Mock()
server = s3.server
const { app, db } = await createTestApp()
await insertStorage(db, s3.endpoint)
const headers = await authedHeaders(app)
// ≤5 GiB → one presigned PUT URL and partSize === size.
const createRes = await app.request('/api/objects', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'upload-smoke.txt',
type: 'text/plain',
size: 11,
parent: '',
}),
})
expect(createRes.status).toBe(201)
const object = (await createRes.json()) as {
id: string
upload: { sessionId: string; partSize: number; parts: Array<{ url: string; headers: Record<string, string> }> }
}
expect(object.upload.parts).toHaveLength(1)
expect(object.upload.partSize).toBe(11)
// PUT the bytes directly to the presigned URL, read the ETag.
const putRes = await fetch(object.upload.parts[0].url, {
method: 'PUT',
headers: object.upload.parts[0].headers,
body: 'hello world',
})
expect(putRes.status).toBe(200)
const etagHeader = putRes.headers.get('etag')
expect(etagHeader).toBeTruthy()
// Finalize: completions HEADs the object and matches the reported ETag.
const completeRes = await app.request(`/api/objects/${object.id}/uploads/${object.upload.sessionId}/completions`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parts: [{ partNumber: 1, etag: etagHeader }] }),
})
expect(completeRes.status).toBe(200)
expect(((await completeRes.json()) as { status: string }).status).toBe('active')
const objectRes = await app.request(`/api/objects/${object.id}`, { headers })
expect(objectRes.status).toBe(200)
const active = (await objectRes.json()) as { downloadUrl: string }
const downloadRes = await fetch(active.downloadUrl)
expect(downloadRes.status).toBe(200)
await expect(downloadRes.text()).resolves.toBe('hello world')
})
})
// ─── Error-branch coverage (AIP-193 bodies) ───────────────────────────────────
// These exercise the inline `apiError(...)` guards in the handlers that the
// happy-path tests above don't reach: cross-org list authz, missing-storage
// resolution, the download-task-upload confirm guards, and the editor-access
// gate for an API key principal on session-only object routes.
// Creates an API key via the real better-auth plugin. Workspace-scoped keys use
// a workspace-capable template; user-workspace keys use WebDAV.
async function createUserApiKey(
auth: Awaited<ReturnType<typeof createTestApp>>['auth'],
userId: string,
opts: { orgId?: string; permissions?: Record<string, string[]> } = {},
): Promise<string> {
// biome-ignore lint/suspicious/noExplicitAny: better-auth plugin API not fully typed
const result = (await (auth.api as any).createApiKey({
body: {
configId: opts.orgId ? 'ihost' : 'webdav',
name: 'test-api-key',
userId,
...(opts.orgId ? { organizationId: opts.orgId } : {}),
permissions: opts.permissions,
},
})) as { key: string }
return result.key
}
const downloaderHeartbeat = {
version: '1.0.0',
hostname: 'host',
platform: 'linux',
arch: 'x64',
engine: 'aria2',
capabilities: ['http', 'magnet', 'torrent'],
maxConcurrentTasks: 2,
currentTasks: 0,
downloadBps: 0,
uploadBps: 0,
freeDiskBytes: 1024 * 1024 * 1024,
}
// Registers a downloader, creates a task, lets heartbeat claim it, and returns
// the upload token plus the task's target folder. The token authenticates as a
// `download-task-upload` principal scoped to that task/folder.
async function mintTaskUploadContext(
app: TestApp,
db: TestDb,
opts: { targetFolder: string },
): Promise<{ uploadToken: string; targetFolder: string; orgId: string }> {
const admin = await adminHeaders(app)
const codeRes = await app.request('/api/auth/device/code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: 'zpan-cli', scope: 'downloader:register' }),
})
const code = (await codeRes.json()) as { device_code: string; user_code: string }
await app.request(`/api/auth/device?user_code=${encodeURIComponent(code.user_code)}`, { headers: admin })
await app.request('/api/auth/device/approve', {
method: 'POST',
headers: { ...admin, 'Content-Type': 'application/json' },
body: JSON.stringify({ userCode: code.user_code }),
})
const tokenRes = await app.request('/api/auth/device/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: code.device_code,
client_id: 'zpan-cli',
}),
})
const cliToken = (await tokenRes.json()) as { access_token: string }
const downloaderHeaders = { Authorization: `Bearer ${cliToken.access_token}`, 'Content-Type': 'application/json' }
const createDownloaderRes = await app.request('/api/downloads/downloaders', {
method: 'POST',
headers: downloaderHeaders,
body: JSON.stringify({ name: 'object-error-downloader', heartbeat: downloaderHeartbeat }),
})
const downloader = (await createDownloaderRes.json()) as { token: string }
await app.request('/api/downloads/downloaders/me/heartbeats', {
method: 'POST',
headers: { Authorization: `Bearer ${downloader.token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ ...downloaderHeartbeat, currentTasks: 0 }),
})
const user = await adminHeaders(app)
const createTaskRes = await app.request('/api/downloads/tasks', {
method: 'POST',
headers: { ...user, 'Content-Type': 'application/json' },
body: JSON.stringify({
source: { type: 'http', uri: 'https://example.com/file.txt' },
targetFolder: opts.targetFolder,
name: 'file.txt',
}),
})
expect(createTaskRes.status).toBe(201)
const assignedRes = await app.request('/api/downloads/downloaders/me/heartbeats', {
method: 'POST',
headers: { Authorization: `Bearer ${downloader.token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ ...downloaderHeartbeat, currentTasks: 0 }),
})
const assigned = (await assignedRes.json()) as {
assignments: Array<{ status: { assignment?: { uploadToken?: string } } }>
}
const uploadToken = assigned.assignments[0]?.status.assignment?.uploadToken
if (!uploadToken) throw new Error('upload_token_missing')
const orgRows = await db.all<{ orgId: string }>(sql`SELECT org_id AS orgId FROM download_tasks LIMIT 1`)
return { uploadToken, targetFolder: opts.targetFolder, orgId: orgRows[0].orgId }
}
describe('Objects API — error branches', () => {
it('accepts a download-task-upload token scoped to a Unicode target folder', async () => {
const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
await insertStorage(db)
const targetFolder = 'Media/Music/欧阳娜娜/NANA II (2020)'
const { uploadToken } = await mintTaskUploadContext(app, db, { targetFolder })
const res = await app.request('/api/objects', {
method: 'POST',
headers: { Authorization: `Bearer ${uploadToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '藏.mp3',
type: 'audio/mpeg',
size: 1,
parent: targetFolder,
}),
})
expect(res.status).toBe(201)
const body = (await res.json()) as { createdBy: unknown; name: string; parent: string }
expect(body).toMatchObject({
name: '藏.mp3',
parent: targetFolder,
createdBy: { type: 'device' },
})
expect(body.createdBy).not.toHaveProperty('resolved')
})
it('creates a folder with a workspace API key that has objects:create [spec: objects/creator-attribution]', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
await insertStorage(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
const orgId = await getOrgId(db)
const key = await createUserApiKey(auth, userId, { orgId, permissions: { objects: ['create'] } })
const res = await app.request('/api/objects', {
method: 'POST',
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'api-key-folder', type: 'folder', dirtype: 1, parent: '' }),
})
expect(res.status).toBe(201)
const body = (await res.json()) as { createdBy: unknown; name: string; orgId: string }
expect(body).toMatchObject({
name: 'api-key-folder',
orgId,
createdBy: { type: 'api_key' },
})
expect(body.createdBy).not.toHaveProperty('resolved')
})
it('returns null instead of attributing a legacy object to the workspace owner [spec: objects/legacy-creator]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'legacy-creator-file', name: 'legacy.txt' })
const res = await app.request('/api/objects/legacy-creator-file', { headers })
expect(res.status).toBe(200)
await expect(res.json()).resolves.toMatchObject({ id: 'legacy-creator-file', createdBy: null })
const creatorRes = await app.request('/api/objects/legacy-creator-file/creator', { headers })
expect(creatorRes.status).toBe(404)
})
it('returns 403 when an object API key is missing the route scope', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
const userId = await getUserIdByEmail(db, 'test@example.com')
const orgId = await getOrgId(db)
const key = await createUserApiKey(auth, userId, { orgId, permissions: { objects: ['create'] } })
const res = await app.request('/api/objects', { headers: { Authorization: `Bearer ${key}` } })
expect(res.status).toBe(403)
const body = (await res.json()) as { error: { message: string; status: string } }
expect(body.error.message).toBe('Forbidden')
expect(body.error.status).toBe('PERMISSION_DENIED')
})
it('denies a fixed-workspace API key list orgId override before cross-org lookup', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
const userId = await getUserIdByEmail(db, 'test@example.com')
const orgId = await getOrgId(db)
await insertTeamOrg(db, 'Teamfixedlist')
await insertMember(db, 'Teamfixedlist', userId, 'owner')
const key = await createUserApiKey(auth, userId, { orgId, permissions: { objects: ['read'] } })
const res = await app.request('/api/objects?orgId=Teamfixedlist', { headers: { Authorization: `Bearer ${key}` } })
expect(res.status).toBe(403)
})
it('allows same-workspace copy but denies cross-space transfer for a fixed-workspace API key', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
await insertStorage(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'FixedCopySource', name: 'copy.txt' })
await insertTeamOrg(db, 'Teamfixedtransfer')
await insertMember(db, 'Teamfixedtransfer', userId, 'editor')
const key = await createUserApiKey(auth, userId, { orgId, permissions: { objects: ['update'] } })
const headers = { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' }
const copy = await app.request('/api/objects/FixedCopySource/copies', {
method: 'POST',
headers,
body: JSON.stringify({ parent: '' }),
})
expect(copy.status).toBe(201)
const transfer = await transferRequest(app, headers, 'FixedCopySource', {
targetOrgId: 'Teamfixedtransfer',
mode: 'copy',
})
expect(transfer.status).toBe(403)
})
it('returns 403 when listing an org the user cannot read via orgId override', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
await insertTeamOrg(db, 'Teamforeign')
const res = await app.request('/api/objects?orgId=Teamforeign', { headers })
expect(res.status).toBe(403)
const body = (await res.json()) as { error: { message: string; status: string } }
expect(body.error.message).toBe('Forbidden')
expect(body.error.status).toBe('PERMISSION_DENIED')
})
it('returns 404 when a file references a missing storage on GET', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
const orgId = await getOrgId(db)
// File with a non-empty object key but no matching storage row.
await insertFile(db, orgId, { id: 'Mnostorage', name: 'orphan.txt' })
const res = await app.request('/api/objects/Mnostorage', { headers })
expect(res.status).toBe(404)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Storage not found')
})
it('returns 404 when copying a file whose storage is missing', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
const orgId = await getOrgId(db)
await insertFile(db, orgId, { id: 'Mcopyorphan', name: 'orphan.txt' })
const res = await app.request('/api/objects/Mcopyorphan/copies', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: '' }),
})
expect(res.status).toBe(404)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Storage not found')
})
it('returns 404 when transferring a missing object', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await insertStorage(db)
const userId = await getUserIdByEmail(db, 'test@example.com')
await insertTeamOrg(db, 'Teamdest')
await insertMember(db, 'Teamdest', userId, 'editor')
const res = await transferRequest(app, headers, 'DoesNotExist', { targetOrgId: 'Teamdest', mode: 'copy' })
expect(res.status).toBe(404)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Not found')
})
it('rejects a download-task-upload token that tries to soft-delete (trash) an object', async () => {
const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
await insertStorage(db)
const { uploadToken, orgId } = await mintTaskUploadContext(app, db, { targetFolder: 'Remote' })
await insertFile(db, orgId, { id: 'Mtasktrash', name: 'file.txt', parent: 'Remote' })
// DELETE /objects/:id requires objects:delete; a download-task-upload token
// is authenticated but only carries upload lifecycle scopes.
const res = await app.request('/api/objects/Mtasktrash', {
method: 'DELETE',
headers: { Authorization: `Bearer ${uploadToken}` },
})
expect(res.status).toBe(403)
})
it('rejects a download-task-upload completion outside the task target folder', async () => {
const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
await insertStorage(db)
const { uploadToken, orgId } = await mintTaskUploadContext(app, db, { targetFolder: 'Remote' })
// Draft sits outside the token's authorized folder, so the completion guard denies
// before any session lookup.
await insertFile(db, orgId, { id: 'Mtaskoutside', name: 'file.txt', parent: 'Elsewhere', status: 'draft' })
const res = await app.request('/api/objects/Mtaskoutside/uploads/AnySession/completions', {
method: 'POST',
headers: { Authorization: `Bearer ${uploadToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ parts: [{ partNumber: 1, etag: 'abc' }] }),
})
expect(res.status).toBe(403)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Forbidden')
})
it('rejects download-task-upload re-sign outside the task target folder', async () => {
const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
await insertStorage(db)
const { uploadToken, orgId } = await mintTaskUploadContext(app, db, { targetFolder: 'Remote' })
await insertFile(db, orgId, {
id: 'Mtaskpresignoutside',
name: 'file.txt',
parent: 'Elsewhere',
status: 'draft',
})
const res = await app.request('/api/objects/Mtaskpresignoutside/uploads/AnySession/parts', {
method: 'POST',
headers: { Authorization: `Bearer ${uploadToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ partNumbers: [1] }),
})
expect(res.status).toBe(403)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Forbidden')
})
it('rejects download-task-upload re-sign for another downloader upload session', async () => {
const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
await insertStorage(db)
const { uploadToken } = await mintTaskUploadContext(app, db, { targetFolder: 'Remote' })
const createRes = await app.request('/api/objects', {
method: 'POST',
headers: { Authorization: `Bearer ${uploadToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'owned-by-first.txt',
type: 'text/plain',
size: 100,
parent: 'Remote',
}),
})
expect(createRes.status).toBe(201)
const created = (await createRes.json()) as { id: string; upload: { sessionId: string } }
await db.run(
sql`UPDATE object_upload_sessions SET created_by = 'downloader:other' WHERE id = ${created.upload.sessionId}`,
)
const res = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}/parts`, {
method: 'POST',
headers: { Authorization: `Bearer ${uploadToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ partNumbers: [1] }),
})
expect(res.status).toBe(403)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Forbidden')
})
it('rejects download-task-upload abort outside the task target folder', async () => {
const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
await insertStorage(db)
const { uploadToken, orgId } = await mintTaskUploadContext(app, db, { targetFolder: 'Remote' })
await insertFile(db, orgId, { id: 'Mtaskabortoutside', name: 'file.txt', parent: 'Elsewhere', status: 'draft' })
const res = await app.request('/api/objects/Mtaskabortoutside/uploads/AnySession', {
method: 'DELETE',
headers: { Authorization: `Bearer ${uploadToken}` },
})
expect(res.status).toBe(403)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Forbidden')
})
it('allows repeated download-task-upload abort after the draft is gone', async () => {
const { app, db, platform } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' })
await insertStorage(db)
const targetFolder = 'Remote'
const { uploadToken } = await mintTaskUploadContext(app, db, { targetFolder })
const createRes = await app.request('/api/objects', {
method: 'POST',
headers: { Authorization: `Bearer ${uploadToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'task-cancel.txt',
type: 'text/plain',
size: 1,
parent: targetFolder,
}),
})
expect(createRes.status).toBe(201)
const created = (await createRes.json()) as { id: string; upload: { sessionId: string } }
const first = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${uploadToken}` },
})
expect(first.status).toBe(204)
const tokenGateway = createDownloadTokenGateway()
const claims = await tokenGateway.verifyDownloadToken(platform, uploadToken)
if (!claims || claims.typ !== 'download-task-upload') throw new Error('task_upload_claims_missing')
const otherDownloaderId = 'other-downloader'
await db
.update(downloadTasks)
.set({ assignedDownloaderId: otherDownloaderId })
.where(eq(downloadTasks.id, claims.taskId))
const otherUploadToken = await tokenGateway.signDownloadToken(platform, {
...claims,
downloaderId: otherDownloaderId,
jti: randomUUID(),
})
const otherDownloader = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${otherUploadToken}` },
})
expect(otherDownloader.status).toBe(403)
await db
.update(downloadTasks)
.set({ assignedDownloaderId: claims.downloaderId })
.where(eq(downloadTasks.id, claims.taskId))
const second = await app.request(`/api/objects/${created.id}/uploads/${created.upload.sessionId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${uploadToken}` },
})
expect(second.status).toBe(204)
void db
})
})