fix(website): replace fabricated account states with truthful ones (W-2FA, W-devices, W-metrics)

The account page asserted several things that were not backed by real state:
- Two-Factor Authentication was hardcoded to "ENABLED" for every signed-in user.
  Now derives from the real Firebase multiFactor enrollment and shows "NOT ENABLED"
  (enroll in the mobile app) unless a factor is actually enrolled.
- "App Connections & Sync" showed a hardcoded connected iOS app (iPhone 16 Pro, v4.1.0,
  "14m ago") and a paired "Cardo Packtalk Edge". Replaced with truthful states: real
  Google account link, authenticated-device count from the real /api/account/sessions
  data, and an honest "Unavailable on web" for Bluetooth comms/accessories (a browser
  cannot observe hardware pairing). Header status reflects real sign-in state.
- Dashboard metrics initialised to fabricated totals (12,480 mi / 184 rides / 56.4 deg).
  Now initialise to zero; AppCloudData replaces them with the rider's real ride totals.
- Dead controls fixed: "Manage Devices" and "Review active sessions" now anchor the real
  Active Device Sessions panel; the telemetry-privacy control no longer presents an
  unverified setting with a no-op "Change" button (managed in the mobile app).

Also adds the Phase 1 website feature audit matrix (docs/).

Tests: account-truthful-states asserts no fabricated device/pairing/sync claims render
and that 2FA is never shown as ENABLED without a real enrolled factor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Developer 2026-10-14 12:02:18 -04:00
parent c014c2f04f
commit 217e5a11c0
3 changed files with 215 additions and 22 deletions

View File

@ -2,7 +2,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { onAuthStateChanged, User } from 'firebase/auth'; import { onAuthStateChanged, multiFactor, User } from 'firebase/auth';
import { firebaseAuth } from '../../lib/firebase-client'; import { firebaseAuth } from '../../lib/firebase-client';
import { AppCloudData } from '../../components/app-cloud-data'; import { AppCloudData } from '../../components/app-cloud-data';
import { FirebaseLink } from '../../components/firebase-link'; import { FirebaseLink } from '../../components/firebase-link';
@ -22,7 +22,10 @@ export default function AccountPage() {
const [syncTrigger, setSyncTrigger] = useState(0); const [syncTrigger, setSyncTrigger] = useState(0);
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const [lastSynced, setLastSynced] = useState<string | null>(null); const [lastSynced, setLastSynced] = useState<string | null>(null);
const [metrics, setMetrics] = useState({ totalMiles: 12480, rideCount: 184, peakLean: 56.4 }); // Initialise to zero; AppCloudData replaces these with real totals from the rider's
// Firestore rides (or leaves them at zero when signed out / no rides). Never seed
// fabricated stats.
const [metrics, setMetrics] = useState({ totalMiles: 0, rideCount: 0, peakLean: 0 });
// Active Device Sessions State // Active Device Sessions State
const [sessions, setSessions] = useState<DeviceSession[]>([]); const [sessions, setSessions] = useState<DeviceSession[]>([]);
@ -42,6 +45,18 @@ export default function AccountPage() {
return () => unsubscribe(); return () => unsubscribe();
}, []); }, []);
// Truthful 2FA state: reflect the user's actual Firebase multi-factor enrollment.
// The web app has no MFA enrollment flow, so this is read-only; enrollment is done in
// the mobile app. Never claim 2FA is enabled unless a factor is actually enrolled.
const twoFactorEnrolled = (() => {
if (!user) return false;
try {
return multiFactor(user).enrolledFactors.length > 0;
} catch {
return false;
}
})();
async function loadSessions() { async function loadSessions() {
setIsLoadingSessions(true); setIsLoadingSessions(true);
try { try {
@ -438,14 +453,14 @@ export default function AccountPage() {
<div> <div>
<div className="flex items-center justify-between border-b border-brand-border pb-4 mb-4"> <div className="flex items-center justify-between border-b border-brand-border pb-4 mb-4">
<span className="text-xs font-mono tracking-normal text-brand-cyan">App Connections &amp; Sync</span> <span className="text-xs font-mono tracking-normal text-brand-cyan">App Connections &amp; Sync</span>
<span className="text-xs font-mono text-brand-emerald flex items-center space-x-1.5"> <span className={`text-xs font-mono flex items-center space-x-1.5 ${user ? 'text-brand-emerald' : 'text-slate-400'}`}>
<span className="w-1.5 h-1.5 rounded-full bg-brand-emerald"></span> <span className={`w-1.5 h-1.5 rounded-full ${user ? 'bg-brand-emerald' : 'bg-slate-500'}`}></span>
<span>All Synced</span> <span>{user ? 'Account linked' : 'Not signed in'}</span>
</span> </span>
</div> </div>
{/* Sync List */} {/* Sync List */}
<div className="space-y-3 font-mono text-xs"> <div className="space-y-3 font-mono text-xs">
{/* Google App Account */} {/* Google App Account (real: reflects signed-in Firebase user) */}
<div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between"> <div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-slate-800 flex items-center justify-center font-bold text-slate-300"> <div className="w-8 h-8 rounded-lg bg-slate-800 flex items-center justify-center font-bold text-slate-300">
@ -460,37 +475,41 @@ export default function AccountPage() {
{user ? 'Connected' : 'Not Connected'} {user ? 'Connected' : 'Not Connected'}
</span> </span>
</div> </div>
{/* MotorSignal Mobile App */} {/* Authenticated devices (real: from /api/account/sessions) */}
<div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between"> <div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-cyan-500/15 text-brand-cyan flex items-center justify-center"> <div className="w-8 h-8 rounded-lg bg-cyan-500/15 text-brand-cyan flex items-center justify-center">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg>
</div> </div>
<div> <div>
<span className="text-white block font-sans font-medium">iOS Mobile App</span> <span className="text-white block font-sans font-medium">Authenticated Devices</span>
<span className="text-slate-400 text-[11px]">iPhone 16 Pro • v4.1.0</span> <span className="text-slate-400 text-[11px]">
{!user ? 'Sign in to view linked devices' : isLoadingSessions ? 'Checking sessions…' : 'Manage in Active Device Sessions below'}
</span>
</div> </div>
</div> </div>
<span className="text-slate-400 text-[11px]">14m ago</span> <span className={`text-[11px] px-2 py-0.5 rounded border ${user && sessions.length > 0 ? 'text-brand-emerald bg-emerald-500/10 border-emerald-500/20' : 'text-slate-400 bg-slate-800 border-slate-700'}`}>
{!user ? '—' : isLoadingSessions ? '…' : `${sessions.length} active`}
</span>
</div> </div>
{/* Comms / Cardo */} {/* Helmet comms / accessories — Bluetooth pairing is not observable from a browser */}
<div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between"> <div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-slate-800 text-slate-300 flex items-center justify-center"> <div className="w-8 h-8 rounded-lg bg-slate-800 text-slate-300 flex items-center justify-center">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 100-6 3 3 0 000 6z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 100-6 3 3 0 000 6z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg>
</div> </div>
<div> <div>
<span className="text-white block font-sans font-medium">Helmet Comms Link</span> <span className="text-white block font-sans font-medium">Helmet Comms &amp; Accessories</span>
<span className="text-slate-400 text-[11px]">Cardo Packtalk Edge</span> <span className="text-slate-400 text-[11px]">Bluetooth pairing is managed in the mobile app</span>
</div> </div>
</div> </div>
<span className="text-brand-emerald text-[11px] bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">Paired</span> <span className="text-slate-400 text-[11px] bg-slate-800 px-2 py-0.5 rounded border border-slate-700">Unavailable on web</span>
</div> </div>
</div> </div>
</div> </div>
<div className="pt-3 flex items-center justify-between text-xs font-mono"> <div className="pt-3 flex items-center justify-between text-xs font-mono">
<span className="text-slate-400">Apple Watch Companion: <span className="text-slate-400 font-semibold">Not Connected (Unavailable on Web)</span></span> <span className="text-slate-400">Apple Watch Companion: <span className="text-slate-400 font-semibold">Not Connected (Unavailable on Web)</span></span>
<button className="text-brand-cyan hover:underline font-sans font-medium">Manage Devices</button> <a href="#active-device-sessions" className="text-brand-cyan hover:underline font-sans font-medium">Manage Devices</a>
</div> </div>
</div> </div>
</section> </section>
@ -498,7 +517,7 @@ export default function AccountPage() {
{/* BEGIN: SecurityAndSessionsGrid */} {/* BEGIN: SecurityAndSessionsGrid */}
<section className="grid grid-cols-1 lg:grid-cols-12 gap-6" data-purpose="security-and-session-management"> <section className="grid grid-cols-1 lg:grid-cols-12 gap-6" data-purpose="security-and-session-management">
{/* Sessions (7 cols) */} {/* Sessions (7 cols) */}
<div className="lg:col-span-7 glass-card rounded-xl border border-brand-border p-6" data-purpose="active-browser-sessions"> <div id="active-device-sessions" className="lg:col-span-7 glass-card rounded-xl border border-brand-border p-6 scroll-mt-24" data-purpose="active-browser-sessions">
<div className="flex items-center justify-between border-b border-brand-border pb-4 mb-4"> <div className="flex items-center justify-between border-b border-brand-border pb-4 mb-4">
<div> <div>
<h3 className="text-lg font-bold text-white">Active Device Sessions</h3> <h3 className="text-lg font-bold text-white">Active Device Sessions</h3>
@ -600,27 +619,31 @@ export default function AccountPage() {
</div> </div>
<FirebaseLink linked={!!user} /> <FirebaseLink linked={!!user} />
</div> </div>
{/* 2FA State */} {/* 2FA State — reflects real Firebase multi-factor enrollment; enrollment is managed in the mobile app */}
<div className="flex items-center justify-between p-3 rounded-lg bg-brand-cardAlt border border-brand-border"> <div className="flex items-center justify-between p-3 rounded-lg bg-brand-cardAlt border border-brand-border">
<div> <div>
<span className="text-white font-bold block">Two-Factor Authentication (2FA)</span> <span className="text-white font-bold block">Two-Factor Authentication (2FA)</span>
<span className="text-slate-400 text-[11px]">Hardware TOTP Authenticator</span> <span className="text-slate-400 text-[11px]">{twoFactorEnrolled ? 'Authenticator enrolled' : 'Enroll in the mobile app'}</span>
</div> </div>
{twoFactorEnrolled ? (
<span className="text-brand-emerald bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20 font-bold">ENABLED</span> <span className="text-brand-emerald bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20 font-bold">ENABLED</span>
) : (
<span className="text-slate-400 bg-slate-800 px-2 py-0.5 rounded border border-slate-700 font-bold">NOT ENABLED</span>
)}
</div> </div>
{/* Leaderboard Privacy */} {/* Leaderboard Privacy — managed in the mobile app; the web app has no privacy toggle backend */}
<div className="flex items-center justify-between p-3 rounded-lg bg-brand-cardAlt border border-brand-border"> <div className="flex items-center justify-between p-3 rounded-lg bg-brand-cardAlt border border-brand-border">
<div> <div>
<span className="text-white font-bold block">Telemetry Privacy Mode</span> <span className="text-white font-bold block">Telemetry Privacy Mode</span>
<span className="text-slate-400 text-[11px]">Public Leaderboard (Club Trophies)</span> <span className="text-slate-400 text-[11px]">Leaderboard visibility is managed in the mobile app</span>
</div> </div>
<button className="text-brand-cyan hover:underline">Change</button> <span className="text-slate-400 text-[11px] bg-slate-800 px-2 py-0.5 rounded border border-slate-700">In mobile app</span>
</div> </div>
</div> </div>
</div> </div>
<div className="pt-4 border-t border-brand-border mt-4 flex items-center justify-between text-xs"> <div className="pt-4 border-t border-brand-border mt-4 flex items-center justify-between text-xs">
<span className="text-slate-400 font-mono">Telemetry Data Retention: 100% Retained</span> <span className="text-slate-400 font-mono">Telemetry Data Retention: 100% Retained</span>
<a className="text-brand-cyan hover:underline font-mono" href="/account">Security Logs</a> <a className="text-brand-cyan hover:underline font-mono" href="#active-device-sessions">Review active sessions</a>
</div> </div>
</div> </div>
</section> </section>

View File

@ -0,0 +1,124 @@
# MotorSignal Website — Feature Audit Matrix
Date: 2026-10-14
Branch: `fix/website-production-hardening-20261014`
Baseline: PR #36 integration HEAD `4931920` (`codex/master-consolidation-20261012`)
Scope: `website/` only (Next.js 16.3.4 / React 19). Flutter, Drift, Firebase rules,
Cloudflare Worker, and backend production code are out of scope.
Method: classifications below are grounded in the current source (file:line evidence),
not in filenames or prior docs. "Verified truthful" means the code was read and does a
real backend operation (or honestly reports unavailability); it does **not** claim any
live device / motorcycle validation.
Legend — status: ✅ implemented & truthful · 🟠 partial · 🔴 broken / misleading ·
🏷️ UI-only placeholder · 🔌 backend-dependent · 🗓️ deferred/roadmap.
Severity: P0 security / data-integrity / misleading success · P1 broken core workflow ·
P2 incomplete / usability · P3 roadmap.
## Summary
The PR #36 baseline is substantially real. Club governance writes, group-ride
create/edit/delete/join, the garage/ride-history/export surface, device sessions, the
billing-portal fallback, and the Formation Radar are all implemented against real
Firestore/API paths or honestly report unavailability. The genuine defects are a focused
set of **fake success / fabricated-as-real** spots plus the static Support page.
| # | Defect | File | Severity | Status |
|---|--------|------|----------|--------|
| W1 | Rank "permission matrix" faked a save (setTimeout, no write) | `app/clubs/[clubId]/manage/ranks-roles/page.tsx`, `components/founder-portal/club-permission-matrix.tsx` | P0 | ✅ FIXED (commit c014c2f) |
| W-2FA | Account shows "Two-Factor Authentication ENABLED" hardcoded for every signed-in user | `app/account/page.tsx:603-610` | P0 | 🔴 confirmed |
| W-devices | Account shows hardcoded "iOS Mobile App iPhone 16 Pro v4.1.0 · 14m ago" and "Cardo Packtalk Edge · Paired" as real connected devices | `app/account/page.tsx:463-488` | P1 | 🔴 confirmed |
| W-routes | Club routes: empty Firestore collection falls back to `FIXTURE_CLUB_ROUTES` (fake "Coastal Marshes" route + `/fixtures/routes/*.gpx`) in production | `lib/club-community.ts:265-268` | P1 | 🔴 confirmed |
| W-chat | Club chat: empty channel falls back to `FIXTURE_CHAT_MESSAGES` in production | `lib/club-community.ts:443-447` | P1 | 🔴 confirmed |
| W-metrics | Account dashboard `metrics` initialises to fabricated `12480 mi / 184 rides / 56.4°` before real data loads (persists if load fails) | `app/account/page.tsx:25` | P2 | 🟠 confirmed |
| W2 | Support Center is a static hero + `mailto:` only — no FAQ, no validation, no ticketing | `app/support/page.tsx` | P2 | 🔴 confirmed |
| W-deadctl | Dead/no-op controls: "Change" (telemetry privacy), "Manage Devices", "Security Logs"→`/account`, static "All Synced" | `app/account/page.tsx` | P2/P3 | 🟠 confirmed |
## Feature matrix (by area)
### Public site & navigation
- Homepage `app/page.tsx` + `components/landing/*` — ✅ marketing content.
- 🟠 `components/landing/TelemetryStrip.tsx:10-11` and `app/features/page.tsx:15-20` use
`Math.random` for animated counters. Decorative marketing, not presented as a user's
real telemetry. P3 — acceptable but worth a "demo" label.
- Navigation / responsive mobile nav `app/layout.tsx` — ⏳ not yet width-tested (Phase 8).
### Authentication & account
- Login / register / forgot-password / verify-email — ✅ real (`components/auth-form.tsx`,
`api/auth/[action]`); covered by `tests/auth-form.test.tsx`, `auth-recovery-verification`.
- Account settings `app/account/page.tsx` — ✅ mostly real, with the W-2FA / W-devices /
W-metrics / W-deadctl defects above.
- Device sessions `app/account/page.tsx` + `api/account/sessions` — ✅ real list, revoke &
revoke-all, pagination, truthful empty/loading states.
- Subscription & billing — ✅ truthful: `/api/billing/portal` is unconfigured and the page
says so ("managed through the mobile app"); no fake success (`app/account/page.tsx:107-141`).
### Garage & motorcycle management
- `components/app-cloud-data.tsx` — ✅ real Firestore: add / edit / delete / make-primary /
photo upload (`users/{uid}/motorcycles`), image via `/api/motorcycle-image`.
### Ride history, telemetry sessions & export
- `components/app-cloud-data.tsx` — ✅ real rides from `users/{uid}/rides`, GPX & CSV export
handlers, delete, "Show more" pagination. CSV/GPX byte-integrity not yet re-verified (Phase 4/9).
- Telemetry Forensic Replay `components/web-telemetry-replay-modal.tsx` — ⏳ not yet fully
audited (map tiles, progress, playback smoothness — Phase 4). Tests exist
(`tests/telemetry-replay-export.test.tsx`, `replay-recorded-data.test.tsx`).
### Clubs / Founder Portal
- Profile / access-settings / ownership-transfer / archive — ✅ real writes with owner/admin
authorization (`lib/club-data.ts`; demo branch uses labelled simulated latency only).
- Applications approve/decline — ✅ real transactional writes.
- Member rank assignment — ✅ real (`updateMemberRank`).
- Rank permission matrix — ✅ FIXED (W1): now a truthful read-only reference.
- Club routes — 🔴 W-routes fixture fallback.
- Club chat / wall — 🔴 W-chat fixture fallback (wall send path not yet fully audited).
### Group Rides
- Public page `app/group-rides/page.tsx` — ✅ splits by auth: signed-out →
`GroupRidesShowcase` (clearly labelled "Sample Pack Formations" / "Radar Simulation"),
signed-in → `GroupRidesManagement`. Public path exposes no real private rides.
- Authenticated Hub `components/group-rides/group-rides-management.tsx` — ✅ real:
`/api/group-rides` discovery + Firestore `onSnapshot`; create/edit/delete/join/leave write
to `group_rides`. 🟠 join/ride identifier generated client-side with `Math.random`
(`:1008`) — weak/non-authoritative; 🟠 `INITIAL_GROUP_RIDES` seeds initial state (replaced
on first snapshot; can linger if no Firestore). P2.
- Address autocomplete / route distance / scheduling / join codes / QR — ⏳ not yet fully
audited (Phase 3).
### Live Formation Radar
- `components/group-rides/live-pack-radar.tsx` — ✅ truthful. Real rides subscribe to
Firestore `live_presence` with authorization + loading + error states and **no** fixture
fallback; `INITIAL_RADAR_RIDERS` is used only for the showcase/demo and is explicitly
labelled "Public Demonstration (Simulated Pack)" with "[Simulation Demo]" ping copy. The
historical dummy-data concern is already remediated in this baseline.
### Device connections / hardware
- 🔴 W-devices: account "App Connections" block hardcodes a connected iOS app and a paired
Cardo unit. Apple Watch row is truthful ("Not Connected (Unavailable on Web)").
- Helmet Comms / GoPro / Apple Watch as first-class features — 🗓️ roadmap; must be labelled.
### Support Center
- 🔴 W2: `app/support/page.tsx` is one static hero + `mailto:`. No public ticket backend
exists (the `api/ops/support/tickets/*` routes are the internal operations console, not a
public endpoint). Phase 6 remediation: keep email fallback, add grounded FAQ + validated
contact UX; do not simulate ticket submission.
### Operations console (`app/api/ops/*`, `components/ops/*`, `app/admin`)
- Out of the brief's feature list; appears to be a separate internal console. Not modified.
## Remaining P0–P3 work (website)
- **P0** — W-2FA (false security state). Fix: show real MFA state / "managed in mobile app",
never a hardcoded "ENABLED".
- **P1** — W-devices, W-routes, W-chat (fabricated-as-real). Fix: real state or truthful
empty/unavailable; never fixtures in production paths.
- **P2** — W2 Support (FAQ + validation, email fallback), W-metrics (init to loading/0),
W-deadctl (wire or remove dead controls), group-ride client-side id generation, mobile
responsiveness pass (Phase 8), telemetry replay deep audit (Phase 4).
- **P3** — marketing `Math.random` counters (label as demo), hardware roadmap labelling.
## Open product questions (do not block P0/P1 truthfulness fixes)
1. Support: confirm no public ticket backend is planned for this release (default: email + FAQ).
2. Device-connections block: remove, or convert to a truthful "manage in mobile app" state?
3. Club routes/chat empty states: plain empty state, or keep clearly-labelled "example" content?

View File

@ -0,0 +1,46 @@
import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import AccountPage from '../app/account/page';
// Account page only needs next/navigation stubbed; firebase-client initialises under the
// staging test env and reports a signed-out user (onAuthStateChanged -> null).
vi.mock('next/navigation', () => ({
usePathname: () => '/account',
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn() }),
}));
describe('WEB-W1b/W-2FA/W-devices: account page truthful states', () => {
it('does not fabricate a connected mobile device, paired comms, or sync status', async () => {
render(<AccountPage />);
await waitFor(() => {
expect(screen.getByText('App Connections & Sync')).toBeDefined();
});
// Fabricated device/pairing claims must be gone.
expect(screen.queryByText(/iPhone 16 Pro/)).toBeNull();
expect(screen.queryByText(/Cardo Packtalk Edge/)).toBeNull();
expect(screen.queryByText(/^Paired$/)).toBeNull();
expect(screen.queryByText(/^14m ago$/)).toBeNull();
expect(screen.queryByText(/^All Synced$/)).toBeNull();
// Truthful states are present instead.
expect(screen.getByText('Authenticated Devices')).toBeDefined();
expect(screen.getByText('Helmet Comms & Accessories')).toBeDefined();
expect(screen.getAllByText(/Unavailable on web/i).length).toBeGreaterThan(0);
});
it('never shows 2FA as ENABLED unless a factor is actually enrolled (signed-out = NOT ENABLED)', async () => {
render(<AccountPage />);
await waitFor(() => {
expect(screen.getByText('Two-Factor Authentication (2FA)')).toBeDefined();
});
// Exact-match: the badge node reads "NOT ENABLED" for a user with no enrolled factor,
// so an exact "ENABLED" node must not exist.
expect(screen.getByText('NOT ENABLED')).toBeDefined();
expect(screen.queryByText('ENABLED')).toBeNull();
});
});