mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-19 01:51:11 +08:00
275 lines
14 KiB
TypeScript
275 lines
14 KiB
TypeScript
import { sql } from 'drizzle-orm'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { authedHeaders, createTestApp } 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('; ') }
|
|
}
|
|
|
|
describe('Admin Quotas API', () => {
|
|
it('returns 401 without auth [spec: quotas/admin-auth-required]', async () => {
|
|
const { app } = await createTestApp()
|
|
const res = await app.request('/api/quotas')
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('returns 403 for non-admin [spec: quotas/admin-only]', async () => {
|
|
const { app } = await createTestApp()
|
|
await authedHeaders(app, 'admin@example.com')
|
|
await authedHeaders(app, 'regular@example.com')
|
|
const signInRes = await app.request('/api/auth/sign-in/email', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: 'regular@example.com', password: 'password123456' }),
|
|
})
|
|
const freshHeaders = { Cookie: signInRes.headers.getSetCookie().join('; ') }
|
|
const res = await app.request('/api/quotas', { headers: freshHeaders })
|
|
expect(res.status).toBe(403)
|
|
})
|
|
|
|
it('GET /api/quotas returns the default quota row created at signup [spec: quotas/default-row]', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
const res = await app.request('/api/quotas', { headers })
|
|
expect(res.status).toBe(200)
|
|
const body = (await res.json()) as { items: Array<Record<string, unknown>>; total: number }
|
|
expect(body.items).toHaveLength(1)
|
|
expect(body.total).toBe(1)
|
|
expect(body.items[0].quota).toBe(10485760)
|
|
expect(body.items[0].trafficQuota).toBe(0)
|
|
expect(body.items[0].trafficUsed).toBe(0)
|
|
expect(body.items[0].trafficPeriod).toMatch(/^\d{4}-\d{2}$/)
|
|
})
|
|
|
|
it('GET /api/quotas normalizes stale monthly traffic period in the response without writing [spec: quotas/normalizes-stale-period]', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await db.run(sql`UPDATE org_quotas SET traffic_quota = 1000, traffic_used = 900, traffic_period = '1970-01'`)
|
|
|
|
const res = await app.request('/api/quotas', { headers })
|
|
expect(res.status).toBe(200)
|
|
const body = (await res.json()) as { items: Array<Record<string, unknown>>; total: number }
|
|
expect(body.items[0].trafficUsed).toBe(0)
|
|
expect(body.items[0].trafficPeriod).toMatch(/^\d{4}-\d{2}$/)
|
|
|
|
// The listing is a pure read: it normalizes the stale period in the response
|
|
// but must not mutate the row. Persisting the reset is the cron's job.
|
|
const rows = await db.all<{ trafficUsed: number; trafficPeriod: string }>(
|
|
sql`SELECT traffic_used AS trafficUsed, traffic_period AS trafficPeriod FROM org_quotas LIMIT 1`,
|
|
)
|
|
expect(rows[0].trafficUsed).toBe(900)
|
|
expect(rows[0].trafficPeriod).toBe('1970-01')
|
|
})
|
|
|
|
it('GET /api/quotas lists quotas with org info [spec: quotas/list-with-org]', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
|
|
const orgs = await db.all<{ id: string }>(
|
|
sql`SELECT o.id FROM organization o WHERE o.metadata LIKE '%"type":"personal"%' LIMIT 1`,
|
|
)
|
|
const orgId = orgs[0].id
|
|
|
|
const res = await app.request('/api/quotas', { headers })
|
|
expect(res.status).toBe(200)
|
|
const body = (await res.json()) as { items: Array<Record<string, unknown>>; total: number }
|
|
expect(body.items).toHaveLength(1)
|
|
expect(body.items[0].orgId).toBe(orgId)
|
|
expect(body.items[0].quota).toBe(10485760)
|
|
expect(body.items[0].trafficQuota).toBe(0)
|
|
expect(body.items[0].trafficUsed).toBe(0)
|
|
expect(body.items[0].trafficPeriod).toMatch(/^\d{4}-\d{2}$/)
|
|
expect(body.items[0].orgName).toBeTruthy()
|
|
expect(body.items[0].orgType).toBe('personal')
|
|
})
|
|
|
|
it('GET /api/quotas lists effective quota with active entitlements [spec: quotas/effective-with-entitlements]', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
const orgs = await db.all<{ id: string }>(
|
|
sql`SELECT o.id FROM organization o WHERE o.metadata LIKE '%"type":"personal"%' LIMIT 1`,
|
|
)
|
|
const orgId = orgs[0].id
|
|
const now = Date.now()
|
|
await db.run(sql`UPDATE org_quotas SET quota = 0, traffic_quota = 0 WHERE org_id = ${orgId}`)
|
|
await db.run(sql`DELETE FROM org_quota_entitlements WHERE org_id = ${orgId}`)
|
|
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-admin-storage-plan', ${orgId}, 'storage', 'plan', 'test', 'admin-storage-plan', 5000, ${now}, NULL, 'active', NULL, ${now}, ${now}),
|
|
('ent-admin-storage', ${orgId}, 'storage', 'grant', 'test', 'admin-storage', 3000, ${now}, NULL, 'active', NULL, ${now}, ${now}),
|
|
('ent-admin-traffic-plan', ${orgId}, 'traffic', 'plan', 'test', 'admin-traffic-plan', 1000, ${now}, NULL, 'active', NULL, ${now}, ${now}),
|
|
('ent-admin-traffic', ${orgId}, 'traffic', 'grant', 'test', 'admin-traffic', 2000, ${now}, NULL, 'active', NULL, ${now}, ${now}),
|
|
('ent-admin-revoked', ${orgId}, 'storage', 'grant', 'test', 'admin-revoked', 9000, ${now}, NULL, 'revoked', NULL, ${now}, ${now})
|
|
`)
|
|
|
|
const res = await app.request('/api/quotas', { headers })
|
|
|
|
expect(res.status).toBe(200)
|
|
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
|
|
expect(body.items[0]).toMatchObject({
|
|
baseQuota: 5000,
|
|
entitlementQuota: 3000,
|
|
quota: 8000,
|
|
baseTrafficQuota: 1000,
|
|
entitlementTrafficQuota: 2000,
|
|
trafficQuota: 3000,
|
|
})
|
|
})
|
|
|
|
it('GET /api/quotas exposes active plan and extra quota labels [spec: quotas/plan-labels]', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
const orgs = await db.all<{ id: string }>(
|
|
sql`SELECT o.id FROM organization o WHERE o.metadata LIKE '%"type":"personal"%' LIMIT 1`,
|
|
)
|
|
const orgId = orgs[0].id
|
|
const now = Date.now()
|
|
await db.run(sql`UPDATE org_quotas SET quota = 0, traffic_quota = 0 WHERE org_id = ${orgId}`)
|
|
await db.run(sql`DELETE FROM org_quota_entitlements WHERE org_id = ${orgId}`)
|
|
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-admin-plan-storage', ${orgId}, 'storage', 'plan', 'test', ${`stripe_subscription:sub_storage:${orgId}`}, 3000, ${now}, NULL, 'active', '{"packageName":"Team Plan"}', ${now}, ${now}),
|
|
('ent-admin-plan-storage-old', ${orgId}, 'storage', 'plan', 'test', ${`stripe_subscription:sub_storage_old:${orgId}`}, 2500, ${now}, NULL, 'revoked', '{"packageName":"Old Team Plan"}', ${now}, ${now}),
|
|
('ent-admin-extra-storage', ${orgId}, 'storage', 'grant', 'test', 'storage-pack', 700, ${now}, NULL, 'active', '{"packageName":"Storage Pack"}', ${now}, ${now}),
|
|
('ent-admin-plan-traffic', ${orgId}, 'traffic', 'plan', 'test', ${`stripe_subscription:sub_traffic:${orgId}`}, 4000, ${now}, NULL, 'active', '{"packageName":"Team Plan"}', ${now}, ${now}),
|
|
('ent-admin-extra-traffic', ${orgId}, 'traffic', 'grant', 'test', 'traffic-pack', 900, ${now}, NULL, 'active', '{"packageName":"Traffic Boost"}', ${now}, ${now})
|
|
`)
|
|
|
|
const res = await app.request('/api/quotas', { headers })
|
|
|
|
expect(res.status).toBe(200)
|
|
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
|
|
expect(body.items[0]).toMatchObject({
|
|
baseQuota: 3000,
|
|
entitlementQuota: 700,
|
|
quota: 3700,
|
|
baseTrafficQuota: 4000,
|
|
entitlementTrafficQuota: 900,
|
|
trafficQuota: 4900,
|
|
storagePlanName: 'Team Plan',
|
|
storageExtraNames: ['Storage Pack'],
|
|
trafficPlanName: 'Team Plan',
|
|
trafficExtraNames: ['Traffic Boost'],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('User Quotas API — /api/quotas', () => {
|
|
it('GET /api/quotas/me returns 401 without auth [spec: quotas/me-auth-required]', async () => {
|
|
const { app } = await createTestApp()
|
|
const res = await app.request('/api/quotas/me')
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('GET /api/quotas/me returns the built-in default quota of 10MB when no system option is set [spec: quotas/me-default]', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await authedHeaders(app)
|
|
const res = await app.request('/api/quotas/me', { headers })
|
|
expect(res.status).toBe(200)
|
|
const body = (await res.json()) as Record<string, unknown>
|
|
expect(body.quota).toBe(10485760)
|
|
expect(body.used).toBe(0)
|
|
expect(body.trafficQuota).toBe(0)
|
|
expect(body.trafficUsed).toBe(0)
|
|
expect(body.trafficPeriod).toMatch(/^\d{4}-\d{2}$/)
|
|
expect(body.orgId).toBeTruthy()
|
|
})
|
|
|
|
it('GET /api/quotas/me rejects an API key because the route requires a user session [spec: quotas/me-no-org]', async () => {
|
|
const { app, auth, db } = await createTestApp()
|
|
await authedHeaders(app, 'noorg@example.com')
|
|
const [user] = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'noorg@example.com'`)
|
|
// biome-ignore lint/suspicious/noExplicitAny: better-auth plugin API not fully typed
|
|
const apiKey = (await (auth.api as any).createApiKey({
|
|
body: { configId: 'webdav', userId: user.id },
|
|
})) as { key: string }
|
|
await db.run(sql`DELETE FROM member WHERE user_id = ${user.id}`)
|
|
|
|
const res = await app.request('/api/quotas/me', {
|
|
headers: { Authorization: `Bearer ${apiKey.key}` },
|
|
})
|
|
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('GET /api/quotas/me returns base quota plus active entitlements and labels [spec: quotas/me-effective]', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const adminH = await adminHeaders(app)
|
|
const orgs = await db.all<{ id: string }>(
|
|
sql`SELECT o.id FROM organization o WHERE o.metadata LIKE '%"type":"personal"%' LIMIT 1`,
|
|
)
|
|
const orgId = orgs[0].id
|
|
const now = Date.now()
|
|
await db.run(sql`UPDATE org_quotas SET quota = 0, traffic_quota = 0 WHERE org_id = ${orgId}`)
|
|
await db.run(sql`DELETE FROM org_quota_entitlements WHERE org_id = ${orgId}`)
|
|
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-user-storage-plan', ${orgId}, 'storage', 'plan', 'test', 'user-storage-plan', 1000, ${now}, NULL, 'active', '{"packageName":"Free"}', ${now}, ${now}),
|
|
('ent-user-storage', ${orgId}, 'storage', 'grant', 'test', 'user-storage', 4000, ${now}, NULL, 'active', '{"packageName":"Storage Pack"}', ${now}, ${now}),
|
|
('ent-user-traffic-plan', ${orgId}, 'traffic', 'plan', 'test', 'user-traffic-plan', 2000, ${now}, NULL, 'active', '{"packageName":"Free"}', ${now}, ${now}),
|
|
('ent-user-traffic', ${orgId}, 'traffic', 'grant', 'test', 'user-traffic', 6000, ${now}, NULL, 'active', '{"packageName":"Traffic Boost"}', ${now}, ${now})
|
|
`)
|
|
|
|
const res = await app.request('/api/quotas/me', { headers: adminH })
|
|
|
|
expect(res.status).toBe(200)
|
|
const body = (await res.json()) as Record<string, unknown>
|
|
expect(body).toMatchObject({
|
|
baseQuota: 1000,
|
|
entitlementQuota: 4000,
|
|
quota: 5000,
|
|
baseTrafficQuota: 2000,
|
|
entitlementTrafficQuota: 6000,
|
|
trafficQuota: 8000,
|
|
storagePlanName: 'Free',
|
|
storageExtraNames: ['Storage Pack'],
|
|
trafficPlanName: 'Free',
|
|
trafficExtraNames: ['Traffic Boost'],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Admin quota listing', () => {
|
|
it('normalizes stale traffic periods without historical grant aggregation', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
const now = Date.now()
|
|
const adminOrg = await db.all<{ id: string }>(sql`SELECT id FROM organization LIMIT 1`)
|
|
|
|
await db.run(sql`
|
|
INSERT INTO organization (id, name, slug, metadata, created_at, updated_at)
|
|
VALUES ('team-quota-listing', 'Team Quota Listing', 'team-quota-listing', '{"type":"team"}', ${now}, ${now})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO org_quotas (id, org_id, quota, used, traffic_quota, traffic_used, traffic_period)
|
|
VALUES ('team-quota-listing-row', 'team-quota-listing', 2000, 100, 3000, 2500, '1970-01')
|
|
`)
|
|
await db.run(sql`UPDATE org_quotas SET traffic_quota = 1000, traffic_used = 900, traffic_period = '1970-01'`)
|
|
const res = await app.request('/api/quotas', { headers })
|
|
expect(res.status).toBe(200)
|
|
const body = (await res.json()) as { items: Array<Record<string, unknown>>; total: number }
|
|
|
|
expect(body.total).toBe(2)
|
|
expect(body.items).toHaveLength(2)
|
|
expect(body.items.every((item) => item.trafficUsed === 0)).toBe(true)
|
|
expect(body.items.every((item) => /^\d{4}-\d{2}$/.test(String(item.trafficPeriod)))).toBe(true)
|
|
|
|
const adminItem = body.items.find((item) => item.orgId === adminOrg[0].id)
|
|
expect(adminItem).toMatchObject({ baseQuota: 10485760, quota: 10485760 })
|
|
})
|
|
})
|