1053 lines
68 KiB
JavaScript
1053 lines
68 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).');
|
|
|
|
console.log('--- ALL FOUNDER PORTAL TESTS PASSED (25/25) ---');
|
|
}
|
|
|
|
runAllTests().catch((err) => {
|
|
console.error('Test Suite Failure:', err);
|
|
process.exit(1);
|
|
});
|
|
|
|
|
|
|