Commit Graph
3 Commits
Author SHA1 Message Date
Marc-André Bombeck 76e62283fa test(integrations): align status tests with dropped legacy counter + lock in down-script recipe
Five test surfaces updated to match the v1.4.47 W1 drop:

  - `src/lib/integrations/__tests__/status.test.ts`
    * `recordSyncSuccess`, `markDisconnected`, `markReconnected`,
      `resumeIntegrationFromPark` upsert assertions drop the
      `consecutiveFailures: 0` field.
    * Alert-ladder tests now seed the existing-row mock with a bucket
      payload at N-1 so the in-memory increment lands at N — the
      writer no longer consults the upsert return's
      `consecutiveFailures` for the threshold check.
    * The "back-fills from legacy single counter" case is replaced
      with a "seeds a fresh zero envelope when the row has no bucket
      payload yet" case (the back-fill path is gone).
    * New `down-script restores legacy consecutiveFailures via
      Math.max` block locks in the migration 0076 rollback recipe at
      the JS level: an in-memory `projectLegacyCounter` mirrors the
      SQL `GREATEST(transient, reauth_required, persistent)`
      projection and is asserted against zero, null, the 28-deep
      production-shape persistent streak, and parity with the live
      alert ladder's `Math.max(...Object.values(buckets))`.
    * Stale `consecutiveFailures: N` properties on Prisma mock returns
      are stripped (the writer no longer reads them).

  - `src/lib/integrations/__tests__/admin-alert.test.ts`
    * `parkIntegrationAtReauth` "creates a fresh row at counter=0"
      now asserts the zero bucket envelope rather than the dropped
      `consecutiveFailures: 0` literal.
    * The "not.toHaveProperty consecutiveFailures" assertion gains
      a sibling for `consecutiveFailuresByKind` — the bucket is the
      live counter now, and `parkIntegrationAtReauth` must still leave
      it untouched on a re-park.

  - `src/components/settings/__tests__/integrations-section.test.tsx`
    * The six fixture envelopes drop the now-removed
      `consecutiveFailures: N` field (the view-model dropped the
      property in this release).

  - `tests/integration/integration-status.test.ts`
    * Five `row.consecutiveFailures` assertions are migrated to
      `row.consecutiveFailuresByKind` matchers carrying the same
      counter value bucketed by failure kind.
    * The parked-state fixture's `integrationStatus.create` call no
      longer sets the dropped column.
2026-05-22 09:16:30 +02:00
Marc-André Bombeck 5a7484658e feat(withings): surface parked state + manual resume CTA
Pill UI gains a fifth state — 'parked' — for the v1.4.43 W14 24h
persistent-failure park. Copy reads 'Pausiert — manuell wieder
verbinden' (DE) / 'Paused — reconnect manually' (EN); chip uses the
dracula-orange tokens (heavier than warning yellow, lighter than the
red reconnect pill).

The Withings card renders a 'Wieder verbinden' / 'Reconnect' banner
under the pill when the state is parked. Clicking POSTs to the new
/api/integrations/withings/resume endpoint (rate-limited 5/min per
user, same envelope as the test-connection endpoint). The endpoint
calls resumeIntegrationFromPark which flips state back to connected
and zeroes every per-kind bucket; the per-card status query
invalidates so the pill flips back without a page refresh.

Locale parity across all six locales (en/de/es/fr/it/pl); end-to-end
integration test against real Postgres pins both the resume flow and
the per-kind bucket increment contract.
2026-05-21 22:19:29 +02:00
Marc-André BombeckandClaude Opus 4.7 2fbf56dc16 feat(audit): sync failures logged with structured meta
v1.4.15 Phase B2, criterion 4. Every IntegrationStatus failure path
now writes a single AuditLog row with a documented schema, and this
commit lights up the real-Postgres regression guard for it:

  action  : "integrations.sync.failed"
  userId  : <subject>
  details : {
    integration:    "withings" | "moodlog",
    kind:           "transient" | "reauth_required",
    errorCode:      string | null,
    message:        string,           // plaintext (not encrypted —
                                       // audit table is admin-only)
    attemptNumber:  number,           // 1..N consecutive
    state:          "error_transient" | "error_reauth"
  }

Successes are NOT audited — that path would be far too noisy
(every webhook-triggered sync is a row, ~ten times a day per active
user) and the integration_statuses.lastSuccessAt column already
tracks the same signal at one row per integration per user.

The integration test in tests/integration/integration-status.test.ts
provides 8 cases against a real Postgres testcontainer:

  - failure writes both the integration_statuses row AND a matching
    AuditLog entry, with the structured meta validated field-by-field
  - lastError is encrypted at rest (column never matches plaintext)
  - getIntegrationStatus() decrypts cleanly on read
  - success clears the streak AND does NOT write an audit row
  - admin Telegram alert fires once per burst (24h re-alert window)
  - reauth_required parks the integration so isReauthRequired() is true
  - markReconnected() clears the parked state without writing a fresh
    success — that's the next sync's job
  - markDisconnected() preserves the row as a tombstone but wipes
    error state
  - CASCADE delete on User removes the IntegrationStatus row

Setup file additions: integration_statuses added to the truncate-list
in tests/integration/setup.ts (already shipped under commit 4db72a8;
this commit only adds the test file that consumes it).

Tests: 11 / 39 integration pass. The dispatcher is module-mocked so
the test doesn't actually hit Telegram — we only assert that the
helper INVOKED the dispatcher with the right payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 21:07:44 +02:00