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

1079 lines
70 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(),
runTransaction: async (_db, cb) => cb({ get: async () => ({ exists: () => false }), update: () => {} }),
};
}
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,
updateClubAccessSettings,
fetchEligibleTransferMembers,
transferClubOwnership,
archiveClub,
fetchClubApplications,
approveClubApplication,
declineClubApplication,
validateClubImageUrl,
fetchClubRides,
} = clubData;
const { FIXTURE_FOUNDER_CLUB, FIXTURE_METRICS, FIXTURE_UPCOMING_RIDES, FIXTURE_PAST_RIDES } = 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-[1536px]
assert(shellSrc.includes('lg:pl-72'), 'Missing lg:pl-72 desktop sidebar offset');
assert(shellSrc.includes('max-w-[1536px]'), 'FounderPortalShell must use max-w-[1536px] desktop canvas container');
assert(!shellSrc.includes('max-w-[1440px]'), 'FounderPortalShell must NOT retain old max-w-[1440px] cap');
assert(shellSrc.includes('founder-portal-root'), 'Missing founder-portal-root class on shell');
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 Architecture Status
// =========================================================================
console.log('Testing: FP-1.3 Club Ride Association Architecture Status...');
assert(typesSrc.includes('Production GroupRide documents support optional immutable clubId affiliation'), 'Types must document live optional GroupRide.clubId support');
assert(typesSrc.includes('GroupRide.clubId == club.id'), 'Types must document the canonical Club affiliation link');
assert(typesSrc.includes('Broad client-side Club discovery remains deferred for mutual-block privacy'), 'Types must document the deferred discovery boundary');
assert(typesSrc.includes('ClubMember != GroupRideParticipant'), 'Must document ClubMember != GroupRideParticipant invariant');
console.log(' PASS: Club affiliation is documented as live while broad privacy-preserving discovery remains deferred.');
// =========================================================================
// 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).');
// =========================================================================
// TEST 22: FP-14 Club Settings Implementation Suite (Tests A through X)
// =========================================================================
console.log('Testing: FP-14 Settings Suite (Tests A - X)...');
const settingsPageSrc = fs.readFileSync(path.resolve(__dirname, '../app/clubs/[clubId]/manage/settings/page.tsx'), 'utf8');
const settingsEditorSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/founder-settings-editor.tsx'), 'utf8');
// A. Settings auth guard
assert(settingsPageSrc.includes('firebaseAuth.currentUser'), 'Settings page must check firebaseAuth.currentUser');
assert(settingsPageSrc.includes('setUnauthenticated(true)'), 'Settings page must set unauthenticated on missing user');
assert(settingsPageSrc.includes('/login'), 'Settings page must direct unauthenticated users to /login');
// B. Owner/Admin page authorization
assert(settingsPageSrc.includes('ctx.permissions.canManageSettings'), 'Settings page must check canManageSettings');
const ownerPerms = getPortalPermissions('owner');
const adminPerms = getPortalPermissions('admin');
assert.equal(ownerPerms.canManageSettings, true, 'Owner must be authorized to manage settings');
assert.equal(adminPerms.canManageSettings, true, 'Admin must be authorized to access settings page');
// C. Member/non-member denial
const memberPerms = getPortalPermissions('member');
const unauthPerms = getPortalPermissions('member', false);
assert.equal(memberPerms.canManageSettings, false, 'Member must be denied settings management access');
assert.equal(unauthPerms.canManageSettings, false, 'Unauthenticated caller must be denied settings access');
assert(settingsPageSrc.includes('Management Access Restricted'), 'Settings page must show restricted state for members');
// D. visibility enum exactly public/unlisted/private
const updateAccessBody = clubDataSrc.substring(clubDataSrc.indexOf('function updateClubAccessSettings'), clubDataSrc.indexOf('function fetchEligibleTransferMembers'));
assert(updateAccessBody.includes("'public', 'unlisted', 'private'"), 'updateClubAccessSettings must accept only public, unlisted, private');
// E. admission enum exactly open/request/inviteOnly
assert(updateAccessBody.includes("'open', 'request', 'inviteOnly'"), 'updateClubAccessSettings must accept only open, request, inviteOnly');
// F. access mutation whitelist
assert(updateAccessBody.includes('updateDoc(clubRef, {'), 'updateClubAccessSettings must call updateDoc');
assert(updateAccessBody.includes('visibility: input.visibility,'), 'updateClubAccessSettings must write visibility');
assert(updateAccessBody.includes('admissionPolicy: input.admissionPolicy,'), 'updateClubAccessSettings must write admissionPolicy');
assert(updateAccessBody.includes('updatedAt: serverTimestamp(),'), 'updateClubAccessSettings must write updatedAt');
assert(updateAccessBody.includes("m.role === 'owner' || m.role === 'admin'"), 'updateClubAccessSettings must authorize active owner and active admin');
assert(updateAccessBody.includes("m.status === 'active'"), 'updateClubAccessSettings must verify active membership status');
assert(!updateAccessBody.includes('slug:'), 'updateClubAccessSettings MUST NOT write slug');
assert(!updateAccessBody.includes('ownerUid:'), 'updateClubAccessSettings MUST NOT write ownerUid');
assert(!updateAccessBody.includes('memberCount:'), 'updateClubAccessSettings MUST NOT write memberCount');
assert(!updateAccessBody.includes('archivedAt:'), 'updateClubAccessSettings MUST NOT write archivedAt');
assert(!updateAccessBody.includes('name:'), 'updateClubAccessSettings MUST NOT write name');
assert(!updateAccessBody.includes('description:'), 'updateClubAccessSettings MUST NOT write description');
// G. slug immutable
assert(settingsEditorSrc.includes('IMMUTABLE RESERVATION'), 'Settings editor must show immutable reservation badge for slug');
assert(settingsEditorSrc.includes('Club handles are permanently reserved and cannot be renamed or reassigned.'), 'Settings editor must contain canonical immutable handle copy');
assert(!settingsEditorSrc.includes('name="slug"'), 'Settings editor must not have slug input');
// H. rank does not escalate permissions
assert.equal(memberPerms.canTransferOwnership, false, 'Rank does not grant transfer ownership');
assert.equal(memberPerms.canManageSettings, false, 'Rank does not grant manage settings');
assert(settingsEditorSrc.includes('canManageAccess'), 'Settings editor must allow access settings management for owner and admin');
assert(settingsEditorSrc.includes('!canManageAccess'), 'Settings editor must disable access settings for non-leadership');
// I. owner-only ownership transfer
assert.equal(ownerPerms.canTransferOwnership, true, 'Owner can transfer ownership');
assert.equal(adminPerms.canTransferOwnership, false, 'Admin cannot transfer ownership');
const transferBody = clubDataSrc.substring(clubDataSrc.indexOf('function transferClubOwnership'), clubDataSrc.indexOf('function archiveClub'));
assert(transferBody.includes('cd.ownerUid !== currentOwnerUid'), 'transferClubOwnership must check caller is owner');
// J. transfer target must be active member
assert(transferBody.includes("md.status !== 'active'"), 'transferClubOwnership must verify target is active member');
// K. ownership transfer role transition semantics
assert(transferBody.includes("ownerUid: newOwnerUid"), 'transferClubOwnership must update club root ownerUid to newOwnerUid');
assert(transferBody.includes("role: 'admin'"), 'transferClubOwnership must demote current owner to admin');
assert(transferBody.includes("role: 'owner'"), 'transferClubOwnership must promote new owner to owner');
// L. admin cannot transfer ownership
assert(settingsEditorSrc.includes('Transfer Ownership...'), 'Settings editor must include Transfer Ownership trigger');
assert(settingsEditorSrc.includes('!isOwner'), 'Transfer trigger must be disabled for non-owners');
// M. owner-only archive
assert.equal(ownerPerms.role, 'owner', 'Owner role is owner');
assert.equal(adminPerms.role, 'admin', 'Admin role is admin');
assert.equal(ownerPerms.canTransferOwnership, true, 'Owner has exclusive owner transfer authority');
assert.equal(adminPerms.canTransferOwnership, false, 'Admin lacks owner authority');
const archiveBody = clubDataSrc.substring(clubDataSrc.indexOf('function archiveClub'));
assert(archiveBody.includes('cd.ownerUid !== callerUid'), 'archiveClub must verify caller is owner');
// N. admin cannot archive
assert(archiveBody.includes('Permission denied. Only the club owner can archive the club.'), 'archiveClub must throw on non-owner');
// O. archive is not delete
assert(archiveBody.includes('archivedAt: now,'), 'archiveClub must set archivedAt');
assert(!archiveBody.includes('deleteDoc('), 'archiveClub MUST NOT delete document');
// P. no unarchive implementation
assert(!clubDataSrc.includes('unarchiveClub'), 'Must NOT implement unarchiveClub');
assert(!settingsEditorSrc.includes('Unarchive'), 'Settings editor MUST NOT have Unarchive control');
// Q. no personal telemetry/account/billing data
assert(!settingsEditorSrc.includes('leanAngle'), 'Settings editor must not reference leanAngle');
assert(!settingsEditorSrc.includes('speedMph'), 'Settings editor must not reference speedMph');
assert(!settingsEditorSrc.includes('gForce'), 'Settings editor must not reference gForce');
assert(!settingsEditorSrc.includes('stripe'), 'Settings editor must not reference billing');
// R. production fixture isolation
let transferProdThrew = false;
try {
process.env.NODE_ENV = 'production';
await fetchEligibleTransferMembers('test-club', '');
} catch {
transferProdThrew = true;
} finally {
process.env.NODE_ENV = 'test';
}
assert.equal(transferProdThrew, true, 'fetchEligibleTransferMembers must not return mock in production without auth');
// S. sanitized errors
assert(settingsEditorSrc.includes('sanitizePortalError'), 'Settings editor must use sanitizePortalError');
// T. Settings route/sub-navigation state
assert(sidebarSrc.includes("if (id === 'settings') return `/clubs/${clubParam}/manage/settings${queryParam}`;"), 'FounderSidebar must route to settings');
assert(settingsPageSrc.includes('activeNav="settings"'), 'Settings page must pass activeNav="settings"');
assert(settingsPageSrc.includes('<FounderPortalShell'), 'Settings page must reuse FounderPortalShell');
// U. Profile fields are not editable from Settings
assert(!settingsEditorSrc.includes('name="name"'), 'Settings editor must not have name field input');
assert(!settingsEditorSrc.includes('name="description"'), 'Settings editor must not have description input');
assert(!settingsEditorSrc.includes('name="homeRegion"'), 'Settings editor must not have homeRegion input');
assert(!settingsEditorSrc.includes('name="motorcycleFocus"'), 'Settings editor must not have motorcycleFocus input');
// V. no hard delete
assert(!settingsEditorSrc.includes('deleteDoc'), 'Settings editor must NOT call deleteDoc');
assert(!clubDataSrc.includes('deleteDoc('), 'club-data.ts must NOT call deleteDoc');
// W. bounded ownership-target query
const eligibleBody = clubDataSrc.substring(clubDataSrc.indexOf('function fetchEligibleTransferMembers'), clubDataSrc.indexOf('function transferClubOwnership'));
assert(eligibleBody.includes('limit(25)'), 'fetchEligibleTransferMembers must bound query with limit(25)');
assert(eligibleBody.includes("where('status', '==', 'active')"), 'fetchEligibleTransferMembers must query only active members');
// X. no N+1 profile query architecture
assert(!eligibleBody.includes('public_profiles'), 'fetchEligibleTransferMembers must NOT query public_profiles');
assert(!eligibleBody.includes('getDoc('), 'fetchEligibleTransferMembers must NOT call getDoc in loop');
console.log(' PASS: FP-14 Tests A through X verified (Settings auth guard, Owner/Admin authorization, Member denial, visibility enum, admission enum, access mutation whitelist, immutable slug, rank limits, owner-only transfer, active target, atomic role transition, admin transfer restriction, owner-only archive, admin archive restriction, archive is not delete, no unarchive, no telemetry/billing, fixture safety, sanitized errors, sub-routing, profile separation, no hard delete, bounded query, zero N+1 reads).');
// =========================================================================
// TEST 23: FP-15 Applications Suite (Tests A - V)
// =========================================================================
console.log('Testing: FP-15 Applications Suite (Tests A - V)...');
const applicationsPageSrc = fs.readFileSync(path.resolve(__dirname, '../app/clubs/[clubId]/manage/applications/page.tsx'), 'utf8');
const applicationsCompSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/founder-applications.tsx'), 'utf8');
// A. Route exists and reuses FounderPortalShell with activeNav="applications"
assert(fs.existsSync(path.resolve(__dirname, '../app/clubs/[clubId]/manage/applications/page.tsx')), 'Applications page route must exist');
assert(applicationsPageSrc.includes('<FounderPortalShell'), 'Applications page must reuse FounderPortalShell');
assert(applicationsPageSrc.includes('activeNav="applications"'), 'Applications page must pass activeNav="applications"');
// B. Owner can access
assert.equal(ownerPerms.canApproveApplications, true, 'Owner has canApproveApplications authority');
assert.equal(ownerPerms.canManageSettings, true, 'Owner has canManageSettings authority');
// C. Admin can access
assert.equal(adminPerms.canApproveApplications, true, 'Admin has canApproveApplications authority');
assert.equal(adminPerms.canManageSettings, true, 'Admin has canManageSettings authority');
// D. Member denied
assert.equal(memberPerms.canApproveApplications, false, 'Member must not have canApproveApplications authority');
assert.equal(memberPerms.canManageSettings, false, 'Member must not have canManageSettings authority');
assert(applicationsPageSrc.includes('Management Access Restricted'), 'Applications page must guard against unauthorized members');
// E. Unauthenticated denied
assert.equal(unauthPerms.isAuthenticated, false, 'Unauthenticated user has isAuthenticated = false');
assert.equal(unauthPerms.canApproveApplications, false, 'Unauthenticated user has canApproveApplications = false');
assert(applicationsPageSrc.includes('Authentication Required'), 'Applications page must guard against unauthenticated users');
// F. Pending query bounded to 25
const fetchAppsBody = clubDataSrc.substring(clubDataSrc.indexOf('function fetchClubApplications'), clubDataSrc.indexOf('function approveClubApplication'));
assert(fetchAppsBody.includes('limit(25)'), 'fetchClubApplications must bound query with limit(25)');
// G. Only requested statuses queried
assert(fetchAppsBody.includes("where('status', '==', 'requested')"), 'fetchClubApplications must query status == requested');
// H. Approve semantics: requested -> active
const approveBody = clubDataSrc.substring(clubDataSrc.indexOf('function approveClubApplication'), clubDataSrc.indexOf('function declineClubApplication'));
assert(approveBody.includes("status: 'active'"), 'approveClubApplication must set status: active');
// I. Decline semantics: requested -> declined
const declineBody = clubDataSrc.substring(clubDataSrc.indexOf('function declineClubApplication'));
assert(declineBody.includes("status: 'declined'"), 'declineClubApplication must set status: declined');
// J. Default role: member
assert(approveBody.includes("role: 'member'"), 'approveClubApplication must set role: member');
// K. Default display rank: Member
assert(approveBody.includes("displayRank: 'Member'"), 'approveClubApplication must set displayRank: Member');
// L. memberCount +1 atomic approval behavior
assert(approveBody.includes("memberCount: (cd.memberCount || 0) + 1"), 'approveClubApplication must increment memberCount by 1');
assert(approveBody.includes("lastMembershipChangeUid: targetUid"), 'approveClubApplication must set lastMembershipChangeUid to targetUid');
// M. Decline memberCount delta 0 (no memberCount mutation)
assert(!declineBody.includes('memberCount:'), 'declineClubApplication must not mutate memberCount');
// N. Banned application cannot approve
assert(approveBody.includes("bans', targetUid)"), 'approveClubApplication must check bans subcollection');
assert(approveBody.includes('This application can no longer be approved.'), 'approveClubApplication must reject banned applicant');
// O. Stale request fails safely
assert(approveBody.includes("targetData.status !== 'requested'"), 'approveClubApplication must verify status is requested');
assert(declineBody.includes("targetData.status !== 'requested'"), 'declineClubApplication must verify status is requested');
// P. Duplicate mutation protection
assert(applicationsCompSrc.includes('disabled={Boolean(mutatingUid)}'), 'Buttons must be disabled while mutating');
assert(applicationsCompSrc.includes('if (mutatingUid) return;'), 'Mutation handlers must prevent concurrent double-clicks');
// Q. displayRank does not grant authority
assert.equal(getPortalPermissions('member').canApproveApplications, false, 'Member role cannot approve regardless of display rank');
// R. No private identity fields exposed
assert(!applicationsCompSrc.includes('app.telemetry'), 'Applications component must not expose telemetry data');
assert(!applicationsCompSrc.includes('speedMph'), 'Applications component must not display speedMph');
assert(!applicationsCompSrc.includes('leanAngle'), 'Applications component must not display leanAngle');
assert(!applicationsCompSrc.includes('gForce'), 'Applications component must not display gForce');
assert(!applicationsCompSrc.includes('stripe'), 'Applications component must not display billing info');
assert(!applicationsCompSrc.includes('app.email'), 'Applications component must not display email');
assert(!applicationsCompSrc.includes('app.phone'), 'Applications component must not display phone');
// S. Production failure does not use fixtures
let appProdThrew = false;
try {
process.env.NODE_ENV = 'production';
await fetchClubApplications('test-club', '');
} catch {
appProdThrew = true;
} finally {
process.env.NODE_ENV = 'test';
}
assert.equal(appProdThrew, true, 'fetchClubApplications must reject unauthenticated call in production without fallback');
// T. Applications sidebar active state
assert(sidebarSrc.includes("if (id === 'applications') return `/clubs/${clubParam}/manage/applications${queryParam}`;"), 'FounderSidebar must route to applications');
assert(sidebarSrc.includes("isApplications = item.id === 'applications';"), 'FounderSidebar must treat applications as subroute');
// U. Top bar breadcrumb
assert(topBarSrc.includes("activeNav === 'applications'"), 'FounderTopBar must render applications breadcrumb');
assert(topBarSrc.includes('APPLICATIONS'), 'FounderTopBar must include APPLICATIONS title');
// V. Non-destructive decline (no deletion, history preserved)
assert(!declineBody.includes('deleteDoc('), 'declineClubApplication must NOT delete document');
assert(!applicationsCompSrc.includes('deleteDoc('), 'FounderApplications must NOT delete document');
console.log(' PASS: FP-15 Tests A through V verified (Route exists, Owner/Admin access, Member denial, Unauth denial, Bounded query limit 25, Status requested only, Approve requested->active, Decline requested->declined, Default role member, Default rank Member, memberCount +1 atomic, memberCount delta 0 decline, Ban rejection, Stale request protection, Duplicate click guard, Zero rank privilege, Zero private telemetry/contact leak, Production fixture isolation, Sidebar routing & active state, TopBar breadcrumb, Non-destructive history preservation).');
// =========================================================================
// TEST 24: Codex Review Findings — Ranks-Roles Access Guard + HTTPS Image URL Invariant
// =========================================================================
console.log('Testing: Codex findings — Ranks-Roles access guard & HTTPS image URL invariant...');
// --- 24A: Ranks & Roles route enforces canManageSettings guard ---
const ranksRolesPageSrc = fs.readFileSync(path.resolve(__dirname, '../app/clubs/[clubId]/manage/ranks-roles/page.tsx'), 'utf8');
// Owner/Admin allowed by the established permission model
assert.equal(getPortalPermissions('owner', true).canManageSettings, true, 'Owner must have canManageSettings');
assert.equal(getPortalPermissions('admin', true).canManageSettings, true, 'Admin must have canManageSettings');
// Ordinary member denied
assert.equal(getPortalPermissions('member', true).canManageSettings, false, 'Member must not have canManageSettings');
// Unauthenticated denied
assert.equal(getPortalPermissions('owner', false).isAuthenticated, false, 'Unauthenticated user must have isAuthenticated = false');
assert.equal(getPortalPermissions('owner', false).canManageSettings, false, 'Unauthenticated user must not have canManageSettings');
// Route source: unauthenticated guard, authorization guard, safe unauthorized state, demo-preview preserved
assert(ranksRolesPageSrc.includes("if (!isDemoPreview && !currentUser)"), 'Ranks-Roles page must guard unauthenticated access');
assert(ranksRolesPageSrc.includes('title="Authentication Required"'), 'Ranks-Roles page must render Authentication Required safe state');
assert(ranksRolesPageSrc.includes('!ctx.permissions.canManageSettings'), 'Ranks-Roles page must enforce canManageSettings gate');
assert(ranksRolesPageSrc.includes('setUnauthorized(true)'), 'Ranks-Roles page must set unauthorized state');
assert(ranksRolesPageSrc.includes('title="Management Access Restricted"'), 'Ranks-Roles page must render unauthorized safe state');
assert(ranksRolesPageSrc.includes('allowDemoPreview: isDemoPreview'), 'Ranks-Roles page must preserve demo-preview behavior');
// --- 24B: validateClubImageUrl shared data-layer validator ---
// Accepted: valid https URLs (returns trimmed value)
assert.equal(validateClubImageUrl('https://example.com/logo.png', 'Club logo URL'), 'https://example.com/logo.png');
assert.equal(validateClubImageUrl(' https://cdn.example.com/b banner.jpg ', 'Club banner URL'), 'https://cdn.example.com/b banner.jpg');
// Accepted: blank/empty values clear the field (returns null)
assert.equal(validateClubImageUrl('', 'Club logo URL'), null);
assert.equal(validateClubImageUrl(' ', 'Club banner URL'), null);
// Rejected: http://, data:, blob:, javascript:, ftp:, malformed
const rejectedUrls = [
'http://example.com/logo.png',
'data:image/png;base64,iVBORw0KGgo=',
'blob:https://example.com/uuid',
'javascript:alert(1)',
'ftp://example.com/logo.png',
'not-a-url',
'example.com/logo.png',
'https://',
];
for (const bad of rejectedUrls) {
let threw = false;
try {
validateClubImageUrl(bad, 'Club logo URL');
} catch (err) {
threw = true;
assert(
err.message === 'Club logo URL must be a valid HTTPS URL.' || err.message === 'Club logo URL must use a secure https:// URL.',
`Unexpected validation error message for ${bad}: ${err.message}`
);
}
assert.equal(threw, true, `validateClubImageUrl must reject: ${bad}`);
}
// Never silently normalizes an invalid URL into a valid one
assert.equal(validateClubImageUrl('https://example.com/x.png', 'Club logo URL'), 'https://example.com/x.png');
// --- 24C: updateClubProfile invokes validator before Firestore reads ---
const updateProfileBody = clubDataSrc.substring(clubDataSrc.indexOf('export async function updateClubProfile'), clubDataSrc.indexOf('export interface UpdateClubAccessSettingsInput'));
assert(updateProfileBody.includes('validateClubImageUrl(input.logoUrl'), 'updateClubProfile must validate logoUrl via validateClubImageUrl');
assert(updateProfileBody.includes('validateClubImageUrl(input.bannerUrl'), 'updateClubProfile must validate bannerUrl via validateClubImageUrl');
assert(updateProfileBody.indexOf('validateClubImageUrl(input.logoUrl') < updateProfileBody.indexOf('getDoc(clubRef)'), 'URL validation must run before Firestore reads');
assert(!updateProfileBody.includes('input.logoUrl.trim()'), 'updateClubProfile must not write unvalidated logoUrl');
assert(!updateProfileBody.includes('input.bannerUrl.trim()'), 'updateClubProfile must not write unvalidated bannerUrl');
// Behavioral: invalid URL rejected before any Firestore read (mocked getDoc never resolves a club)
let urlThrew = false;
try {
await updateClubProfile('any-club', 'caller-uid', {
name: 'Valid Club Name',
description: '',
homeRegion: '',
motorcycleFocus: '',
logoUrl: 'http://evil.example.com/x.png',
});
} catch (err) {
urlThrew = true;
assert.equal(err.message, 'Club logo URL must use a secure https:// URL.');
}
assert.equal(urlThrew, true, 'updateClubProfile must reject http:// logoUrl before touching Firestore');
// Behavioral: valid input with no URL fields reaches the not-found read (proves validation did not throw)
let notFoundThrew = false;
try {
await updateClubProfile('any-club', 'caller-uid', {
name: 'Valid Club Name',
description: '',
homeRegion: '',
motorcycleFocus: '',
logoUrl: 'https://example.com/ok.png',
bannerUrl: '',
});
} catch (err) {
notFoundThrew = true;
assert.equal(err.message, 'Club not found.', `Expected Firestore read after passing validation, got: ${err.message}`);
}
assert.equal(notFoundThrew, true, 'updateClubProfile with valid URLs must proceed to Firestore read');
// --- 24D: Firestore rules enforce the same invariant at the storage layer ---
const rulesSrc = fs.readFileSync(path.resolve(__dirname, '../../firestore.rules'), 'utf8');
assert(rulesSrc.includes('function isValidClubImageUrl(url)'), 'firestore.rules must define isValidClubImageUrl');
assert(rulesSrc.includes("url.matches('(?i)^https://\\\\S+$')"), 'firestore.rules must require https:// scheme');
const rulesCase2 = rulesSrc.substring(rulesSrc.indexOf('// Case 2: Owner general metadata update'), rulesSrc.indexOf('// Case 3: Admin editing metadata'));
const rulesCase3 = rulesSrc.substring(rulesSrc.indexOf('// Case 3: Admin editing metadata'), rulesSrc.indexOf('// Case 4: Member count'));
for (const [label, block] of [['Case 2 (owner)', rulesCase2], ['Case 3 (admin)', rulesCase3]]) {
assert(block.includes("isValidClubImageUrl(request.resource.data.logoUrl)"), `firestore.rules ${label} must validate logoUrl`);
assert(block.includes("isValidClubImageUrl(request.resource.data.bannerUrl)"), `firestore.rules ${label} must validate bannerUrl`);
}
console.log(' PASS: Ranks-Roles canManageSettings guard verified (owner/admin allowed, member/unauth denied, demo-preview preserved); HTTPS image URL invariant enforced in app layer and Firestore rules (https accepted, blank clears, http/data/blob/javascript/malformed rejected).');
// =========================================================================
// TEST 25: FP-16 Clubs Rides Management Suite (Truthfulness Hardening)
// =========================================================================
console.log('Testing: FP-16 Clubs Rides Management Suite (Truthfulness Hardening)...');
const ridesPageSrc = fs.readFileSync(path.resolve(__dirname, '../app/clubs/[clubId]/manage/rides/page.tsx'), 'utf8');
const founderRidesSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/founder-rides.tsx'), 'utf8');
const clubRideCardSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/club-ride-card.tsx'), 'utf8');
const clubFixturesSrc = fs.readFileSync(path.resolve(__dirname, '../lib/club-fixtures.ts'), 'utf8');
// A. Route exists and reuses FounderPortalShell with activeNav="rides"
assert(fs.existsSync(path.resolve(__dirname, '../app/clubs/[clubId]/manage/rides/page.tsx')), 'Rides page route must exist');
assert(ridesPageSrc.includes('<FounderPortalShell'), 'Rides page must reuse FounderPortalShell');
assert(ridesPageSrc.includes('activeNav="rides"'), 'Rides page must pass activeNav="rides"');
// B. Owner can access
assert.equal(ownerPerms.canPlanRides, true, 'Owner has canPlanRides authority');
assert.equal(ownerPerms.canManageSettings, true, 'Owner has canManageSettings authority');
// C. Admin can access
assert.equal(adminPerms.canPlanRides, true, 'Admin has canPlanRides authority');
assert.equal(adminPerms.canManageSettings, true, 'Admin has canManageSettings authority');
// D. Member denied management access
assert.equal(memberPerms.canPlanRides, false, 'Member must not have canPlanRides authority');
assert.equal(memberPerms.canManageSettings, false, 'Member must not have canManageSettings authority');
assert(ridesPageSrc.includes('Management Access Restricted'), 'Rides page must guard against unauthorized members');
// E. Unauthenticated denied
assert.equal(unauthPerms.isAuthenticated, false, 'Unauthenticated user has isAuthenticated = false');
assert.equal(unauthPerms.canPlanRides, false, 'Unauthenticated user has canPlanRides = false');
assert(ridesPageSrc.includes('Authentication Required'), 'Rides page must guard against unauthenticated users');
// F. Production rides page renders an explicit discovery-pending state, never a false empty schedule
assert(ridesPageSrc.includes('Club Rides Discovery Pending'), 'Production rides page must display Club Rides Discovery Pending notice');
assert(ridesPageSrc.includes('DISCOVERY PENDING'), 'Production rides page must display DISCOVERY PENDING badge');
assert(ridesPageSrc.includes('Club-affiliated Group Rides can now be created in the mobile application'), 'Must truthfully describe current mobile capability');
// G. fetchClubRides must not issue an unsupported broad production query
const fetchRidesBody = clubDataSrc.substring(clubDataSrc.lastIndexOf('export async function fetchClubRides'));
assert(!fetchRidesBody.includes("where('clubId'"), 'Scaffold fetchClubRides must not issue a broad client-side clubId query');
assert(fetchRidesBody.includes('Club ride discovery is not available until a privacy-preserving production read path is implemented.'), 'Production discovery must fail explicitly');
// H. Action hardening: All unsupported actions are disabled with explanatory tooltips
assert(founderRidesSrc.includes('title="Founder Portal ride planning is not wired yet"'), 'Plan Club Ride must be disabled with tooltip');
assert(founderRidesSrc.includes('SOON'), 'Plan Club Ride must have SOON badge');
assert(clubRideCardSrc.includes('title="Ride management is planned for a future release"'), 'Manage Ride must be disabled');
assert(clubRideCardSrc.includes('title="Ride details are planned for a future release"'), 'View Ride must be disabled');
assert(clubRideCardSrc.includes('title="Route navigation is planned for a future release"'), 'View Route must be disabled');
assert(clubRideCardSrc.includes('title="Member roster view is planned for a future release"'), 'View Roster must be disabled');
// I. Model corrections: completedAt removed, completion uses status == completed, routeDistanceMeters is straight-line
assert(!typesSrc.includes('completedAt'), 'types.ts must not define completedAt');
assert(!clubFixturesSrc.includes('completedAt'), 'club-fixtures.ts must not contain completedAt');
assert(typesSrc.includes("Completion is indicated strictly by status == 'completed'"), 'types.ts must document completion via status');
assert(typesSrc.includes('routeDistanceMeters is a STRAIGHT-LINE snapshot'), 'types.ts must document straight-line snapshot');
assert(clubRideCardSrc.includes('Straight-line distance'), 'club-ride-card.tsx must label distance as Straight-line distance');
// J. Location privacy invariant (ClubMember != GroupRideParticipant)
assert(founderRidesSrc.includes(verbatimNotice), 'Missing verbatim live location privacy notice in FounderRides');
assert(ridesPageSrc.includes(verbatimNotice), 'Missing verbatim live location privacy notice in rides page');
// K. Demo preview development gating
assert(fetchRidesBody.includes("options?.allowDemoPreview && process.env.NODE_ENV === 'development'"), 'fetchClubRides must gate demo preview to development');
assert(ridesPageSrc.includes("searchParams?.get('preview') === 'demo'"), 'rides page must require ?preview=demo for demo mode');
// L. Production fixture isolation (never falls back to demo data in production)
let ridesProdThrew = false;
try {
process.env.NODE_ENV = 'production';
await fetchClubRides('', '');
} catch {
ridesProdThrew = true;
} finally {
process.env.NODE_ENV = 'test';
}
assert.equal(ridesProdThrew, true, 'fetchClubRides must reject empty clubId in production without fallback');
// M. Sidebar routing & TopBar breadcrumb
assert(sidebarSrc.includes("if (id === 'rides') return `/clubs/${clubParam}/manage/rides${queryParam}`;"), 'FounderSidebar must route to rides');
assert(sidebarSrc.includes("isRides = item.id === 'rides';"), 'FounderSidebar must treat rides as subroute');
assert(topBarSrc.includes("activeNav === 'rides'"), 'FounderTopBar must handle rides activeNav');
assert(topBarSrc.includes('RIDES'), 'FounderTopBar must include RIDES breadcrumb title');
// N. Fixture past rides present and valid
assert(Array.isArray(FIXTURE_PAST_RIDES), 'FIXTURE_PAST_RIDES must be an array');
assert(FIXTURE_PAST_RIDES.length > 0, 'FIXTURE_PAST_RIDES must not be empty');
assert.equal(FIXTURE_PAST_RIDES[0].status, 'completed', 'Past rides must have completed status');
console.log(' PASS: FP-16 Tests A through N verified (Route exists, Owner/Admin access, Member denial, Unauth denial, discovery-pending production state, unsupported broad query absent, actions disabled, model corrections, privacy notice, dev-gated preview, production fixture isolation, Sidebar/TopBar, fixtures valid).');
// =========================================================================
// TEST 26: Founder Portal Page Width Normalization Invariant
// =========================================================================
console.log('Testing: Founder Portal page width normalization across Profile, Applications, and Ranks & Roles...');
const profileEditorSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/founder-profile-editor.tsx'), 'utf8');
const appNormalizedSrc = fs.readFileSync(path.resolve(__dirname, '../components/founder-portal/founder-applications.tsx'), 'utf8');
const ranksPageSrc = fs.readFileSync(path.resolve(__dirname, '../app/clubs/[clubId]/manage/ranks-roles/page.tsx'), 'utf8');
// Assert Profile Editor canvas normalization
assert(!profileEditorSrc.includes('max-w-6xl mx-auto'), 'founder-profile-editor.tsx must NOT contain restrictive max-w-6xl mx-auto container');
assert(!profileEditorSrc.includes('max-w-7xl mx-auto'), 'founder-profile-editor.tsx must NOT contain max-w-7xl mx-auto container');
assert(profileEditorSrc.includes('<div className="w-full space-y-6 pb-12">'), 'founder-profile-editor.tsx must use w-full canvas matching Overview');
// Assert Applications canvas normalization
assert(!appNormalizedSrc.includes('max-w-6xl mx-auto'), 'founder-applications.tsx must NOT contain restrictive max-w-6xl mx-auto container');
assert(!appNormalizedSrc.includes('max-w-7xl mx-auto'), 'founder-applications.tsx must NOT contain max-w-7xl mx-auto container');
assert(appNormalizedSrc.includes('<div className="w-full space-y-6">'), 'founder-applications.tsx must use w-full canvas matching Overview');
// Assert Ranks & Roles canvas normalization
assert(!ranksPageSrc.includes('max-w-6xl mx-auto'), 'ranks-roles/page.tsx must NOT contain restrictive max-w-6xl mx-auto container');
assert(!ranksPageSrc.includes('max-w-7xl mx-auto'), 'ranks-roles/page.tsx must NOT contain max-w-7xl mx-auto container');
assert(ranksPageSrc.includes('<div className="w-full space-y-6 pb-12">'), 'ranks-roles/page.tsx must use w-full canvas matching Overview');
console.log(' PASS: Page width normalization verified (Profile, Applications, Ranks & Roles all use w-full canvas without restrictive max-w-6xl caps).');
console.log('--- ALL FOUNDER PORTAL TESTS PASSED (26/26) ---');
}
runAllTests().catch((err) => {
console.error('Test Suite Failure:', err);
process.exit(1);
});