Files
civitai__civitai/tests
Zachary Lowden ab1c907f77 feat(app-blocks): W13 P3a PR-b — off-site approve/reject + assertListingAssetsComplete wiring (dark) (#2954)
* feat(app-blocks): W13 P3a — off-site approve/reject + assertListingAssetsComplete wiring (dark)

PR-b of the App Store off-site (external-link) listing flow. Extends
offsite-listing.service (PR-a, on main) with the moderator approve/reject
state machine and ACTIVATES the dark P1 mandatory-asset gate at approve.
Everything stays dark behind app-blocks-author / moderatorProcedure — no UI.

- approveExternalRequest (moderatorProcedure): loads the request + its draft
  AppListing, asserts pending, then enforces two gates BEFORE any mutation:
  (1) assertListingAssetsComplete(listing) — THE P3 activation: approve FAILS
  BAD_REQUEST { missing } unless icon + cover + >=1 screenshot (a screenshot
  whose Image was deleted / imageId null does NOT count, mirroring
  getListingAssets); (2) re-validates the STORED externalUrl (defense-in-depth,
  a non-https stored value blocks approve). Then, in ONE transaction, flips the
  request pending->approved (status-guarded TOCTOU), flips the listing
  draft->approved (status-guarded), sets reviewedBy*/reviewedAt/approvalNotes,
  and supersedes any sibling pending request for the slug (parity w/ the on-site
  publish-request approve).
- rejectExternalRequest (moderatorProcedure): requires rejectionReason >=10;
  flips the request pending->rejected + reviewedBy*, and DELETES the draft
  AppListing (status-guarded deleteMany({id, status:'draft'}) — releases the
  slug, can never remove an approved listing).
- Router: wire approveExternalRequest / rejectExternalRequest on the appListings
  router as moderatorProcedure; new approveExternalRequestSchema /
  rejectExternalRequestSchema (mirror the on-site approve/reject shapes). Service
  failures map to BAD_REQUEST with the message (mirrors blocks.approve/reject).

Locked decisions honored:
- Reject deletes the draft (releases the slug); approve flips draft->approved so
  the approved-only read path surfaces it in the store.
- v1 ALLOWS mod self-approve (reviewer == submitter) — trusted, enables
  single-mod dogfood + the approve e2e. A reviewer!=submitter restriction is
  DEFERRED to GA/P3b (noted in-code). Self-approve is NOT blocked.
- No schema/DDL change, no migration; no ...input spread (update data built
  explicitly).

Tests: offsite-listing.schema (+8 approve/reject shape), offsite-listing.service
(+17 approve/reject: happy-path, gate-blocked per missing asset + all-present
pass, non-pending, stored-URL re-validation, supersede, TOCTOU, mod self-approve
allowed), router authz matrix (+7: both procs moderatorProcedure, non-mod
FORBIDDEN, gate error -> BAD_REQUEST) — 92 in the three files, all green.
New e2e preview-apps-external-approve.spec.ts (reject path + approve-gate path,
mod, self-cleaning; approve-SUCCESS->store deferred to PR-c since there is no
delete-approved path yet — noted in the spec header). Runs in Tekton
pr-smoke-test, not locally.

PR-c (UI: submit form + kind-aware review queue) and PR-d (#2821 retirement)
follow as separate PRs off main.

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

* fix(app-blocks): W13 P3a post-audit — primary-gate-in-tx, error mapping, atomic reject

Post-audit hardening of the off-site listing approve/reject flow (PR #2954):

1. Gate on the PRIMARY inside the approve tx. assertListingAssetsComplete
   previously read iconId/coverId + the screenshot count via dbRead (replica),
   but the sibling asset mutators write to dbWrite — so under replica lag + a
   concurrent owner asset-edit the gate could pass on stale-complete state. The
   authoritative gate now re-reads iconId/coverId + the imageId-bearing
   screenshot count via the tx client (primary), row-consistent with the status
   flip; the cheap replica pre-tx check is retained as a fail-fast.

2. Proper error-code mapping in the router approve/reject catch. A new
   mapOffsiteError passes shaped TRPCErrors through, maps typed
   OffsiteRequestError codes (NOT_FOUND->NOT_FOUND, NOT_OWNED->FORBIDDEN,
   NOT_PENDING->BAD_REQUEST), and turns any unexpected infra/Prisma throw into
   INTERNAL_SERVER_ERROR with a generic message (raw error kept only on cause) —
   replacing the blanket BAD_REQUEST + raw-message that mis-coded typed failures
   and leaked infra messages to moderators.

3. Atomic reject. The request flip + draft delete are now wrapped in one
   $transaction (parity with approve) so a crash between them can't orphan a
   hidden draft listing squatting the slug.

Tests: dbRead/dbWrite split into DISTINCT mocks so the gate's primary reads are
asserted (incl. a replica-lag divergence test where the replica reads complete
but the primary is incomplete -> approve BLOCKED); added typed->proper-code and
untyped->INTERNAL_SERVER_ERROR (no message leak) router tests; added a
reject-atomicity test. 147 related unit tests green.

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

* fix(app-blocks/offsite): TS2352 in mapOffsiteError — cast Error via {code?:unknown}

Error -> {code:string} is an invalid direct cast (missing required prop);
the line-124 guard already narrows via {code?:unknown} + typeof===string,
so read it the same way. Would fail the Tekton typecheck otherwise.

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-07-06 15:51:34 -05:00
..
2025-08-12 13:20:21 -04:00
2025-05-23 14:00:07 -04:00

Video: https://discord.com/channels/955572167662260295/1062092338698145812/1338591521401733192

# Playwright Testing

### Goal
E2E testing for the main app.
Catch potential issues when changing code, or evaluate edge cases.

### Anti-Goal
For this to be a frustrating pain-in-the-ass time vampire.
It's better to have 1 decent test than try to do 10 perfect ones and give up because it's too time consuming.

### What to Do

- Write tests for common user flows (fill a form, click a button, get a result)
- Handle what-if scenarios (errors, browsers, etc)
- A good rule of thumb is: if it's been reported as a bug, we should have a test in place for it (and things like it)

### What Not to Do

- Write "1==1" tests (dopamine hit, but pointless)
- Mandate coverage percentages (leads to annoyance and features not being done)

---

### How

Testing is intended to work on local development (docker) for consistency with users/data and easy tear down.

1) Run local services (`make init` or devcontainers)
2) Create a file in the `tests/` directory, or use an existing one. Doesn't really matter. Open to directory structure, so something like `tests/generator/gen-queue.spec.ts` would be reasonable.
3) Start writing tests.
    (a) can be done by hand if you know what you're looking to do
    (b) easier approach: `npm run test:gen -- --load-storage tests/auth/{user}.json --viewport-size 1920,1080 http://localhost:3000/{url}`
        - This allows you to create tests by interacting with the page and picking locators
    (c) we'll need better locators, especially for icons. add `data-testid=` to the places you need them (they'll be stripped from production)
    (d) use the various authed users to test different scenarios (mod, full access, muted, etc)
    (e) feel free to mock responses from any of the APIs, but in general it's best to only do this for external services
4) Run with either `npm run test` or `npm run test:ui` to do it interactively with screenshots
5) If you need to reset the db after each test, you can either:
    (a) clean up the mutations as part of the test (delete an object you just made)
    (b) `make boostrap-db` to reset the whole database back to normal
6) We'll eventually set up the github action to run this before a deploy

### Test Failures

There are 4 types of test failures:

1) A bad test (always fail)
    - these might have bad selectors or inaccurate logic
    - **solution**: fix them
2) A flaky test (sometimes fail)
    - frustrating tests which seem to pass most of the time, but not always
        - this is usually a result of race conditions, mismatched timing, or not properly awaiting events like animations
    - **solution**: narrow down which part of the test fails, and catch the flaky issue
3) Intended code change
    - we might have changed the verbiage on a button, which makes certain locators no longer work
        - this is fine, although locators should try to be as agnostic as possible
    - alternatively, we might have simply changed the business logic
    - **solution**: in either case, simply update the test itself
4) Unintended code change
    - you've changed something in the app, and a test breaks due to the introduction of a bug
        - this is the major reason we have tests
    - **solution**: leave the tests alone, they're doing their job. fix the code.