MotorSignal-App/website/tests/founder-portal.test.cjs

580 lines
35 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ts = require('typescript');
// Transpile TypeScript modules on the fly
function loadTsModule(relativePath) {
const fullPath = path.resolve(__dirname, relativePath);
const dir = path.dirname(fullPath);
const tsCode = fs.readFileSync(fullPath, 'utf8');
const jsCode = ts.transpileModule(tsCode, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
jsx: ts.JsxEmit.ReactJSX,
},
}).outputText;
const m = { exports: {} };
const customRequire = (specifier) => {
if (specifier === './firebase-client' || specifier.includes('firebase-client')) {
return { firestore: {}, firebaseAuth: { currentUser: null, authStateReady: async () => {} } };
}
if (specifier === 'firebase/firestore') {
return {
collection: () => ({}),
doc: (_db, ...parts) => ({ path: parts.join('/') }),
getDoc: async () => ({ exists: () => false, data: () => ({}) }),
getDocs: async () => ({ docs: [] }),
query: () => ({}),
where: () => ({}),
limit: () => ({}),
collectionGroup: () => ({}),
updateDoc: async () => {},
serverTimestamp: () => new Date(),
};
}
if (specifier.startsWith('.')) {
const resolved = path.resolve(dir, specifier);
if (fs.existsSync(resolved + '.ts')) return loadTsModule(path.relative(__dirname, resolved + '.ts'));
if (fs.existsSync(resolved + '.tsx')) return loadTsModule(path.relative(__dirname, resolved + '.tsx'));
if (fs.existsSync(resolved)) return require(resolved);
}
return require(specifier);
};
const fn = new Function('require', 'exports', 'module', '__filename', '__dirname', jsCode);
fn(customRequire, m.exports, m, fullPath, dir);
return m.exports;
}
console.log('--- STARTING FOUNDER PORTAL TEST SUITE ---');
// 1. Load domain logic & fixtures
const clubData = loadTsModule('../lib/club-data.ts');
const clubFixtures = loadTsModule('../lib/club-fixtures.ts');
const {
getPortalPermissions,
sanitizePortalError,
fetchClubData,
fetchManageableClubs,
fetchFounderClubContext,
updateClubProfile,
} = clubData;
const { FIXTURE_FOUNDER_CLUB, FIXTURE_METRICS } = clubFixtures;
// =========================================================================
// TEST 1: Role-Based Portal Access (Owner vs Admin vs Member vs Unauthenticated)
// =========================================================================
console.log('Testing: Role-based portal access...');
// Unauthenticated
const unauthPerms = getPortalPermissions('member', false);
assert.equal(unauthPerms.isAuthenticated, false);
assert.equal(unauthPerms.canManageSettings, false);
assert.equal(unauthPerms.canInviteMembers, false);
assert.equal(unauthPerms.canApproveApplications, false);
assert.equal(unauthPerms.canPlanRides, false);
assert.equal(unauthPerms.canTransferOwnership, false);
// Authenticated Member (Standard non-admin rider)
const memberPerms = getPortalPermissions('member', true);
assert.equal(memberPerms.isAuthenticated, true);
assert.equal(memberPerms.role, 'member');
assert.equal(memberPerms.canManageSettings, false, 'Member must not manage settings');
assert.equal(memberPerms.canInviteMembers, false, 'Member must not invite members');
assert.equal(memberPerms.canApproveApplications, false, 'Member must not approve applications');
assert.equal(memberPerms.canPlanRides, false, 'Member must not plan club rides');
assert.equal(memberPerms.canTransferOwnership, false, 'Member must not transfer ownership');
// Authenticated Admin
const adminPerms = getPortalPermissions('admin', true);
assert.equal(adminPerms.isAuthenticated, true);
assert.equal(adminPerms.role, 'admin');
assert.equal(adminPerms.canManageSettings, true, 'Admin can manage general settings');
assert.equal(adminPerms.canInviteMembers, true, 'Admin can invite members');
assert.equal(adminPerms.canApproveApplications, true, 'Admin can approve applications');
assert.equal(adminPerms.canPlanRides, true, 'Admin can plan club rides');
assert.equal(adminPerms.canTransferOwnership, false, 'Admin MUST NOT transfer ownership');
// Authenticated Owner
const ownerPerms = getPortalPermissions('owner', true);
assert.equal(ownerPerms.isAuthenticated, true);
assert.equal(ownerPerms.role, 'owner');
assert.equal(ownerPerms.canManageSettings, true);
assert.equal(ownerPerms.canInviteMembers, true);
assert.equal(ownerPerms.canApproveApplications, true);
assert.equal(ownerPerms.canPlanRides, true);
assert.equal(ownerPerms.canTransferOwnership, true, 'Only Owner can transfer ownership');
console.log(' PASS: Role-based permissions strictly enforced (Owner, Admin, Member, Unauth).');
// =========================================================================
// TEST 2: Club Display Rank != Administrative Role (Rank never grants access)
// =========================================================================
console.log('Testing: Club Rank does not escalate software permissions...');
// If a member has Display Rank 'President' but software role 'member':
const spoofedRankPerms = getPortalPermissions('member', true);
assert.equal(spoofedRankPerms.canApproveApplications, false);
assert.equal(spoofedRankPerms.canTransferOwnership, false);
console.log(' PASS: Club Rank is purely display/organizational; software permissions derive only from role.');
// =========================================================================
// TEST 3: Safe Error Sanitization (No raw backend exception leakage)
// =========================================================================
console.log('Testing: Safe error sanitization & leak prevention...');
const leaks = [
'FirebaseException: [cloud_firestore/permission-denied] Missing or insufficient permissions at /databases/(default)/documents/clubs/ms-atl-01',
'FAILED_PRECONDITION: The query requires an index.',
'Error: connect ECONNREFUSED 127.0.0.1:8080 at TCPConnectWrap.afterConnect [as oncomplete]',
'DocumentNotFoundException: document /databases/(default)/documents/clubs/unknown was not found',
'TypeError: Cannot read property uid of undefined at club_service.dart:142',
];
for (const rawLeak of leaks) {
const sanitized = sanitizePortalError(rawLeak);
assert(!sanitized.includes('FirebaseException'), `Leaked FirebaseException in: ${sanitized}`);
assert(!sanitized.includes('FAILED_PRECONDITION'), `Leaked FAILED_PRECONDITION in: ${sanitized}`);
assert(!sanitized.includes('/databases/'), `Leaked Firestore path in: ${sanitized}`);
assert(!sanitized.includes('127.0.0.1'), `Leaked network internals in: ${sanitized}`);
assert(!sanitized.includes('.dart:'), `Leaked stack trace in: ${sanitized}`);
assert(
sanitized === 'Club information is restricted or requires authentication.' ||
sanitized === "Club information couldn't be loaded. Check your network connection." ||
sanitized === 'Club information could not be found.' ||
sanitized === 'Something went wrong. Please try again.',
`Unexpected sanitized message: ${sanitized}`
);
}
console.log(' PASS: Zero raw backend exception or Firestore path leakage.');
// =========================================================================
// TEST 4: No Silent Production Demo Fallbacks
// =========================================================================
async function runAllTests() {
console.log('Testing: No silent production demo fallbacks...');
let threw = false;
try {
await fetchClubData('nonexistent-club-id');
} catch (err) {
threw = true;
assert.equal(err.message, 'Club information could not be found.');
}
assert.equal(threw, true, 'fetchClubData must throw not-found instead of returning demo club');
console.log(' PASS: Production runtime strictly refuses silent demo fallbacks.');
// =========================================================================
// TEST 5: ClubMember != GroupRideParticipant Invariant & Location Privacy
// =========================================================================
console.log('Testing: ClubMember != GroupRideParticipant & Location Privacy Invariant...');
// Read ClubRideCard source to verify exact verbatim notice text
const rideCardSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/club-ride-card.tsx'), 'utf8');
const verbatimNotice = 'Live location is shared only with eligible ride participants while sharing is enabled. Club membership alone never grants access to rider location.';
assert(rideCardSrc.includes(verbatimNotice), 'Missing verbatim live location privacy notice in ClubRideCard');
// Verify that forbidden claims are NOT made in ClubRideCard
assert(!rideCardSrc.includes('end-to-end encryption'), 'ClubRideCard must not falsely claim end-to-end encryption');
assert(!rideCardSrc.includes('RTK'), 'ClubRideCard must not falsely claim RTK');
assert(!rideCardSrc.includes('mesh networking'), 'ClubRideCard must not falsely claim mesh networking');
console.log(' PASS: Verbatim live location privacy notice verified; no unverified claims present.');
// =========================================================================
// TEST 6: Club Mileage Privacy Behavior & Verified Badge Removal
// =========================================================================
console.log('Testing: Club Miles privacy behavior & badge hardening...');
const metricCardSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/metric-card.tsx'), 'utf8');
const mileageDisclaimer = 'Calculated from eligible completed Club Rides. Private rider mileage is not automatically included.';
assert(metricCardSrc.includes(mileageDisclaimer), 'Missing mandatory Club Miles privacy disclaimer');
assert(metricCardSrc.includes('Completed Club Rides only'), 'Missing "Completed Club Rides only" badge text');
assert(!metricCardSrc.includes('VERIFIED'), 'MetricCard must NOT contain misleading "VERIFIED" badge');
console.log(' PASS: Club Miles strict privacy semantics enforced (eligible completed rides only, no false VERIFIED badge).');
// =========================================================================
// TEST 7: Roles & Ranks Architecture Note & No Fabricated Numbers
// =========================================================================
console.log('Testing: Roles & Ranks separation architecture note...');
const rolesRanksSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/roles-and-ranks-summary.tsx'), 'utf8');
const verbatimRbacNote = 'Administrative roles determine software permissions. Club ranks are organizational titles and do not grant additional access.';
assert(rolesRanksSrc.includes(verbatimRbacNote), 'Missing verbatim RBAC architecture note');
assert(rolesRanksSrc.includes('ADMINISTRATIVE ROLES (Software Permissions)'), 'Missing software permissions section');
assert(rolesRanksSrc.includes('CLUB RANKS (ORGANIZATIONAL TITLES)'), 'Missing organizational titles section');
assert(!rolesRanksSrc.includes('106 Members'), 'RolesAndRanksSummary must not contain hardcoded 106 Members fabrication');
console.log(' PASS: Roles vs Ranks separation and verbatim architecture note verified (no fabricated counts).');
// =========================================================================
// TEST 8: Top Bar Dev-Gating
// =========================================================================
console.log('Testing: Top Bar role preview switcher dev-gating...');
const topBarSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/founder-top-bar.tsx'), 'utf8');
assert(topBarSrc.includes("process.env.NODE_ENV === 'development'"), 'Top bar role preview must be dev-gated');
console.log(' PASS: Top Bar role preview switcher strictly dev-gated.');
// =========================================================================
// TEST 9: Dynamic /clubs/manage Discovery & Page Guards
// =========================================================================
console.log('Testing: /clubs/manage discovery route & access guards...');
const manageDiscoverySrc = fs.readFileSync(path.resolve(__dirname, '../app/clubs/manage/page.tsx'), 'utf8');
assert(!manageDiscoverySrc.includes("redirect('/clubs/motorsignal-atlanta/manage')"), 'Must NOT have static redirect to Atlanta');
assert(manageDiscoverySrc.includes('fetchManageableClubs'), 'Must discover manageable clubs via fetchManageableClubs');
assert(manageDiscoverySrc.includes('No Clubs to Manage'), 'Must render "No Clubs to Manage" empty state');
assert(manageDiscoverySrc.includes('Select Club to Manage'), 'Must render Club Selector for >1 clubs');
// FP-1.2: Administrative Role Terminology (Owner, Admin, Member - NEVER Officer)
assert(manageDiscoverySrc.includes("c.role === 'owner' ? 'Owner' : 'Admin'"), 'Manageable-Club selector must map admin role to "Admin", NOT "Officer"');
assert(!manageDiscoverySrc.includes("? 'Owner' : 'Officer'"), 'Manageable-Club selector must NEVER map admin role to "Officer"');
assert(!manageDiscoverySrc.includes("Owner or Officer"), 'Manageable-Club selector copy must say Owner or Admin, never Owner or Officer');
const managePageSrc = fs.readFileSync(path.resolve(__dirname, '../app/clubs/[clubId]/manage/page.tsx'), 'utf8');
assert(managePageSrc.includes('Management Access Restricted'), 'Must guard against non-admin member access');
const clubsPageSrc = fs.readFileSync(path.resolve(__dirname, '../app/clubs/page.tsx'), 'utf8');
assert(clubsPageSrc.includes('href="/clubs/manage"'), 'Missing link to /clubs/manage on /clubs page');
console.log(' PASS: /clubs/manage dynamic discovery, role terminology (Owner/Admin/Member), and access control verified.');
// =========================================================================
// TEST 10: Responsive Layout Breakpoints
// =========================================================================
console.log('Testing: Responsive layout classes across 1440, 1024, 768, and mobile...');
const shellSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/founder-portal-shell.tsx'), 'utf8');
const sidebarSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/founder-sidebar.tsx'), 'utf8');
// Desktop persistent sidebar: lg:pl-72, lg:translate-x-0, max-w-[1440px]
assert(shellSrc.includes('lg:pl-72'), 'Missing lg:pl-72 desktop sidebar offset');
assert(shellSrc.includes('max-w-[1440px]'), 'Missing max-w-[1440px] desktop canvas container');
assert(sidebarSrc.includes('w-72'), 'Missing w-72 persistent sidebar width');
// Mobile drawer: translate-x-full lg:translate-x-0, touch targets
assert(sidebarSrc.includes('-translate-x-full lg:translate-x-0'), 'Missing mobile drawer translation classes');
assert(sidebarSrc.includes('fixed left-0 top-0 h-full'), 'Missing fixed sidebar drawer layout');
console.log(' PASS: Responsive shell verified for 1440px desktop, 1024px tablet, and mobile off-canvas drawer.');
// =========================================================================
// TEST 11: FP-1.3 Founder Identity Contract
// =========================================================================
console.log('Testing: FP-1.3 Founder Identity Contract (/public_profiles point-read & conservative fallback)...');
const clubDataSrc = fs.readFileSync(path.resolve(__dirname, '../lib/club-data.ts'), 'utf8');
const typesSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/types.ts'), 'utf8');
// Verify ownerUid is documented as ownership identity only, not display name
assert(typesSrc.includes('Identifies ownership identity only'), 'Club.ownerUid must document ownership identity only');
assert(typesSrc.includes('Canonical profile resolution: /public_profiles/{ownerUid}'), 'Club must document profile resolution path');
assert(typesSrc.includes("Never persist a redundant 'founderName' field on /clubs/{clubId}"), 'Club must forbid redundant founderName in schema');
// Verify club-data.ts point-reads public_profiles/{ownerUid}
assert(clubDataSrc.includes("doc(firestore, 'public_profiles', d.ownerUid)"), 'fetchClubData must perform point read to public_profiles/{ownerUid}');
assert(clubDataSrc.includes("founderName = d.ownerUid === callerUid ? 'You' : 'Club Founder'"), 'Must maintain conservative fallback if profile not resolved');
console.log(' PASS: Founder identity contract verified (ownerUid is identity only; profile read via /public_profiles/{ownerUid}; conservative fallback).');
// =========================================================================
// TEST 12: FP-1.3 Club Ride Association Contract (GroupRide.clubId == club.id)
// =========================================================================
console.log('Testing: FP-1.3 Canonical Club Ride Association (GroupRide.clubId == club.id)...');
// Verify canonical query in club-data.ts
assert(clubDataSrc.includes("where('clubId', '==', resolvedClubId)"), 'Upcoming rides must query strictly where clubId == resolvedClubId');
assert(typesSrc.includes('Authoritative link: GroupRide.clubId == club.id'), 'Upcoming rides type must document canonical link');
assert(typesSrc.includes('Affiliation is NEVER inferred from accepted rosters'), 'Must document non-inference from rosters');
assert(typesSrc.includes('ClubMember != GroupRideParticipant'), 'Must document ClubMember != GroupRideParticipant invariant');
console.log(' PASS: Club Ride Association strictly bound to GroupRide.clubId == club.id (never inferred from rosters/accessUids).');
// =========================================================================
// TEST 13: FP-1.3 Route Distance & Duration Storage Contract
// =========================================================================
console.log('Testing: FP-1.3 Route Distance & Duration Storage Contract...');
// Verify types.ts documents persisted straightLineDistance vs optional Valhalla road estimates
assert(typesSrc.includes('straightLineDistanceMeters: Geodesic Haversine distance between ordered stops (persisted)'), 'Must document straightLineDistanceMeters contract');
assert(typesSrc.includes('estimatedDistanceMeters & estimatedDurationSeconds: Optional Valhalla road estimates'), 'Must document optional road estimates contract');
// Verify club-ride-card renders '--' when metrics are unpopulated
assert(rideCardSrc.includes("ride.distanceMiles ? `${ride.distanceMiles} mi` : '--'"), 'ClubRideCard must render -- when distance is unpopulated');
assert(rideCardSrc.includes("ride.durationString ?? '--'"), 'ClubRideCard must render -- when duration is unpopulated');
console.log(' PASS: Route distance & duration contract verified (persisted straight-line vs optional road estimates; fallback to --).');
// =========================================================================
// TEST 14 (Test A): FP-13 Profile Authorization Guard
// =========================================================================
console.log('Testing: FP-13 Test A - Profile Authorization Guard...');
// Owner and Admin allowed
const ownerPerms14 = getPortalPermissions('owner', true);
assert.equal(ownerPerms14.canManageSettings, true, 'Owner must have canManageSettings');
const adminPerms14 = getPortalPermissions('admin', true);
assert.equal(adminPerms14.canManageSettings, true, 'Admin must have canManageSettings');
// Member (non-admin) forbidden
const memberPerms14 = getPortalPermissions('member', true);
assert.equal(memberPerms14.canManageSettings, false, 'Member must NOT have canManageSettings');
// Unauthenticated forbidden
const unauthPerms14 = getPortalPermissions('member', false);
assert.equal(unauthPerms14.isAuthenticated, false, 'Unauth must not be authenticated');
assert.equal(unauthPerms14.canManageSettings, false, 'Unauth must NOT have canManageSettings');
// Verify page component inspects auth and permissions
const profilePageSrc = fs.readFileSync(path.resolve(__dirname, '../app/clubs/[clubId]/manage/profile/page.tsx'), 'utf8');
assert(profilePageSrc.includes('!isDemoPreview && !currentUser'), 'Profile page must check authentication');
assert(profilePageSrc.includes('title="Authentication Required"'), 'Profile page must render Authentication Required error');
assert(profilePageSrc.includes("router.push('/login')"), 'Profile page must direct unauthenticated users to /login');
assert(profilePageSrc.includes('!isDemoPreview && !ctx.permissions.canManageSettings'), 'Profile page must check canManageSettings permission');
assert(profilePageSrc.includes('title="Management Access Restricted"'), 'Profile page must render Management Access Restricted error');
console.log(' PASS: Test A verified (Owner/Admin allowed; Member forbidden; Unauthenticated rejected with SafeErrorState -> /login).');
// =========================================================================
// TEST 15 (Test B): FP-13 Profile Query Gate (O(1) Point-Read Only)
// =========================================================================
console.log('Testing: FP-13 Test B - Profile Query Gate (O(1) Point-Read Only)...');
// Extract fetchFounderClubContext body from club-data.ts source
const fetchCtxMatch = clubDataSrc.match(/export async function fetchFounderClubContext[\s\S]*?^}/m);
assert(fetchCtxMatch, 'fetchFounderClubContext must exist in club-data.ts');
const fetchCtxBody = fetchCtxMatch[0];
// Forbid collection queries in profile context
assert(!fetchCtxBody.includes('group_rides'), 'fetchFounderClubContext must NOT query group_rides');
assert(!fetchCtxBody.includes('club_applications'), 'fetchFounderClubContext must NOT query club_applications');
assert(!fetchCtxBody.includes('collectionGroup'), 'fetchFounderClubContext must NOT perform collectionGroup queries');
assert(!fetchCtxBody.includes('getDocs('), 'fetchFounderClubContext must NOT call getDocs (strictly point-reads getDoc only)');
console.log(' PASS: Test B verified (Zero collection queries; zero group_rides; zero club_applications; strictly O(1) point-reads).');
// =========================================================================
// TEST 16 (Test C): FP-13 Profile Field Mutability & Whitelist Enforcement
// =========================================================================
console.log('Testing: FP-13 Test C - Profile Field Mutability & Whitelist Enforcement...');
// Test input validations via updateClubProfile
await assert.rejects(
async () => updateClubProfile('test-club', '', { name: 'Valid Name', description: '', homeRegion: '', motorcycleFocus: '' }),
/Authentication required/,
'Must require callerUid'
);
await assert.rejects(
async () => updateClubProfile('test-club', 'uid-1', { name: 'ab', description: '', homeRegion: '', motorcycleFocus: '' }),
/Club name must be between 3 and 50 characters/,
'Must reject name shorter than 3 chars'
);
await assert.rejects(
async () => updateClubProfile('test-club', 'uid-1', { name: 'A'.repeat(51), description: '', homeRegion: '', motorcycleFocus: '' }),
/Club name must be between 3 and 50 characters/,
'Must reject name longer than 50 chars'
);
await assert.rejects(
async () => updateClubProfile('test-club', 'uid-1', { name: 'Valid Club', description: 'A'.repeat(501), homeRegion: '', motorcycleFocus: '' }),
/Club description must not exceed 500 characters/,
'Must reject description longer than 500 chars'
);
await assert.rejects(
async () => updateClubProfile('test-club', 'uid-1', { name: 'Valid Club', description: '', homeRegion: 'A'.repeat(101), motorcycleFocus: '' }),
/Home region must not exceed 100 characters/,
'Must reject homeRegion longer than 100 chars'
);
await assert.rejects(
async () => updateClubProfile('test-club', 'uid-1', { name: 'Valid Club', description: '', homeRegion: '', motorcycleFocus: 'A'.repeat(101) }),
/Motorcycle focus must not exceed 100 characters/,
'Must reject motorcycleFocus longer than 100 chars'
);
// Verify updateClubProfile implementation in club-data.ts strictly writes whitelisted fields
const updateMatch = clubDataSrc.match(/export async function updateClubProfile[\s\S]*?^}/m);
assert(updateMatch, 'updateClubProfile must exist in club-data.ts');
const updateBody = updateMatch[0];
assert(updateBody.includes('updateDoc(clubRef, {'), 'updateClubProfile must call updateDoc');
assert(!updateBody.includes('slug:'), 'updateClubProfile MUST NOT write slug');
assert(!updateBody.includes('visibility:'), 'updateClubProfile MUST NOT write visibility');
assert(!updateBody.includes('admissionPolicy:'), 'updateClubProfile MUST NOT write admissionPolicy');
assert(!updateBody.includes('ownerUid:'), 'updateClubProfile MUST NOT write ownerUid');
assert(!updateBody.includes('memberCount:'), 'updateClubProfile MUST NOT write memberCount');
assert(!updateBody.includes('archivedAt:'), 'updateClubProfile MUST NOT write archivedAt');
console.log(' PASS: Test C verified (Strict input length bounds, whitelisted update, forbidden fields rejected).');
// =========================================================================
// TEST 17 (Test D): FP-13 Development Preview Fixture Safety
// =========================================================================
console.log('Testing: FP-13 Test D - Development Preview Fixture Safety...');
// In development with allowDemoPreview: true -> returns fixture
const prevEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
const demoCtx = await fetchFounderClubContext('any-id', undefined, { allowDemoPreview: true });
assert.equal(demoCtx.isDemoFixture, true, 'Must return isDemoFixture: true when opt-in in development');
assert.equal(demoCtx.club.name, FIXTURE_FOUNDER_CLUB.name);
// In production with allowDemoPreview: true -> fixture MUST NOT be returned
process.env.NODE_ENV = 'production';
let prodThrew = false;
try {
await fetchFounderClubContext('non-existent-club-id', undefined, { allowDemoPreview: true });
} catch {
prodThrew = true;
}
assert.equal(prodThrew, true, 'Fixture MUST NEVER be returned in production, even if allowDemoPreview is set');
process.env.NODE_ENV = prevEnv;
console.log(' PASS: Test D verified (Fixture returned strictly in development with explicit allowDemoPreview opt-in; never in production).');
// =========================================================================
// TEST 18 (Test E): FP-13 Shared Sub-Routing & Active Navigation State
// =========================================================================
console.log('Testing: FP-13 Test E - Shared Sub-Routing & Active Navigation State...');
// Verify route links exist in FounderSidebar
assert(sidebarSrc.includes('getRouteHref'), 'FounderSidebar must include getRouteHref');
assert(sidebarSrc.includes('/manage/profile'), 'FounderSidebar must route to /manage/profile');
assert(sidebarSrc.includes('/manage'), 'FounderSidebar must route to /manage');
// Verify exact active styling for profile
assert(sidebarSrc.includes('bg-surface-container-high text-primary-container border-l-2 border-primary-container font-medium'), 'Profile item must have active highlight border and background');
// Verify profile page passes activeNav="profile"
assert(profilePageSrc.includes('activeNav="profile"'), 'Profile page must pass activeNav="profile" to FounderPortalShell');
assert(profilePageSrc.includes('<FounderPortalShell'), 'Profile page must reuse FounderPortalShell (not duplicate shell)');
console.log(' PASS: Test E verified (Shared sub-routing, activeNav="profile", shell reuse without duplication).');
// =========================================================================
// TEST 19 (Test F): FP-13 Privacy Boundary Contract
// =========================================================================
console.log('Testing: FP-13 Test F - Privacy Boundary Contract...');
const editorSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/founder-profile-editor.tsx'), 'utf8');
// Verify verbatim privacy contract statement
assert(editorSrc.includes('Public Club Profile is organizational identity only. Member telemetry, GPS tracks, personal ride logs, and private account data are not included.'), 'Editor must contain canonical Privacy Contract statement');
assert(editorSrc.includes('PRIVACY CONTRACT'), 'Editor must have PRIVACY CONTRACT section');
// Verify zero telemetry leakage in editor
assert(!editorSrc.includes('leanAngle'), 'Profile editor must not reference telemetry leanAngle');
assert(!editorSrc.includes('speedMph'), 'Profile editor must not reference telemetry speedMph');
assert(!editorSrc.includes('gForce'), 'Profile editor must not reference telemetry gForce');
console.log(' PASS: Test F verified (Privacy contract displayed, personal telemetry/GPS strictly isolated).');
// =========================================================================
// TEST 20 (Test G): FP-13 Public Directory Preview Contract
// =========================================================================
console.log('Testing: FP-13 Test G - Public Directory Preview Contract...');
// Verify Directory Preview exists and renders identity
assert(editorSrc.includes('DIRECTORY PREVIEW'), 'Editor must include DIRECTORY PREVIEW');
assert(editorSrc.includes('LIVE SIMULATION'), 'Editor must display LIVE SIMULATION badge');
assert(editorSrc.includes('@{club.slug}'), 'Directory preview must render club handle');
assert(editorSrc.includes('initials'), 'Directory preview must render emblem initials');
// Verify ZERO aggregate metrics in directory preview
const previewMatch = editorSrc.match(/CARD A: PUBLIC DIRECTORY PREVIEW[\s\S]*?CARD B: ACCESS/);
assert(previewMatch, 'Card A Directory Preview section must be identifiable');
const previewBody = previewMatch[0];
assert(!previewBody.includes('club.memberCount'), 'Directory Preview MUST NOT render member count');
assert(!previewBody.includes('MEMBERS'), 'Directory Preview MUST NOT render MEMBERS metric');
assert(!previewBody.includes('UPCOMING RIDES'), 'Directory Preview MUST NOT render UPCOMING RIDES metric');
assert(!previewBody.includes('TOTAL MILES'), 'Directory Preview MUST NOT render TOTAL MILES metric');
console.log(' PASS: Test G verified (Directory preview renders organizational identity only; zero aggregate metrics).');
// =========================================================================
// TEST 21: FP-13.1 Club Profile Hardening Suite (Tests A through J)
// =========================================================================
console.log('Testing: FP-13.1 Hardening Suite (Tests A - J)...');
// A. FounderPortalShell does not contain STATUS: NOMINAL or global metric footer
assert(!shellSrc.includes('STATUS: NOMINAL'), 'FounderPortalShell must NOT contain STATUS: NOMINAL');
assert(!shellSrc.includes('MEMBERS'), 'FounderPortalShell must NOT contain global MEMBERS metric');
assert(!shellSrc.includes('UPCOMING RIDES'), 'FounderPortalShell must NOT contain global UPCOMING RIDES metric');
assert(!shellSrc.includes('ACCESSIBLE ROUTES'), 'FounderPortalShell must NOT contain global ACCESSIBLE ROUTES metric');
assert(!shellSrc.includes('<footer'), 'FounderPortalShell must NOT contain global operational status footer');
// B. Profile contains no unsupported verified badge
assert(!editorSrc.includes('title="Verified Syndicate"'), 'Profile must NOT contain Verified Syndicate title');
assert(!editorSrc.includes('verified'), 'Profile must NOT contain verified checkmark badge');
// C. Profile contains no FP-13 / MODULE implementation terminology
assert(!editorSrc.includes('FP-13'), 'Profile must NOT contain FP-13 terminology');
assert(!editorSrc.includes('FP-13.1'), 'Profile must NOT contain FP-13.1 terminology');
assert(!editorSrc.includes('MODULE'), 'Profile must NOT contain MODULE terminology');
assert(!editorSrc.includes('STAGE'), 'Profile must NOT contain STAGE terminology');
// D. Profile contains no internal Club document/registry ID
assert(!editorSrc.includes('REGISTRY REF'), 'Profile must NOT contain REGISTRY REF');
assert(!editorSrc.includes('club.id.slice'), 'Profile must NOT expose truncated Club document ID');
// E. Profile contains no unsupported media format/storage labels
assert(!editorSrc.includes('VECTOR_BOUND'), 'Profile must NOT contain VECTOR_BOUND');
assert(!editorSrc.includes('CANVAS // TELEMETRY DUSK'), 'Profile must NOT contain CANVAS // TELEMETRY DUSK');
assert(!editorSrc.includes('512x512 PNG'), 'Profile must NOT contain 512x512 PNG requirement');
assert(!editorSrc.includes('16:5 RATIO'), 'Profile must NOT contain 16:5 RATIO label');
assert(!editorSrc.includes('lh3.googleusercontent.com'), 'Profile must NOT contain fabricated stock car photo');
// F. Directory Preview remains identity-only
assert(!previewBody.includes('club.memberCount'), 'Directory Preview MUST NOT render member count');
assert(!previewBody.includes('MEMBERS'), 'Directory Preview MUST NOT render MEMBERS metric');
assert(!previewBody.includes('UPCOMING RIDES'), 'Directory Preview MUST NOT render UPCOMING RIDES metric');
assert(!previewBody.includes('TOTAL MILES'), 'Directory Preview MUST NOT render TOTAL MILES metric');
assert(!previewBody.includes('verified'), 'Directory Preview MUST NOT render verified badge');
// G. visibility/admission remain non-editable
assert(!updateBody.includes('visibility:'), 'updateClubProfile MUST NOT write visibility');
assert(!updateBody.includes('admissionPolicy:'), 'updateClubProfile MUST NOT write admissionPolicy');
assert(!editorSrc.includes('name="visibility"'), 'Profile editor must NOT have visibility input');
assert(!editorSrc.includes('name="admissionPolicy"'), 'Profile editor must NOT have admissionPolicy input');
// H. O(1) point-read contract unchanged
assert(!fetchCtxBody.includes('group_rides'), 'fetchFounderClubContext must NOT query group_rides');
assert(!fetchCtxBody.includes('club_applications'), 'fetchFounderClubContext must NOT query club_applications');
assert(!fetchCtxBody.includes('collectionGroup'), 'fetchFounderClubContext must NOT perform collectionGroup queries');
assert(!fetchCtxBody.includes('getDocs('), 'fetchFounderClubContext must NOT call getDocs');
// I. profile mutation whitelist unchanged
assert(updateBody.includes('updateDoc(clubRef, {'), 'updateClubProfile must call updateDoc');
assert(!updateBody.includes('slug:'), 'updateClubProfile MUST NOT write slug');
assert(!updateBody.includes('ownerUid:'), 'updateClubProfile MUST NOT write ownerUid');
assert(!updateBody.includes('memberCount:'), 'updateClubProfile MUST NOT write memberCount');
assert(!updateBody.includes('archivedAt:'), 'updateClubProfile MUST NOT write archivedAt');
// J. production fixture isolation unchanged
assert.equal(demoCtx.isDemoFixture, true, 'Must return isDemoFixture: true in development with opt-in');
assert.equal(prodThrew, true, 'Fixture MUST NEVER be returned in production');
console.log(' PASS: FP-13.1 Tests A through J verified (Footer removed, verified badge removed, stage terms removed, internal ID removed, media labels removed, preview identity-only, non-editable visibility/admission, O(1) reads, whitelist mutation, fixture safety).');
console.log('--- ALL FOUNDER PORTAL TESTS PASSED (21/21) ---');
}
runAllTests().catch((err) => {
console.error('Test Suite Failure:', err);
process.exit(1);
});