Files
civitai__civitai/tests
Zachary Lowden 5e8ec4f70b W13 P3b PR4 — off-site claimListing (mod-arbitrated ownership reassignment) [DARK] (#2970)
* W13 P3b PR4 — off-site claimListing (mod-arbitrated ownership reassignment) [DARK]

The FINAL P3b piece, split out of PR3. A moderator-only, arbitrated ownership
transfer for an approved OR delisted off-site AppListing: reassigns
`AppListing.userId` to a mod-verified target user, fully audited, no self-service.

Service (`offsite-moderation.service.ts`), mirroring the delist/relist/purge
tx + guard + audit pattern EXACTLY:
- kind guard: offsite-only (on-site is 1:1 with an owned AppBlock) → generic NOT_FOUND
- status guard: allow claim only on {approved, removed}; draft/pending/rejected →
  NOT_TRANSITIONABLE, zero events
- target-user validation on the PRIMARY inside the tx → friendly INVALID_TARGET_USER
  (BAD_REQUEST) instead of a raw FK 23503 leaking as INTERNAL
- pre-state (before.userId + slug) snapshotted from the in-tx PRIMARY read (mirrors
  PR3 purge) so replica lag can't stamp a stale owner
- status-guarded updateMany (status IN approved|removed); 0-count → NOT_TRANSITIONABLE,
  rollback before the audit event (zero events on a guarded/raced claim)
- ONE AppListingModerationEvent(action='claim', actor=reviewer, before/after userId, slug)
- `AppListingPublishRequest.submittedByUserId` left INTACT (locked decision — the
  historical submission record is preserved; claim reassigns the owner only)

Router: `claimListing` = moderatorProcedure + inner isModerator recheck + mapOffsiteError.
NO protectedProcedure self-claim endpoint (mod-only is the whole trust boundary).
Schema: `claimListingSchema` (appListingId, positive-int targetUserId, bounded reason).
No schema/migration change — the `claim` action already exists in the merged CHECK.

UI (dark, mods only): a Claim action on the Reports-tab action set + a modal with a
numeric target-user id + reason. View-model (`appListingModerationView`) offers claim
on approved AND removed rows.

Tests (all pass with symlinked deps): +claim service coverage (happy-path
approved/removed, status/kind/target-user guards, TOCTOU, submittedByUserId untouched,
in-tx-primary snapshot, audit correctness, reason floor); router authz (moderator-only,
tester/anon forbidden, INVALID_TARGET_USER→BAD_REQUEST, single claim proc / no self-claim);
view-model claim action-set; e2e claim guard rejections + tester-forbidden (happy path is
unit-covered — a claimable state isn't constructible in preview).

Closes P3b. Activation gate unchanged: all three P3b migrations before `release`.

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

* fix(w13-p3b): thread+resolve reportId on claimListing (mirror delist); render owner transfer in history

Post-audit coherence fixes for W13 P3b PR4 (claimListing):

- claimListing now accepts an optional reportId (schema, same shape/bounds as
  delist), links it on the AppListingModerationEvent, and resolves that report in
  the SAME tx — listing-scoped + status-guarded, so a mismatched/already-closed
  reportId is a silent no-op (the claim still succeeds). Mirrors delistListing
  exactly. In the impersonation flow (report -> delist -> claim -> ban) the claim
  is the substantive resolution, so it now closes the triggering report instead of
  leaving it pending. No migration: reportId column already exists (PR1).
- UI: the Claim modal (always initiated from a report row) passes report.id.
- History modal: renders the claim event's before/after owner transfer
  ("owner: X -> Y"), guarded to the {userId}-shaped claim payload so it never
  mis-reads the {status}-shaped delist/relist/purge/report-* events.
- Tests: +2 (matching reportId resolves+links; cross-listing reportId scoped
  no-op) and pin the no-report case (event reportId=null, report untouched).

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-07 06:37:55 -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.