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:
parent
c014c2f04f
commit
217e5a11c0
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { AppCloudData } from '../../components/app-cloud-data';
|
||||
import { FirebaseLink } from '../../components/firebase-link';
|
||||
|
|
@ -22,7 +22,10 @@ export default function AccountPage() {
|
|||
const [syncTrigger, setSyncTrigger] = useState(0);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
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
|
||||
const [sessions, setSessions] = useState<DeviceSession[]>([]);
|
||||
|
|
@ -42,6 +45,18 @@ export default function AccountPage() {
|
|||
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() {
|
||||
setIsLoadingSessions(true);
|
||||
try {
|
||||
|
|
@ -438,14 +453,14 @@ export default function AccountPage() {
|
|||
<div>
|
||||
<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 & Sync</span>
|
||||
<span className="text-xs font-mono text-brand-emerald flex items-center space-x-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-brand-emerald"></span>
|
||||
<span>All Synced</span>
|
||||
<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 ${user ? 'bg-brand-emerald' : 'bg-slate-500'}`}></span>
|
||||
<span>{user ? 'Account linked' : 'Not signed in'}</span>
|
||||
</span>
|
||||
</div>
|
||||
{/* Sync List */}
|
||||
<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="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">
|
||||
|
|
@ -460,37 +475,41 @@ export default function AccountPage() {
|
|||
{user ? 'Connected' : 'Not Connected'}
|
||||
</span>
|
||||
</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="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">
|
||||
<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>
|
||||
<span className="text-white block font-sans font-medium">iOS Mobile App</span>
|
||||
<span className="text-slate-400 text-[11px]">iPhone 16 Pro • v4.1.0</span>
|
||||
<span className="text-white block font-sans font-medium">Authenticated Devices</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>
|
||||
<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>
|
||||
{/* 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="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">
|
||||
<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>
|
||||
<span className="text-white block font-sans font-medium">Helmet Comms Link</span>
|
||||
<span className="text-slate-400 text-[11px]">Cardo Packtalk Edge</span>
|
||||
<span className="text-white block font-sans font-medium">Helmet Comms & Accessories</span>
|
||||
<span className="text-slate-400 text-[11px]">Bluetooth pairing is managed in the mobile app</span>
|
||||
</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 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>
|
||||
<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>
|
||||
</section>
|
||||
|
|
@ -498,7 +517,7 @@ export default function AccountPage() {
|
|||
{/* BEGIN: SecurityAndSessionsGrid */}
|
||||
<section className="grid grid-cols-1 lg:grid-cols-12 gap-6" data-purpose="security-and-session-management">
|
||||
{/* 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>
|
||||
<h3 className="text-lg font-bold text-white">Active Device Sessions</h3>
|
||||
|
|
@ -600,27 +619,31 @@ export default function AccountPage() {
|
|||
</div>
|
||||
<FirebaseLink linked={!!user} />
|
||||
</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>
|
||||
<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>
|
||||
{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-slate-400 bg-slate-800 px-2 py-0.5 rounded border border-slate-700 font-bold">NOT ENABLED</span>
|
||||
)}
|
||||
</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>
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue