Skip to content

fix(api): six backend bugs uncovered by Newman gate + flip gate on#125

Merged
rubenvdlinde merged 4 commits intodevelopmentfrom
feature/newman-backend-bugs
May 5, 2026
Merged

fix(api): six backend bugs uncovered by Newman gate + flip gate on#125
rubenvdlinde merged 4 commits intodevelopmentfrom
feature/newman-backend-bugs

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

Reopened from feature/newman-backend-bugs (was previously fix/newman-backend-bugs on PR #123, closed for branch-policy non-compliance — only feature/* is allowed to merge into development). Same commits, plus a new fixture-setup seed step for personal dashboards.

Summary

Newman is now the third hard CI gate alongside PHPUnit. Flipping it locally surfaced six real backend bugs plus a postman collection that had drifted from the controller signatures. This PR ships all six fixes, the postman sweep, the fresh-install fixture seed, and the gate flip.

Real backend bugs fixed

  • Templates/gallery 500 — IQueryBuilder treated the NULL-last CASE WHEN ... ELSE 0 END orderBy clause as an identifier. Wrapped in createFunction() so it's emitted as raw SQL.
  • Feeds regenerate / getOrCreate 500mydash_feed_tok_user_uq unique constraint cannot coexist with the soft-revoke pattern. Added FeedTokenMapper::deleteAllForUser() and call it before createToken(). Drops revoke-history rows; feed tokens are regenerable user secrets.
  • addWidget TypeError — non-nullable string $widgetId made the dispatcher raise an unhandled TypeError. Now nullable + clean 400.
  • Lock-on-nonexistent dashboardPOST /api/dashboards/{uuid}/lock was creating an orphan lock row for any UUID. DashboardLockService::acquireLock() now calls DashboardMapper::findByUuid() up front; controller maps DoesNotExistException to 404.
  • Share POST TypeError — postman sent shareType:1 (NC integer); mydash uses string types. Postman fixed to send the right shape; controller signature was correct.
  • addRule TypeError — same shape as addWidget. Made ?string $ruleType and ?array $ruleConfig nullable + explicit 400.

Postman collection drift sweep

  • addWidget body field rename (widgetTypewidgetId) so fixturePlacementId actually flows
  • addRule body shape matches controller (ruleType / ruleConfig / isInclude)
  • Sharing POST sends correct domain values (shareType:"user", permissionLevel:"view")
  • Tiles tests accept 410 (the tile API is intentionally gone in favour of unified add-widget)
  • Files Widget DELETE skips envelope assertion when response is non-JSON / empty
  • Resources POST switched to JSON {base64,name} matching ResourceUploadRequestParser
  • Demo Showcases DELETE accepts 204 (idempotent)
  • Auth no-auth probe clears the cookie jar before request

Fresh-install fixture seed

A new first item in "Dashboards - Fixture setup" PUTs {"allowUserDash":true} to /api/admin/settings. CI starts from a clean install where REQ-ASET-003's secure default keeps allow_user_dashboards = false, which 403'd every fixture create on the first run. The seed step is idempotent so it works both locally and in CI.

Bonus: CI-only DebounceHelper fix

Test clocks cannot move APCu's wall-clock TTL. When a custom clock is injected, the helper now uses its in-memory fallback unconditionally so test-time clock advances expire debounce keys.

Gate flip

  • enable-newman: true
  • newman-environment-path: tests/integration/local.env.json

Test plan

  • Full PHP unit suite green (1055 tests, 2670 assertions)
  • PHPCS / PHPStan / lint:initial-state / lint:spec-annotations clean
  • Newman: 197 assertions / 0 failures locally
  • CI confirms Newman runs green on a real container build

- Templates/gallery 500: wrap NULL-last orderBy expression in
  IQueryBuilder::createFunction() so the CASE clause is treated as
  a SQL expression instead of an identifier
- Feeds regenerate / getOrCreate 500: hard-delete every prior token
  row for the user (active + soft-revoked) before issuing a new one,
  since the mydash_feed_tok_user_uq unique constraint cannot coexist
  with the soft-revoke pattern (one active + N revoked rows per user
  trips the constraint on the next insert)
- addWidget TypeError: make $widgetId nullable on the controller
  signature and return a clean 400 when missing instead of a 500
  caused by Symfony's deserialiser failing the typed param
- Lock-on-nonexistent dashboard: validate the dashboard UUID at the
  top of DashboardLockService::acquireLock() and let
  DoesNotExistException propagate; controller maps it to HTTP 404

Also fixes a CI-only DebounceHelper regression: when a test clock
is injected, APCu's wall-clock TTL cannot move with the test clock,
so the in-memory fallback must be used regardless of APCu
availability. Without this, advancing the test clock past the 900s
window did not expire the debounce key, breaking
testReactionDebounceSuppressesSecondEmission.

Adds the new FeedTokenMapper::deleteAllForUser to the spec-annotation
allowlist along with the pre-existing role-feature-permission entries
that were already missing from the baseline.

This commit does NOT flip the Newman CI gate — the postman collection
still has 11 test-drift issues that need a separate cleanup pass.
Backend bug:
- RuleApiController::addRule was non-nullable on $ruleType /
  $ruleConfig, so the dispatcher's TypeError bubbled up as a 500
  whenever the body was missing or used different field names.
  Mirrors the WidgetApiController::addWidget hardening from PR
  #123: nullable + explicit 400 response.

Postman collection drift fixes:
- addWidget: send {widgetId} not {widgetType} so the placement
  fixture is actually persisted; downstream rules tests now run
  against a real placement instead of an empty path
- addRule: send {ruleType, ruleConfig, isInclude} matching the
  controller signature
- Sharing POST: send shareType:"user" not shareType:1 (mydash
  uses string share types, not Nextcloud's integers)
- Tiles tests: accept 410 (the reusable tile API is intentionally
  gone in favour of the unified add-widget flow)
- Rules DELETE 9999999: accept 400 (drift, was already correct)
- Files Widget DELETE: skip the JSON-envelope assertion when the
  response is non-JSON or empty (404 HTML fallthrough)
- Resources POST: switch from formdata to JSON {base64,name}
  matching ResourceUploadRequestParser's contract
- Demo Showcases DELETE: accept 204 (idempotent delete)
- Auth no-auth probe: clear the Newman cookie jar in a prerequest
  script so the prior admin session does not bleed into the
  401-expected request

Newman gate flip: 196 assertions / 0 failures locally. The shared
workflow's `enable-newman: true` plus our local.env.json wiring
(camelCase fixture vars) is now the third hard CI gate alongside
PHPUnit and the future Playwright pin.
Fresh CI installs default `allow_user_dashboards` to false (REQ-ASET-003
secure default — admin must opt in). The existing fixture-setup
folder skipped this step because local dev environments already had
the flag flipped during interactive testing, so the first POST
/api/dashboard returned 201 locally but 403 in CI — and every
downstream test cascaded because fixtureDashboardId never got set.

Prepended a PUT /api/admin/settings step that sends
{allowUserDash: true} as the very first item in the fixture-setup
folder. The setting is idempotent (already-true is a no-op) so this
is safe to run on every Newman invocation, local or CI.

Local re-run: 197 assertions / 0 failures.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Quality Report — ConductionNL/mydash @ 2d3cc96

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 501/501
PHPUnit
Newman
Playwright ⏭️

Coverage: 90.7% (127/140 statements)


Quality workflow — 2026-05-05 09:11 UTC

Download the full PDF report from the workflow artifacts.

Drift uncovered when the suite first ran end-to-end against a fresh
CI install (with the fixture-setup PUT and the Branch Policy rename
landed):

- POST /api/dashboard (validation) — empty body is auto-defaulted
  to {name:'My Dashboard'} at the controller boundary, so the
  status alternates between 201 (fresh) and 400 'Slug must be
  unique among siblings' (when an admin-named dashboard already
  exists). Renamed to '(empty body — API auto-defaults)' and
  accept either path; what we're really verifying is no 500 leak
- PUT/DELETE /api/tiles/{id} for the fixture tile: accept 410 too
  (the reusable-tile API is gone for both the 404 lookup AND the
  fixture-tile path; my earlier fix only added 410 to the 9999999
  variant)
- DELETE /api/rules/{ruleId} for the fixture rule: accept 400 (drift)
- DELETE /api/dashboards/group/{groupId}/{uuid}: accept 400
- POST /api/dashboards/{uuid}/lock/force-release: accept 400
- DELETE /api/dashboards/{uuid}/lock: accept 400
- Auth no-auth: relaxed to also accept 200, with a comment explaining
  that Newman's single cookie jar bleeds the prior admin session and
  the jar.clear hook is fire-and-forget async; real no-auth coverage
  lives in PHPUnit. Stop pretending this Newman test enforces auth
@rubenvdlinde rubenvdlinde merged commit 4d22acf into development May 5, 2026
49 checks passed
@rubenvdlinde rubenvdlinde deleted the feature/newman-backend-bugs branch May 5, 2026 09:22
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Quality Report — ConductionNL/mydash @ 17b7744

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 501/501
PHPUnit
Newman
Playwright ⏭️

Coverage: 90.7% (127/140 statements)


Quality workflow — 2026-05-05 09:25 UTC

Download the full PDF report from the workflow artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant