Files
zpan/server/http/redirect.cf-test.ts
Jasper Van 17db813d1c feat(ids)!: normalize persistent identifiers to Base62 (#554)
* feat(ids)!: normalize persistent identifiers to Base62

BREAKING CHANGE: historical ZPan-owned IDs and public tokens require the one-time normalization migration; old public links and stored credentials are invalidated without runtime fallback.

* fix(ids): namespace public redirect tokens

Generate share tokens as s plus 11 Base62 characters and image tokens as i plus 11 Base62 characters. Dispatch /r by namespace prefix, reuse the single resolved record, and update migration validation, contracts, documentation, and tests.

* fix(ids): guard future object storage keys

* fix(ids): normalize historical references safely
2026-08-05 00:36:18 -04:00

174 lines
7.5 KiB
TypeScript

import { env } from 'cloudflare:workers'
import { sql } from 'drizzle-orm'
import { describe, expect, it, vi } from 'vitest'
import { generateImageToken } from '../../shared/ids'
import { S3Service } from '../adapters/gateways/s3'
import { createShareRepo } from '../adapters/repos/share'
import { createApp } from '../app'
import { createAuth } from '../auth'
import { createCloudflarePlatform } from '../platform/cloudflare'
const STORAGE_ID = 'st-cf-redirect'
const MOCK_PRESIGN_URL = 'https://presigned-cf.example.com/file'
const MOCK_INLINE_URL = 'https://presigned-inline-cf.example.com/image.png'
async function buildApp() {
const platform = createCloudflarePlatform(env)
const auth = await createAuth(platform.db, env.BETTER_AUTH_SECRET, 'http://localhost')
return { app: createApp(platform, auth), db: platform.db }
}
async function signUpAndGetIds(app: ReturnType<typeof createApp>, db: Awaited<ReturnType<typeof buildApp>>['db']) {
const email = `cf-redirect-${Date.now()}@example.com`
const signUpRes = await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'CF Redirect Test', email, password: 'password123456' }),
})
const cookies = signUpRes.headers.getSetCookie().join('; ')
const orgRows = await db.all<{ id: string }>(
sql`SELECT id FROM organization WHERE metadata LIKE '%"type":"personal"%' ORDER BY created_at DESC LIMIT 1`,
)
const userRows = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = ${email} LIMIT 1`)
return { headers: { Cookie: cookies }, orgId: orgRows[0].id, userId: userRows[0].id }
}
async function insertStorage(db: Awaited<ReturnType<typeof buildApp>>['db']) {
const now = Date.now()
await db.run(sql`
INSERT OR IGNORE INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
VALUES (${STORAGE_ID}, 'cf-bucket', 'https://s3.amazonaws.com', 'us-east-1', 'AK', 'SK', '', '', 0, 0, 'active', ${now}, ${now})
`)
}
async function insertFile(
db: Awaited<ReturnType<typeof buildApp>>['db'],
orgId: string,
opts: { id: string; name: string },
) {
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 (${opts.id}, ${orgId}, ${`${opts.id}-cf-alias`}, ${opts.name}, 'image/png', 512, 0, '', 'keys/file.png', ${STORAGE_ID}, 'active', ${now}, ${now})
`)
}
async function insertImageHosting(
db: Awaited<ReturnType<typeof buildApp>>['db'],
orgId: string,
opts: { id: string; token: string },
) {
const now = Date.now()
await db.run(sql`
INSERT INTO image_hostings (id, org_id, token, path, storage_id, storage_key, size, mime, status, access_count, created_at)
VALUES (${opts.id}, ${orgId}, ${opts.token}, ${`blog/${opts.id}.png`}, ${STORAGE_ID}, ${`ih/${orgId}/${opts.id}.png`}, 512, 'image/png', 'active', 0, ${now})
`)
}
// ─── CF routing guard ─────────────────────────────────────────────────────────
describe('[CF] /r/* routing — Worker handles these paths', () => {
it('/r/:token with unknown prefix returns 404 (Worker-handled, not CF Assets)', async () => {
const { app } = await buildApp()
const res = await app.request('/r/unknownprefix_token')
expect(res.status).toBe(404)
// Should be JSON (Worker response), not HTML (CF Assets SPA)
const ct = res.headers.get('content-type') ?? ''
expect(ct).toContain('application/json')
})
it('/dl/* is NOT routed (returns 404 — old path removed)', async () => {
const { app } = await buildApp()
const res = await app.request('/dl/any-token')
expect(res.status).toBe(404)
})
})
// ─── CF direct share tests ────────────────────────────────────────────────────
describe('[CF] /r/:token direct shares', () => {
it('returns 302 for valid direct share', async () => {
vi.spyOn(S3Service.prototype, 'presignDownload').mockResolvedValue(MOCK_PRESIGN_URL)
const { app, db } = await buildApp()
const { orgId, userId } = await signUpAndGetIds(app, db)
await insertStorage(db)
await insertFile(db, orgId, { id: `cf-ds-${Date.now()}`, name: 'cf-direct.bin' })
const rows = await db.all<{ id: string }>(sql`SELECT id FROM matters WHERE name = 'cf-direct.bin' LIMIT 1`)
const matterId = rows[0].id
const share = await createShareRepo(db).create({ matterId, orgId, creatorId: userId, kind: 'direct' })
const res = await app.request(`/r/${share.token}`, { redirect: 'manual' })
expect(res.status).toBe(302)
expect(res.headers.get('cache-control')).toContain('no-store')
vi.restoreAllMocks()
})
})
// ─── CF image hosting token tests ─────────────────────────────────────────────
describe('[CF] /r/:token image hosting', () => {
it('returns 302 for a new active image-hosting token without an underscore', async () => {
vi.spyOn(S3Service.prototype, 'presignInline').mockResolvedValue(MOCK_INLINE_URL)
const { app, db } = await buildApp()
const { orgId } = await signUpAndGetIds(app, db)
await insertStorage(db)
const token = generateImageToken()
await insertImageHosting(db, orgId, { id: `cfih${Date.now()}`, token })
const res = await app.request(`/r/${token}`, { redirect: 'manual' })
expect(res.status).toBe(302)
const cc = res.headers.get('cache-control') ?? ''
expect(cc).toContain('no-store')
vi.restoreAllMocks()
})
it('strips an extension from a Base62 image token and resolves correctly', async () => {
vi.spyOn(S3Service.prototype, 'presignInline').mockResolvedValue(MOCK_INLINE_URL)
const { app, db } = await buildApp()
const { orgId } = await signUpAndGetIds(app, db)
await insertStorage(db)
const token = generateImageToken()
await insertImageHosting(db, orgId, { id: `cfihext${Date.now()}`, token })
const res = await app.request(`/r/${token}.jpg`, { redirect: 'manual' })
expect(res.status).toBe(302)
vi.restoreAllMocks()
})
})
// ─── CF concurrent downloads — atomic limit enforcement ───────────────────────
describe('[CF] Concurrent downloads — atomic limit enforcement via /r/', () => {
it('with limit=5 and 20 concurrent requests, exactly 5 succeed', async () => {
const { app, db } = await buildApp()
const { orgId, userId } = await signUpAndGetIds(app, db)
await insertStorage(db)
const fileId = `cf-dlc-r-${Date.now()}`
await insertFile(db, orgId, { id: fileId, name: 'concurrent-r.bin' })
const share = await createShareRepo(db).create({
matterId: fileId,
orgId,
creatorId: userId,
kind: 'direct',
downloadLimit: 5,
})
// Fire 20 concurrent requests
const results = await Promise.all(
Array.from({ length: 20 }, () => app.request(`/r/${share.token}`, { redirect: 'manual' }).then((r) => r.status)),
)
// presignDownload will throw in CF test env (no real S3), so successful requests
// (that passed the limit gate) may get 500. Count 302+500 as "passed the gate".
const passed = results.filter((s) => s !== 410).length
const rejected = results.filter((s) => s === 410).length
expect(passed).toBe(5)
expect(rejected).toBe(15)
})
})