Commit Graph
22 Commits
Author SHA1 Message Date
Jasper VanandClaude Opus 4.8 705aa67a2d refactor(server): usecase-per-resource — move all handler logic into usecases, lock the http boundary (#435)
* refactor(server): enforce http→usecase boundary + extract storages usecase

Adds an AST-based lint (scripts/lint-http-boundary.ts, `pnpm lint:http`, wired
into CI) that forbids http handlers from reaching into deps ports directly
(`c.get('deps').<port>.<method>()`) — the runtime signal of business logic
leaking into the delivery layer, which dependency-cruiser's import-graph rules
cannot see. It ships with a migration ratchet of the 30 handlers that still
violate: CI fails on any new violation and on any ratcheted file that has become
clean, so the list only shrinks. When empty, the boundary is locked.

Converts storages as the first usecase-per-resource example:
- usecases/storage.ts owns all storage business rules (Community storage limit,
  egress-credit-billing feature gate, activity logging)
- http/storages.ts is now thin: validate → call usecase → serialize
- usecases/storage.test.ts exhausts the branches with fake ports (14 cases)
- storages removed from the ratchet (29 remain)

Behavior preserved: storages.integration.test.ts (24) unchanged and green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract profile + notification resource usecases

Converts two owner/public single-port resources to the usecase-per-resource
convention (handlers now only validate → call usecase → serialize):
- usecases/profile.ts (getPublicProfile) + usecases/notification.ts
  (list/unreadCount/markRead/markAllRead), each with fake-port unit tests
- http/profile.ts, http/notifications.ts no longer touch deps ports
- ratchet: 29 → 27

Behavior preserved: profile + notifications integration suites (23) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract audit + quota + announcement resource usecases

- usecases/audit.ts (listAuditEvents)
- usecases/quota.ts (listQuotaOverview with org-type parsing, getUserQuota with
  personal-org fallback)
- usecases/announcement.ts (user/admin list + CRUD)
Handlers keep only pure input parsing (pagination clamp) + serialization; no
deps-port access. Each usecase has fake-port unit tests (12 cases).
ratchet: 27 → 24

Behavior preserved: audit/quotas/announcements integration suites (65) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract auth-provider/background-job/email-config/invite-code/site-invitation/user usecases

Wave of six independent resources converted to usecase-per-resource (parallel
subagents, centrally verified). Each: new usecases/<resource>.ts holding all
port access + business rules, a thinned handler (validate → call usecase →
serialize, no deps-port access), and fake-port unit tests.

- auth-provider.ts: provider config list/upsert/delete; OIDC validation +
  social-login free-limit gate as outcome unions
- background-job.ts: list/get/cancel/create/retry; keeps port-thrown
  BackgroundJobError mapping
- email-config.ts: masked get / save rows / send-test (send_failed outcome)
- invite-code.ts: list/validate/generate(expiry policy)/delete outcome union
- site-invitation.ts: create/resend/revoke/getByToken; email-before-write
  ordering preserved
- user.ts: admin user status/delete + entitlement CRUD; repo-chosen failure
  statuses threaded through unchanged
ratchet: 24 → 18

Verified: typecheck, lint:http, lint:arch, biome all clean; 90 new unit tests +
124 existing integration tests green (behavior preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract ihost/team/branding/me/trash/system resource usecases

Second parallel wave (centrally verified). Handlers thinned to validate → call
usecase → serialize; all port access + business rules moved into usecases.

- image-hosting.ts (extended) + image-hosting-config.ts: ihost upload/list/delete
  + config CRUD with CF custom-hostname lifecycle; quota→422 preserved
- team.ts: /api/teams + /api/admin/teams (invite links, join, activity feed,
  org logo, admin quota entitlements); role checks + repo-failure threading
- branding.ts (extended): admin write orchestration (logo/favicon upload,
  theme, single audit event) + reset; white_label gating stays in middleware
- me.ts: avatar upload/delete (gateway status passthrough, DB-first delete)
- trash.ts: empty-trash (reuses purge.ts; trash_empty audit only when >0)
- system.ts: instance info, changelog, system-options CRUD (signup/captcha/quota
  validation ordering preserved)

Also removed dead code: the speculative team.ts createTeamGate (the team-create
limit is enforced in auth.ts via licensing.checkTeamLimit; nothing called it).
ratchet: 18 → 10

Verified: typecheck, lint:http, lint:arch, biome clean; 148 new unit tests +
228 integration tests green (behavior preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract cloud-store + licensing-admin + events usecases

Third parallel wave (centrally verified + test fixups).

- cloud-store.ts: storefront reads, checkout/orders, and webhook delivery
  (cloud event token verification + idempotency); binding gate as outcome union
- licensing.ts (extended, admin section): initiatePairing / pollPairing (cert
  verify + rollback) / triggerRefresh / unbindLicense
- events.ts: the multiplexed SSE stream as a (deps, params, signal, emit)
  usecase; the handler owns the ReadableStream/Response and feeds ONE
  AbortController from both teardown paths (request abort + body cancel)

Streaming fix: guard the stream controller so a consumer cancel() — which
already closes it before firing the abort listener — no longer double-closes
(ERR_INVALID_STATE), eliminating the unhandled errors in the events suite.

Test fixups (behavior was correct, verified by integration): cloud-store fake
rebuilt the bound client per request and reset its response queue (singleton
now); licensing-admin unit test forced onto the node env (paseto-ts needs a
real TextEncoder).
ratchet: 10 → 5

Verified: typecheck, lint:http, lint:arch, biome clean; 61 unit + 77 integration
tests green (behavior preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract download-traffic metering into a usecase

traffic-metering-utils.ts was an http helper that read deps off the request and
ran the quota+egress download meter inline. Moves the decision into
cloud-traffic-metering.ts as meterDownloadTraffic / reportDownloadEgress
(deps-first, returning a plain {ok|quota_exceeded|insufficient_credits}
outcome). The http helper stays as a thin Context adapter that resolves the
cloud base URL, calls the usecase (deps passed whole), and renders the 422/402
responses — so its four consumers (shares, objects, redirect, webdav) are
unchanged and the file is now boundary-clean.
ratchet: 5 → 4

Verified: typecheck, lint:http, lint:arch, biome clean; cloud-traffic-metering
unit (13, incl. 3 new download tests) + 104 consumer integration tests
(redirect/objects-quota/share-public/cloud-traffic-metering) green — download
metering behavior preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract redirect + share + object resource usecases

The download-flow consumers. Each download orchestration (resolve → access/
expiry/limit gates → atomic increment → meter → presign → audit) moved into its
resource usecase, which calls meterDownloadTraffic/reportDownloadEgress(deps, …)
directly; the handler computes cloudBaseUrl, manages cookies, and renders the
route-specific 302/JSON/410/422/402 responses from the returned outcome.

- usecases/redirect.ts: /r/:token (ds_ direct share + ih_ image hosting), with
  refer-allowlist + presign-rollback
- usecases/share.ts: public + authed share routes; cookies become usecase
  *decisions* the handler applies (view-dedup, password session); imports
  save-to-drive + share-notification unchanged
- usecases/object.ts: upload sessions, confirm, list/move/trash/restore/delete,
  copy/transfer, download; keeps ObjectUploadSessionError; consolidated two
  identical write-access middlewares
- usecases/share-ref.ts: pure share-token helpers (HMAC ref codec, breadcrumb,
  access gate, presign TTL) moved out of http/share-utils so usecases can import
  them without reaching into http; share-utils re-exports them for handlers
ratchet: 4 → 1 (only webdav remains)

Verified: typecheck, lint:http, lint:arch, biome clean; 152 new unit + 206
integration tests (redirect/shares/share-public/objects/objects-quota) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract webdav resource usecase — http boundary fully locked

The last and largest handler (1273 lines, 63 violations). All WebDAV port
orchestration — auth resolution, path/lock/dead-property access, PROPPATCH, PUT
(streamed reservation + rollback), MKCOL, DELETE, MOVE, recursive COPY, and the
GET download metering — moves into usecases/webdav.ts. The handler keeps the
protocol machinery: XML multistatus rendering, status codes (207/201/204/423/
412/409/416), header parsing (Depth/Destination/Range/If/Lock-Token/Overwrite),
basic-auth/API-key parsing, and all streaming Response framing (FixedLengthStream,
single-range 206, multipart/byteranges). The GET path calls meterDownloadTraffic
directly; getWebDavObjectBody returns the S3 body for the handler to stream,
preserving the exact (storage, object[, range]) call shape and refund-on-failure.

ratchet: 1 → 0. `pnpm lint:http` now reports "http boundary fully locked".

Verified: typecheck, lint:http (LOCKED), lint:arch, biome clean; 43 unit + 41
integration tests (the 2027-line webdav spec) green — behavior preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(webdav): preserve api-key rate-limit message; deterministic object test dates

- resolveWebDavAuth now threads the original ApiKeyRateLimitError message
  through its rate_limited outcome so the 429 body stays "Rate limit exceeded."
  (the webdav auth refactor had hardcoded "Rate limited") — restores
  api-keys-rate-limit.integration.test.ts.
- object.test.ts file() used argless new Date() in both the mock and the
  expected value; a shared FIXED_DATE makes the deep-equal deterministic (it
  flaked under full-suite load).

Full suite green: 4327 passed (184 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): isolate the e2e database — own throwaway DB, wiped each run

entry-node and e2e/global-setup both honor DATABASE_URL, but it defaulted to the
shared dev ./zpan.db and nothing wiped it — so a local `pnpm e2e` ran against
(and mutated) the dev database and wasn't clean between runs. playwright.config
now defaults DATABASE_URL to a throwaway .e2e/e2e.db (node runtime; CF uses D1)
and wipes it on every run, and sets reuseExistingServer:false so e2e never
silently reuses a running dev server. CI is unaffected (fresh box; reuse already
off). Opt out by setting DATABASE_URL yourself.

Verified: `pnpm e2e auth.spec.ts` (7 passed) ran on .e2e/e2e.db while ./zpan.db
stayed byte-for-byte unchanged (mtime+size identical).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:47:55 -04:00
Jasper Van ac00164fb4 ci(e2e): run core checks before cloud store flow
ci(e2e): run core checks before cloud store flow
2026-06-08 10:55:11 -04:00
saltbo 87b40bb8ef test: stabilize cloud e2e auth flow 2026-06-03 03:10:44 -04:00
saltbo f5eab8f611 test: relax cloud e2e readiness timeouts 2026-06-01 12:27:04 -04:00
saltbo 76d615833d ci: stabilize cf cloud store e2e 2026-06-01 11:57:29 -04:00
saltbo 752c3cbecb ci: use real e2e server entrypoints 2026-06-01 11:30:59 -04:00
saltbo d8feabeaee ci: use system chrome for e2e 2026-06-01 11:21:36 -04:00
saltbo 2438275ea8 feat(archive): queue streaming archive jobs 2026-05-15 09:59:08 -04:00
saltbo ab9014133f test(e2e): isolate cloud runtime ports 2026-05-10 12:19:09 -04:00
saltbo 800cda9f9c test(cloud): add tunnel-backed store e2e 2026-05-09 22:17:52 -04:00
saltbo 193b5ffd7c test(e2e): reduce CI timeout amplification 2026-05-01 22:19:30 -04:00
saltbo f0f3769e84 fix(e2e): stabilize responsive playwright coverage 2026-04-20 22:10:51 -04:00
saltboandClaude Opus 4.6 3f15a6af52 fix(e2e): eliminate all skipped tests with setup project and admin login
- Replace globalSetup function with a setup project that runs after
  webServer starts, fixing the timing issue where seed API calls
  failed because the server wasn't ready yet
- Admin tests sign in with known credentials (fallback chain for
  CI vs local dev) instead of registering new users that may not
  get admin role
- Remove storage column tests that required seeded data — the
  overflow test covers the same responsive behavior
- Remove settings form test that couldn't navigate via page.goto
  due to SPA session loss

Result: 51 tests, 51 passed, 0 skipped, 0 failed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:08:01 -04:00
saltboandClaude Opus 4.6 6c1d7dc062 refactor(e2e): replace test.skip with grep tags for device filtering
Use @desktop/@tablet/@mobile/@all tags in test titles and project-level
grep patterns instead of runtime test.skip(). This eliminates ~90
spurious skipped tests — each project now only loads tests tagged for
its device.

Before: 144 total (43 pass, 101 skip)
After:  55 total (43 pass, 12 skip — admin-only skips)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:44:21 -04:00
saltboandClaude Opus 4.6 015415bd18 fix(e2e): use global setup to seed admin and storage before all tests
Add e2e/global-setup.ts that registers the first user (admin) and
creates a storage backend via API before any tests run. This replaces
the per-test seedStorage calls that failed when the test user wasn't
admin. All folder-dependent tests now work reliably in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:15:15 -04:00
saltboandClaude Opus 4.6 d3d501524b fix(ci): fix E2E server startup in CI and increase dialog timeouts
- Use --env-file=.dev.vars only locally (CI injects env vars directly)
- Increase dialog close timeouts from 5s to 10s for CF Workers runtime
- Replace fragile class-based action button selectors with row-scoped
  role queries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:57:31 -04:00
saltboandClaude Opus 4.6 bd26e54b3e feat(ui): add responsive layout for desktop, tablet, and mobile
Toolbar buttons collapse to icon-only on mobile with sr-only text.
Table columns (size, modified, email, quota, etc.) progressively hide
at sm/md/lg breakpoints. Admin and trash pages get matching treatment.
Playwright config gains tablet (768×1024) and mobile (390×844) projects
with 38 E2E tests covering overflow, column visibility, and sidebar
behavior across all three viewports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:58:23 -04:00
saltboandClaude Opus 4.6 a8f1154959 feat(ci): run E2E tests on both Node and CF Workers runtimes
Split E2E into two parallel jobs:
- e2e-node: Node backend + Vite (--mode node), uses process.env
- e2e-cf: CF Workers via cloudflare vite plugin, uses .dev.vars

Playwright config switches webServer based on E2E_RUNTIME env var.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:27:48 -04:00
saltboandClaude Opus 4.6 9b1cde68fa fix(e2e): use --mode node for Vite in Playwright to skip cloudflare plugin
E2E tests use the Node backend (tsx watch server/entry-node.ts).
Without --mode node, Vite loads the cloudflare() plugin which starts
a Workers runtime that fails on missing env bindings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:25:38 -04:00
saltboandClaude Opus 4.6 310e4950b3 refactor: flatten monorepo to single-package CF Pages Functions structure
Replace pnpm workspace monorepo (packages/server, packages/web, packages/shared)
with a flat single-package layout following Hono's pages-stack pattern. Switch from
pnpm to npm and from Workers+Assets to CF Pages Functions deployment model.

- Move source: packages/server/src/ → server/, packages/web/src/ → src/, packages/shared/src/ → shared/
- Add functions/api/[[route]].ts as CF Pages Functions entry (replaces entry-cloudflare.ts)
- Update 22 import paths: server uses relative, web uses @shared/@server aliases
- Merge three package.json into one, switch to npm
- Update wrangler.toml: remove main/assets (Pages auto-detects functions/ dir)
- Add per-directory tsconfig.json for VS Code type resolution
- Simplify Dockerfile for flat layout
- Fix react-pdf CSS import path (dist/esm/ → dist/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:27:38 -04:00
saltboandClaude Opus 4.6 6b937948df feat: add project infrastructure — testing, CI, linting, logging
- Vitest unit/integration tests for server (22 tests) and shared (14 tests)
- Cloudflare Workers integration tests via @cloudflare/vitest-pool-workers (9 tests)
- Playwright E2E config with auto webServer startup
- GitHub Actions CI: lint → typecheck → test (Node + CF) → E2E
- Biome for linting + formatting (replaces ESLint + Prettier)
- Unified access log middleware for all API routes
- Lefthook pre-commit hooks: lint, typecheck, test
- Coverage threshold enforced at 90% (currently 96.9%)
- Drizzle D1 migrations generated from schema
- Fix pre-existing typecheck errors across all packages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:11:03 -04:00
saltboandClaude Opus 4.6 c6f94dcb43 feat: scaffold v2.0 monorepo with working auth flow
pnpm workspace monorepo with three packages:
- @zpan/server: Hono + Better Auth + Drizzle, dual entry (CF Pages / Node.js)
- @zpan/web: React 19 + Vite + TanStack Router + shadcn/ui
- @zpan/shared: TypeScript types and Zod schemas

Working features:
- Email/password registration and login via Better Auth
- Session-based auth with cookie management
- Authenticated route guard (redirects to /sign-in)
- Sidebar navigation (Files, Recycle Bin, Storage Backends, Users, Settings)
- Placeholder pages for all navigation items
- Vite proxy for local dev (/api/* -> server)
- Platform abstraction for CF D1 and Node.js SQLite

All 5 Playwright E2E tests passing:
- Auth redirect, sign-up, sign-in, sidebar, page navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 21:34:16 -04:00