Files
zpan/server/http/users.integration.test.ts
T
2026-07-26 15:55:44 -04:00

757 lines
32 KiB
TypeScript

import { sql } from 'drizzle-orm'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { authedHeaders, createTestApp, seedBusinessLicense } from '../test/setup.js'
async function adminHeaders(app: ReturnType<typeof import('../app')['createApp']>) {
// Sign up first user (gets promoted to admin via hook)
await authedHeaders(app, 'admin@example.com', 'password123456')
// Sign in again to get a session that reflects the admin role
const signInRes = await app.request('/api/auth/sign-in/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'admin@example.com', password: 'password123456' }),
})
return { Cookie: signInRes.headers.getSetCookie().join('; ') }
}
async function signUpUser(app: ReturnType<typeof import('../app')['createApp']>, email: string, name = 'Other User') {
const res = await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, password: 'password123456' }),
})
return res.json()
}
async function personalOrgId(db: Awaited<ReturnType<typeof createTestApp>>['db'], userId: string): Promise<string> {
const rows = 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 = ${userId}
AND (o.slug LIKE 'personal-%' OR COALESCE(o.metadata, '') LIKE '%"type":"personal"%')
LIMIT 1
`)
if (!rows[0]) throw new Error(`No personal org found for user ${userId}`)
return rows[0].id
}
describe('User entitlements API (admin)', () => {
it('GET /api/users/:id/quota returns the user storage used/total [spec: users/quota-personal-org]', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'quota-sub@example.com')
const rows = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'quota-sub@example.com'`)
const userId = rows[0].id
const orgId = await personalOrgId(db, userId)
await db.run(sql`UPDATE org_quotas SET used = 4242 WHERE org_id = ${orgId}`)
const res = await app.request(`/api/users/${userId}/quota`, { headers })
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ used: 4242, total: 10485760, hasPersonalOrg: true })
})
it('GET /api/users/:id/quota reports hasPersonalOrg=false when the user has none', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'quota-noorg@example.com')
const rows = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'quota-noorg@example.com'`)
const userId = rows[0].id
await db.run(sql`DELETE FROM member WHERE user_id = ${userId}`)
const res = await app.request(`/api/users/${userId}/quota`, { headers })
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ used: 0, total: 0, hasPersonalOrg: false })
})
it('GET /api/users/:id/entitlements lists entitlements for an admin', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'entlist@example.com')
const rows = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'entlist@example.com'`)
const userId = rows[0].id
const res = await app.request(`/api/users/${userId}/entitlements`, { headers })
expect(res.status).toBe(200)
})
it('GET /api/users/:id/entitlements returns 404 for a missing user', async () => {
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const res = await app.request('/api/users/nonexistent/entitlements', { headers })
expect(res.status).toBe(404)
})
it('POST /api/users/:id/entitlements grants storage entitlement for a personal org [spec: users/grant-entitlement]', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'grant-storage@example.com')
const users = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'grant-storage@example.com'`)
const userId = users[0].id
const orgId = await personalOrgId(db, userId)
const res = await app.request(`/api/users/${userId}/entitlements`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ resourceType: 'storage', bytes: 123456, note: 'launch bonus' }),
})
expect(res.status).toBe(201)
const body = (await res.json()) as { orgId: string; entitlement: Record<string, unknown> }
expect(body.orgId).toBe(orgId)
expect(body.entitlement).toMatchObject({
orgId,
resourceType: 'storage',
entitlementType: 'grant',
source: 'admin_grant',
bytes: 123456,
status: 'active',
})
const entitlements = await db.all<{ bytes: number; entitlementType: string; source: string }>(
sql`SELECT bytes, entitlement_type AS entitlementType, source FROM org_quota_entitlements WHERE org_id = ${orgId} AND source = 'admin_grant'`,
)
expect(entitlements).toEqual([{ bytes: 123456, entitlementType: 'grant', source: 'admin_grant' }])
})
it('PATCH /api/users/:id/entitlements/:eid updates an admin grant [spec: users/update-entitlement]', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
const user = (await signUpUser(app, 'edit-grant@example.com')) as { user: { id: string } }
const userId = user.user.id
const grant = await app.request(`/api/users/${userId}/entitlements`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ resourceType: 'storage', bytes: 1000 }),
})
const { entitlement } = (await grant.json()) as { entitlement: { id: string } }
const expiresAt = '2030-01-01T00:00:00.000Z'
const res = await app.request(`/api/users/${userId}/entitlements/${entitlement.id}`, {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ bytes: 5000, expiresAt, note: 'bumped' }),
})
expect(res.status).toBe(200)
const body = (await res.json()) as { entitlement: Record<string, unknown> }
expect(body.entitlement).toMatchObject({ id: entitlement.id, bytes: 5000, status: 'active' })
const rows = await db.all<{ bytes: number; expiresAt: number }>(
sql`SELECT bytes, expires_at AS expiresAt FROM org_quota_entitlements WHERE id = ${entitlement.id}`,
)
expect(rows[0].bytes).toBe(5000)
expect(rows[0].expiresAt).toBe(new Date(expiresAt).getTime())
})
it('DELETE /api/users/:id/entitlements/:eid revokes an admin grant [spec: users/revoke-entitlement]', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
const user = (await signUpUser(app, 'revoke-grant@example.com')) as { user: { id: string } }
const userId = user.user.id
const grant = await app.request(`/api/users/${userId}/entitlements`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ resourceType: 'storage', bytes: 2000 }),
})
const { entitlement } = (await grant.json()) as { entitlement: { id: string } }
const res = await app.request(`/api/users/${userId}/entitlements/${entitlement.id}`, {
method: 'DELETE',
headers,
})
expect(res.status).toBe(204)
const rows = await db.all<{ status: string }>(
sql`SELECT status FROM org_quota_entitlements WHERE id = ${entitlement.id}`,
)
expect(rows[0].status).toBe('revoked')
})
it('PATCH /api/users/:id/entitlements/:eid preserves unspecified fields', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
const user = (await signUpUser(app, 'patch-partial@example.com')) as { user: { id: string } }
const userId = user.user.id
const grant = await app.request(`/api/users/${userId}/entitlements`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ resourceType: 'storage', bytes: 1000, expiresAt: '2030-01-01T00:00:00.000Z' }),
})
const { entitlement } = (await grant.json()) as { entitlement: { id: string } }
const bytesOnly = await app.request(`/api/users/${userId}/entitlements/${entitlement.id}`, {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ bytes: 7000 }),
})
expect(bytesOnly.status).toBe(200)
let rows = await db.all<{ bytes: number; expiresAt: number | null }>(
sql`SELECT bytes, expires_at AS expiresAt FROM org_quota_entitlements WHERE id = ${entitlement.id}`,
)
expect(rows[0].bytes).toBe(7000)
expect(rows[0].expiresAt).toBe(new Date('2030-01-01T00:00:00.000Z').getTime())
const expiryOnly = await app.request(`/api/users/${userId}/entitlements/${entitlement.id}`, {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ expiresAt: null }),
})
expect(expiryOnly.status).toBe(200)
rows = await db.all<{ bytes: number; expiresAt: number | null }>(
sql`SELECT bytes, expires_at AS expiresAt FROM org_quota_entitlements WHERE id = ${entitlement.id}`,
)
expect(rows[0].bytes).toBe(7000)
expect(rows[0].expiresAt).toBeNull()
})
it('PATCH /api/users/:id/entitlements/:eid handles a grant with no metadata', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'no-metadata-grant@example.com')
const users = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'no-metadata-grant@example.com'`)
const userId = users[0].id
const orgId = await personalOrgId(db, userId)
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, expires_at, status, metadata, created_at, updated_at)
VALUES
('ent-no-meta', ${orgId}, 'storage', 'grant', 'admin_grant', 'admin_grant:no-meta', 1000, ${now}, NULL, 'active', NULL, ${now}, ${now})
`)
const res = await app.request(`/api/users/${userId}/entitlements/ent-no-meta`, {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ note: 'first note' }),
})
expect(res.status).toBe(200)
const rows = await db.all<{ metadata: string }>(
sql`SELECT metadata FROM org_quota_entitlements WHERE id = 'ent-no-meta'`,
)
expect(JSON.parse(rows[0].metadata)).toMatchObject({ note: 'first note' })
})
it('PATCH and DELETE entitlement return 404 for an unknown entitlement id', async () => {
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const user = (await signUpUser(app, 'unknown-ent@example.com')) as { user: { id: string } }
const userId = user.user.id
const patch = await app.request(`/api/users/${userId}/entitlements/does-not-exist`, {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ bytes: 1 }),
})
expect(patch.status).toBe(404)
const del = await app.request(`/api/users/${userId}/entitlements/does-not-exist`, {
method: 'DELETE',
headers,
})
expect(del.status).toBe(404)
})
it('PATCH and DELETE entitlement return 404 when the user has no personal org', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'no-org-edit@example.com')
const users = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'no-org-edit@example.com'`)
const userId = users[0].id
await db.run(sql`DELETE FROM member WHERE user_id = ${userId}`)
const patch = await app.request(`/api/users/${userId}/entitlements/any`, {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ bytes: 1 }),
})
expect(patch.status).toBe(404)
const del = await app.request(`/api/users/${userId}/entitlements/any`, {
method: 'DELETE',
headers,
})
expect(del.status).toBe(404)
})
it('DELETE /api/users/:id/entitlements/:eid rejects non-admin-grant sources [spec: users/entitlement-source-guard]', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'free-plan-revoke@example.com')
const users = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'free-plan-revoke@example.com'`)
const userId = users[0].id
const orgId = await personalOrgId(db, userId)
const free = await db.all<{ id: string }>(
sql`SELECT id FROM org_quota_entitlements WHERE org_id = ${orgId} AND source = 'free_plan' LIMIT 1`,
)
const res = await app.request(`/api/users/${userId}/entitlements/${free[0].id}`, {
method: 'DELETE',
headers,
})
expect(res.status).toBe(400)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Only admin-granted entitlements can be modified')
})
it('PATCH /api/users/:id/entitlements/:eid rejects non-admin-grant sources', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'free-plan-edit@example.com')
const users = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'free-plan-edit@example.com'`)
const userId = users[0].id
const orgId = await personalOrgId(db, userId)
const free = await db.all<{ id: string }>(
sql`SELECT id FROM org_quota_entitlements WHERE org_id = ${orgId} AND source = 'free_plan' LIMIT 1`,
)
const res = await app.request(`/api/users/${userId}/entitlements/${free[0].id}`, {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ bytes: 9999 }),
})
expect(res.status).toBe(400)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Only admin-granted entitlements can be modified')
})
it('POST /api/users/:id/entitlements rejects traffic grants', async () => {
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const user = (await signUpUser(app, 'traffic-grant@example.com')) as { user: { id: string } }
const res = await app.request(`/api/users/${user.user.id}/entitlements`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ resourceType: 'traffic', bytes: 123456 }),
})
expect(res.status).toBe(400)
})
it('POST /api/users/:id/entitlements fails when selected user has no personal org', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await signUpUser(app, 'no-personal-org@example.com')
const users = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'no-personal-org@example.com'`)
const userId = users[0].id
await db.run(sql`DELETE FROM member WHERE user_id = ${userId}`)
const res = await app.request(`/api/users/${userId}/entitlements`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ resourceType: 'storage', bytes: 123456 }),
})
expect(res.status).toBe(404)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe(`Personal organization not found for user: ${userId}`)
})
it('POST /api/users/:id/entitlements rejects non-positive bytes', async () => {
const { app } = await createTestApp()
const headers = await adminHeaders(app)
const user = (await signUpUser(app, 'zero-grant@example.com')) as { user: { id: string } }
const res = await app.request(`/api/users/${user.user.id}/entitlements`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ resourceType: 'storage', bytes: 0 }),
})
expect(res.status).toBe(400)
})
})
// ─── User avatar (PUT/DELETE /api/users/me/avatar) ────────────────────────────
const CLOUD_AVATAR_URL = 'https://avatars.zpan.cloud/user/u1.webp'
// Stub the Cloud avatar service and capture each avatar request so tests can
// assert the /avatars/:scope/:id path, the image content type, and the bearer
// auth that reach Cloud. `seedBusinessLicense` makes the instance Cloud-paired
// (active license binding, refresh token 'test-refresh-token').
function stubCloudAvatarFetch() {
const calls: { url: string; method: string; contentType: string | null; authorization: string | null }[] = []
vi.stubGlobal(
'fetch',
vi.fn(async (url: string | URL, init?: RequestInit) => {
const u = String(url)
if (u.includes('/avatars/')) {
const headers = new Headers(init?.headers)
calls.push({
url: u,
method: init?.method ?? 'GET',
contentType: headers.get('content-type'),
authorization: headers.get('authorization'),
})
if (init?.method === 'DELETE') return new Response(null, { status: 204 })
return new Response(JSON.stringify({ url: CLOUD_AVATAR_URL, key: 'avatars/user/u1' }), {
status: 201,
headers: { 'content-type': 'application/json' },
})
}
return new Response('unexpected fetch', { status: 404 })
}),
)
return calls
}
function makeFile(type: string, bytes = 16): File {
return new File([new Uint8Array(bytes)], `f.${type.split('/')[1]}`, { type })
}
describe('PUT /api/users/me/avatar', () => {
beforeEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it('returns 401 without auth [spec: avatar/auth-required]', async () => {
const { app } = await createTestApp()
const form = new FormData()
form.set('file', makeFile('image/png'))
const res = await app.request('/api/users/me/avatar', { method: 'PUT', body: form })
expect(res.status).toBe(401)
})
it('returns 415 when Content-Type is not multipart [spec: avatar/multipart-required]', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const res = await app.request('/api/users/me/avatar', {
method: 'PUT',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ nope: true }),
})
expect(res.status).toBe(415)
})
it('returns 400 when file field is missing [spec: avatar/file-required]', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const form = new FormData()
form.set('notFile', 'x')
const res = await app.request('/api/users/me/avatar', { method: 'PUT', headers, body: form })
expect(res.status).toBe(400)
})
it('returns 400 for an unsupported mime, before any Cloud call [spec: avatar/mime-validated]', async () => {
const { app, db } = await createTestApp()
await seedBusinessLicense(db)
const headers = await authedHeaders(app)
const calls = stubCloudAvatarFetch()
const form = new FormData()
form.set('file', makeFile('application/pdf'))
const res = await app.request('/api/users/me/avatar', { method: 'PUT', headers, body: form })
expect(res.status).toBe(400)
expect(calls).toHaveLength(0)
})
it('returns 413 when the file exceeds 1 MiB, before any Cloud call [spec: avatar/size-limit]', async () => {
const { app, db } = await createTestApp()
await seedBusinessLicense(db)
const headers = await authedHeaders(app)
const calls = stubCloudAvatarFetch()
const form = new FormData()
form.set('file', makeFile('image/png', 2 * 1024 * 1024))
const res = await app.request('/api/users/me/avatar', { method: 'PUT', headers, body: form })
expect(res.status).toBe(413)
expect(calls).toHaveLength(0)
})
it('returns 503 cloud_required when the instance is not paired to Cloud [spec: avatar/needs-cloud]', async () => {
const { app } = await createTestApp()
const headers = await authedHeaders(app)
const form = new FormData()
form.set('file', makeFile('image/png'))
const res = await app.request('/api/users/me/avatar', { method: 'PUT', headers, body: form })
expect(res.status).toBe(503)
})
it('hosts the avatar on Cloud, writes user.image, returns the URL [spec: avatar/upload]', async () => {
const { app, db } = await createTestApp()
await seedBusinessLicense(db)
const headers = await authedHeaders(app)
const calls = stubCloudAvatarFetch()
const form = new FormData()
form.set('file', makeFile('image/webp'))
const res = await app.request('/api/users/me/avatar', { method: 'PUT', headers, body: form })
expect(res.status).toBe(200)
const body = (await res.json()) as { url: string }
expect(body.url).toBe(CLOUD_AVATAR_URL)
const put = calls.find((c) => c.method === 'PUT')
expect(put?.url).toMatch(/\/avatars\/user\//)
expect(put?.contentType).toBe('image/webp')
expect(put?.authorization).toBe('Bearer test-refresh-token')
const rows = await db.all<{ image: string | null }>(sql`SELECT image FROM user LIMIT 1`)
expect(rows[0]?.image).toBe(CLOUD_AVATAR_URL)
})
})
describe('DELETE /api/users/me/avatar', () => {
beforeEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it('returns 401 without auth', async () => {
const { app } = await createTestApp()
const res = await app.request('/api/users/me/avatar', { method: 'DELETE' })
expect(res.status).toBe(401)
})
it('clears user.image and deletes the Cloud avatar [spec: avatar/delete]', async () => {
const { app, db } = await createTestApp()
await seedBusinessLicense(db)
const headers = await authedHeaders(app)
await db.run(sql`UPDATE user SET image = 'https://example.com/old.png'`)
const calls = stubCloudAvatarFetch()
const res = await app.request('/api/users/me/avatar', { method: 'DELETE', headers })
expect(res.status).toBe(204)
const rows = await db.all<{ image: string | null }>(sql`SELECT image FROM user LIMIT 1`)
expect(rows[0]?.image).toBeNull()
const del = calls.find((c) => c.method === 'DELETE')
expect(del?.url).toMatch(/\/avatars\/user\//)
})
it('succeeds when the instance is not paired to Cloud (DB cleared, Cloud delete skipped) [spec: avatar/delete-unbound]', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
await db.run(sql`UPDATE user SET image = 'https://example.com/old.png'`)
const calls = stubCloudAvatarFetch()
const res = await app.request('/api/users/me/avatar', { method: 'DELETE', headers })
expect(res.status).toBe(204)
const rows = await db.all<{ image: string | null }>(sql`SELECT image FROM user LIMIT 1`)
expect(rows[0]?.image).toBeNull()
expect(calls).toHaveLength(0)
})
})
// ─── Public user profile (GET /api/users/:username) ───────────────────────────
async function insertUser(
db: Awaited<ReturnType<typeof createTestApp>>['db'],
opts: { id: string; username: string; email: string },
) {
const now = Date.now()
await db.run(sql`
INSERT INTO user (id, name, email, email_verified, username, created_at, updated_at)
VALUES (${opts.id}, 'Test User', ${opts.email}, 1, ${opts.username}, ${now}, ${now})
`)
await db.run(sql`
INSERT INTO organization (id, name, slug, created_at)
VALUES (${`org-${opts.id}`}, 'Personal', ${`personal-${opts.id}`}, ${now})
`)
await db.run(sql`
INSERT INTO member (id, organization_id, user_id, role, created_at)
VALUES (${`member-${opts.id}`}, ${`org-${opts.id}`}, ${opts.id}, 'owner', ${now})
`)
return { orgId: `org-${opts.id}` }
}
type ProfileTestDb = Awaited<ReturnType<typeof createTestApp>>['db']
async function insertProfileMatter(
db: ProfileTestDb,
orgId: string,
id: string,
opts: {
name?: string
status?: string
dirtype?: number
trashedAt?: number | null
purgedAt?: number | null
} = {},
) {
const now = Math.floor(Date.now() / 1000)
const dirtype = opts.dirtype ?? 0
await db.run(sql`
INSERT INTO matters (
id, org_id, alias, name, type, size, dirtype, parent, object, storage_id,
status, trashed_at, purged_at, created_at, updated_at
)
VALUES (
${id}, ${orgId}, ${`${id}-alias`}, ${opts.name ?? `${id}.txt`},
${dirtype === 0 ? 'text/plain' : 'folder'}, ${dirtype === 0 ? 100 : 0}, ${dirtype},
'', ${dirtype === 0 ? `objects/${id}` : ''}, 'profile-storage',
${opts.status ?? 'active'}, ${opts.trashedAt ?? null}, ${opts.purgedAt ?? null}, ${now}, ${now}
)
`)
}
async function insertProfileShare(
db: ProfileTestDb,
creatorId: string,
orgId: string,
matterId: string,
opts: {
id?: string
token?: string
kind?: 'landing' | 'direct'
status?: 'active' | 'revoked'
private?: boolean
expiresAt?: number | null
downloadLimit?: number | null
downloads?: number
} = {},
) {
const now = Math.floor(Date.now() / 1000)
const id = opts.id ?? `share-${matterId}`
const token = opts.token ?? `token-${matterId}`
await db.run(sql`
INSERT INTO shares (
id, token, kind, matter_id, org_id, creator_id, password_hash, expires_at,
download_limit, views, downloads, status, private, created_at
)
VALUES (
${id}, ${token}, ${opts.kind ?? 'landing'}, ${matterId}, ${orgId}, ${creatorId},
NULL, ${opts.expiresAt ?? null}, ${opts.downloadLimit ?? null}, 0, ${opts.downloads ?? 0},
${opts.status ?? 'active'}, ${opts.private ? 1 : 0}, ${now}
)
`)
return { id, token }
}
describe('GET /api/users/:username', () => {
it('returns 404 when user does not exist [spec: profile/user-not-found]', async () => {
const { app } = await createTestApp()
const res = await app.request('/api/users/nonexistent')
expect(res.status).toBe(404)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('User not found')
})
it('returns user info and empty shares [spec: profile/user-info]', async () => {
const { app, db } = await createTestApp()
await insertUser(db, { id: 'user-1', username: 'testuser', email: 'test@example.com' })
const res = await app.request('/api/users/testuser')
expect(res.status).toBe(200)
const body = (await res.json()) as { user: { username: string }; shares: unknown[] }
expect(body.user.username).toBe('testuser')
expect(body.shares).toEqual([])
})
it('works without authentication [spec: profile/public]', async () => {
const { app, db } = await createTestApp()
await insertUser(db, { id: 'user-1', username: 'testuser', email: 'test@example.com' })
const res = await app.request('/api/users/testuser')
expect(res.status).toBe(200)
})
it('returns user info when user exists but has no personal org [spec: profile/no-personal-org]', async () => {
const { app, db } = await createTestApp()
const now = Date.now()
await db.run(sql`
INSERT INTO user (id, name, email, email_verified, username, created_at, updated_at)
VALUES ('user-2', 'Orphan User', 'orphan@example.com', 1, 'orphanuser', ${now}, ${now})
`)
const res = await app.request('/api/users/orphanuser')
expect(res.status).toBe(200)
const body = (await res.json()) as { user: { username: string }; shares: unknown[] }
expect(body.user.username).toBe('orphanuser')
expect(body.shares).toEqual([])
})
it('returns public landing shares and hides private ones without authentication [spec: profile/public-shares]', async () => {
const { app, db } = await createTestApp()
const { orgId } = await insertUser(db, {
id: 'curated-user',
username: 'curated',
email: 'curated@example.com',
})
await insertProfileMatter(db, orgId, 'curated-file', { name: 'Public.txt' })
await insertProfileMatter(db, orgId, 'curated-folder', { name: 'Photos', dirtype: 1 })
await insertProfileMatter(db, orgId, 'private-file', { name: 'Hidden.txt' })
await insertProfileShare(db, 'curated-user', orgId, 'curated-file', { token: 'public-file' })
await insertProfileShare(db, 'curated-user', orgId, 'curated-folder', { token: 'public-folder' })
await insertProfileShare(db, 'curated-user', orgId, 'private-file', {
token: 'hidden-file',
private: true,
})
const res = await app.request('/api/users/curated')
expect(res.status).toBe(200)
const body = (await res.json()) as {
shares: Array<{ token: string; name: string; type: string; size: number | null; isFolder: boolean }>
}
expect(body.shares).toEqual([
{ token: 'public-folder', name: 'Photos', type: 'folder', size: 0, isFolder: true },
{ token: 'public-file', name: 'Public.txt', type: 'text/plain', size: 100, isFolder: false },
])
})
it('never leaks forged listed direct or recipient-targeted shares [spec: profile/privacy-boundaries]', async () => {
const { app, db } = await createTestApp()
const { orgId } = await insertUser(db, {
id: 'privacy-user',
username: 'privacy',
email: 'privacy@example.com',
})
await insertProfileMatter(db, orgId, 'private-direct')
await insertProfileMatter(db, orgId, 'private-targeted')
const direct = await insertProfileShare(db, 'privacy-user', orgId, 'private-direct', { kind: 'direct' })
const targeted = await insertProfileShare(db, 'privacy-user', orgId, 'private-targeted')
await db.run(sql`
INSERT INTO share_recipients (id, share_id, recipient_email, created_at)
VALUES ('private-recipient', ${targeted.id}, 'recipient@example.com', ${Math.floor(Date.now() / 1000)})
`)
const res = await app.request('/api/users/privacy')
expect(res.status).toBe(200)
expect(((await res.json()) as { shares: unknown[] }).shares).toEqual([])
expect(direct.token).toBeTruthy()
})
it('filters every unavailable selected share at read time [spec: profile/availability-filtering]', async () => {
const { app, db } = await createTestApp()
const { orgId } = await insertUser(db, {
id: 'availability-user',
username: 'availability',
email: 'availability@example.com',
})
const now = Math.floor(Date.now() / 1000)
const matters = [
['available', {}],
['revoked', {}],
['expired', {}],
['exhausted', {}],
['trashed', { trashedAt: now }],
['purged', { purgedAt: now }],
['draft', { status: 'draft' }],
] as const
for (const [id, options] of matters) await insertProfileMatter(db, orgId, id, options)
await insertProfileShare(db, 'availability-user', orgId, 'available', { token: 'only-available' })
await insertProfileShare(db, 'availability-user', orgId, 'revoked', { status: 'revoked' })
await insertProfileShare(db, 'availability-user', orgId, 'expired', { expiresAt: now - 1 })
await insertProfileShare(db, 'availability-user', orgId, 'exhausted', {
downloadLimit: 2,
downloads: 2,
})
await insertProfileShare(db, 'availability-user', orgId, 'trashed')
await insertProfileShare(db, 'availability-user', orgId, 'purged')
await insertProfileShare(db, 'availability-user', orgId, 'draft')
await insertProfileShare(db, 'availability-user', orgId, 'missing-target', { token: 'missing-target' })
const res = await app.request('/api/users/availability')
expect(res.status).toBe(200)
expect(((await res.json()) as { shares: Array<{ token: string }> }).shares.map((share) => share.token)).toEqual([
'only-available',
])
})
})