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