Files
zpan/workers/bootstrap.cf-test.ts
T
Jasper Van 66b3ee3435 feat: public share landing page /s/:token + Workers SSR OG meta (#312)
* feat: public share landing page /s/:token + Workers SSR OG meta

- Add SPA route `/s/:token` (TanStack Router, outside _authenticated)
- Implement share components: ShareLanding, FilePreview, FolderBrowser,
  PasswordPrompt, SaveToDriveDialog, ShareError
- File preview: image/video/audio/PDF via object URL fetch; fallback for
  other types with download CTA
- Folder browser: breadcrumb navigation + children table with download
- Password gate: POST /api/share/:token/verify with error feedback
- Save to drive: workspace + folder picker, quota/password/gone error handling
- Workers SSR: inject OG meta tags for /s/:token requests (title, description,
  image, twitter:card); fetch share metadata via /api/share/:token
- Add /s/* to wrangler.toml run_worker_first for SSR routing
- Add zValidator to /:token/children endpoint for typed RPC query params
- Export ShareApiRoute type from server/app.ts; add RPC clients in rpc.ts
- Add share.* i18n keys (en + zh)
- 9 new unit tests covering error code derivation, escaping, i18n coverage

Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c

* test: add coverage for share public API wrappers and path traversal guard

- api.test.ts: add unit tests for getShareLanding, verifySharePassword,
  getShareChildren, saveShareToDrive (success + all error paths)
- share-public.integration.test.ts: add path traversal guard test
  (.. in path param returns 400 Invalid path)

Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c

* test: cover explicit page/pageSize params in children endpoint

Add integration test for GET /api/share/:token/children with explicit
page and pageSize query params to satisfy codecov/patch branch coverage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: add error path coverage for children endpoint

Cover invalid token (404), trashed matter (410), and non-numeric
page/pageSize (NaN fallback) in GET /:token/children to satisfy
codecov/patch threshold requirements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add ASSETS binding to wrangler.toml for Workers SSR

Without binding = "ASSETS", env.ASSETS is undefined at runtime
and the /s/:token SSR handler throws error code 1101.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve CF SSR OG meta by calling service layer directly instead of self-subrequest

Cloudflare Workers cannot fetch() their own origin when the path is listed
in run_worker_first — the request loops back and returns a 500 error code 1101.
Replace the HTTP subrequest in fetchShareMeta with a direct call to
resolveShareByToken(platform.db, token) from the service layer.

Add CF integration tests asserting that a valid landing share produces real
og:title metadata and an unknown token falls back gracefully.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 13:16:20 -04:00

72 lines
3.1 KiB
TypeScript

import { env } from 'cloudflare:workers'
import { describe, expect, it } from 'vitest'
import worker from './bootstrap'
const testEnv = { ...env, BETTER_AUTH_SECRET: env.BETTER_AUTH_SECRET || 'ci-test-secret-that-is-at-least-32-chars' }
const fakeSpaHtml = '<html><head><title>ZPan</title></head><body></body></html>'
const fakeAssets = {
fetch: (_req: RequestInfo | Request) =>
Promise.resolve(new Response(fakeSpaHtml, { status: 200, headers: { 'Content-Type': 'text/html' } })),
} as unknown as Fetcher
describe('[CF] Worker fetch handler', () => {
it('throws when BETTER_AUTH_SECRET is missing', async () => {
const request = new Request('http://localhost/api/health')
const envWithoutSecret = { ...env, BETTER_AUTH_SECRET: '' }
await expect(worker.fetch(request, envWithoutSecret)).rejects.toThrow(
'BETTER_AUTH_SECRET is not configured for this deployment.',
)
})
it('returns a response for a valid request', async () => {
const request = new Request('http://localhost/api/health')
const res = await worker.fetch(request, testEnv)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ status: 'ok' })
})
it('splits and trims TRUSTED_ORIGINS when provided', async () => {
const request = new Request('http://localhost/api/health')
const envWithOrigins = { ...testEnv, TRUSTED_ORIGINS: ' https://a.example.com , https://b.example.com ' }
const res = await worker.fetch(request, envWithOrigins)
expect(res.status).toBe(200)
})
})
describe('[CF] SSR share OG meta injection', () => {
it('injects real file name into og:title for valid landing share', async () => {
const now = Date.now()
await env.DB.prepare(
`INSERT INTO matters (id, org_id, alias, name, type, size, dirtype, parent, object, storage_id, status, created_at, updated_at)
VALUES ('ssr-matter-1', 'org-1', 'ssr-alias-1', 'design-spec.pdf', 'application/pdf', 4096, 0, '', 'obj/key.pdf', 'st-1', 'active', ?, ?)`,
)
.bind(now, now)
.run()
await env.DB.prepare(
`INSERT INTO shares (id, token, kind, matter_id, org_id, creator_id, password_hash, expires_at, download_limit, views, downloads, status, created_at)
VALUES ('ssr-share-1', 'ssrtoken01', 'landing', 'ssr-matter-1', 'org-1', 'user-1', NULL, NULL, NULL, 0, 0, 'active', ?)`,
)
.bind(now)
.run()
const testEnvWithAssets = { ...testEnv, ASSETS: fakeAssets }
const res = await worker.fetch(new Request('http://localhost/s/ssrtoken01'), testEnvWithAssets)
expect(res.status).toBe(200)
const html = await res.text()
expect(html).toContain('<meta property="og:title" content="design-spec.pdf"')
expect(html).not.toContain('Share unavailable')
})
it('returns fallback OG meta for unknown share token', async () => {
const testEnvWithAssets = { ...testEnv, ASSETS: fakeAssets }
const res = await worker.fetch(new Request('http://localhost/s/no-such-token'), testEnvWithAssets)
expect(res.status).toBe(200)
const html = await res.text()
expect(html).toContain('<meta property="og:title" content="Share unavailable"')
})
})