MotorSignal-App/docs/qa-2026-10-01/auth.exercise.mjs

46 lines
8.1 KiB
JavaScript

import {createRequire} from 'node:module';
import {writeFileSync} from 'node:fs';
const require=createRequire(new URL('../../website/package.json',import.meta.url));
const {initializeApp,deleteApp}=require('firebase/app');
const {getAuth,connectAuthEmulator,createUserWithEmailAndPassword,signInWithEmailAndPassword,signOut}=require('firebase/auth');
const {getFirestore,connectFirestoreEmulator,doc,setDoc,getDoc,updateDoc,writeBatch,collection,getDocs,query,where}=require('firebase/firestore');
process.env.FIRESTORE_EMULATOR_HOST='127.0.0.1:8088';process.env.FIREBASE_AUTH_EMULATOR_HOST='127.0.0.1:9099';
const {initializeApp:adminInit}=require('firebase-admin/app');
const {getFirestore:adminDb}=require('firebase-admin/firestore');
const projectId='demo-motorsignal-qa';
const admin=adminDb(adminInit({projectId}));
const roles=['owner','admin','organizer','participant','member','outsider'];
const users={};const results=[];const password=process.env.QA_DEMO_PASSWORD;
if (!password) throw new Error('Set QA_DEMO_PASSWORD for disposable emulator accounts');
async function check(id,fn){try{await fn();results.push({id,status:'PASS'});}catch(e){results.push({id,status:'FAIL',error:e.message});}console.log(JSON.stringify(results.at(-1)));}
async function denied(fn){try{await fn();throw Error('Unexpectedly allowed');}catch(e){if(e.code!=='permission-denied')throw e;}}
for(const role of roles){const app=initializeApp({projectId,apiKey:'demo-key',authDomain:`${projectId}.firebaseapp.com`},role);const auth=getAuth(app);connectAuthEmulator(auth,'http://127.0.0.1:9099',{disableWarnings:true});const db=getFirestore(app);connectFirestoreEmulator(db,'127.0.0.1',8088);const email=`qa-${role}@motorsignal.example`;let account;try{account=await createUserWithEmailAndPassword(auth,email,password);}catch(e){if(e.code!=='auth/email-already-in-use')throw e;account=await signInWithEmailAndPassword(auth,email,password);}users[role]={app,auth,db,uid:account.user.uid,email};await admin.doc(`users/${account.user.uid}`).set({id:account.user.uid,displayName:`QA ${role}`,email});await admin.doc(`public_profiles/${account.user.uid}`).set({uid:account.user.uid,displayName:`QA ${role}`,handle:`qa_${role}`});}
await check('AUTH-SDK-registration-signout-signin',async()=>{const u=users.outsider;await signOut(u.auth);if(u.auth.currentUser!==null)throw Error('Still signed in');await signInWithEmailAndPassword(u.auth,u.email,password);if(u.auth.currentUser.uid!==u.uid)throw Error('UID changed');});
const clubId='qa-club';const now=new Date();
await admin.doc(`clubs/${clubId}`).set({id:clubId,name:'QA Isolated Riders',slug:'qa-isolated-riders',description:'Local emulator fixtures only',ownerUid:users.owner.uid,visibility:'public',admissionPolicy:'request',memberCount:5,createdAt:now,updatedAt:now,archivedAt:null});
for(const role of roles.filter(x=>x!=='outsider'))await admin.doc(`clubs/${clubId}/members/${users[role].uid}`).set({uid:users[role].uid,clubId,role:['owner','admin'].includes(role)?role:'member',status:'active',displayName:`QA ${role}`,joinedAt:now,updatedAt:now});
function ride(id,{club=true,policy='requestToJoin'}={}){return {id,title:`QA ${id}`,organizerUid:users.organizer.uid,status:'scheduled',scheduledStartAt:'2026-10-20T14:00:00Z',departureAt:'2026-10-20T14:30:00Z',participantCount:1,participantLimit:10,accessUids:[users.organizer.uid],visibility:'public',joinPolicy:policy,meetingPoint:{latitude:34.6,longitude:-83.9,displayName:'QA Meeting Point',locationType:'meetup'},createdAt:now,updatedAt:now,...(club?{clubId}:{})};}
// Organizer must be club admin to create an official ride. Keep distinct owner/admin/organizer roles.
await admin.doc(`clubs/${clubId}/members/${users.organizer.uid}`).update({role:'admin'});
for(const [id,opts] of [['qa-request',{}],['qa-open',{policy:'open'}],['qa-personal',{club:false,policy:'open'}]]){await admin.doc(`group_rides/${id}`).set(ride(id,opts));await admin.doc(`group_rides/${id}/participants/${users.organizer.uid}`).set({userId:users.organizer.uid,role:'organizer',status:'accepted'});}
const {organizer,participant,member,outsider,owner}=users;const adm=users.admin;
await check('AUTH-SDK-personal-ride-create',()=>setDoc(doc(organizer.db,'group_rides/qa-created-personal'),ride('qa-created-personal',{club:false})));
await check('AUTH-SDK-club-ride-create',()=>setDoc(doc(organizer.db,'group_rides/qa-created-club'),ride('qa-created-club')));
await check('AUTH-SDK-public-club-discovery',async()=>{const s=await getDocs(query(collection(outsider.db,'clubs'),where('visibility','==','public'),where('archivedAt','==',null)));if(!s.docs.some(d=>d.id===clubId))throw Error('Club missing');});
await check('AUTH-SDK-regular-member-official-create-denied',()=>denied(()=>setDoc(doc(member.db,'group_rides/qa-forbidden'),{...ride('qa-forbidden'),organizerUid:member.uid,accessUids:[member.uid]})));
await check('AUTH-SDK-outsider-club-join-denied',()=>denied(()=>setDoc(doc(outsider.db,`group_rides/qa-request/participants/${outsider.uid}`),{userId:outsider.uid,role:'participant',status:'requested'})));
await check('AUTH-SDK-member-request',()=>setDoc(doc(participant.db,`group_rides/qa-request/participants/${participant.uid}`),{userId:participant.uid,role:'participant',status:'requested'}));
await check('AUTH-SDK-self-approve-denied',()=>denied(()=>updateDoc(doc(participant.db,`group_rides/qa-request/participants/${participant.uid}`),{status:'accepted'})));
await check('AUTH-SDK-admin-approve',async()=>{const b=writeBatch(adm.db);b.update(doc(adm.db,`group_rides/qa-request/participants/${participant.uid}`),{status:'accepted'});b.update(doc(adm.db,'group_rides/qa-request'),{participantCount:2,accessUids:[organizer.uid,participant.uid],updatedAt:now});await b.commit();});
await check('AUTH-SDK-cross-account-approved-visible',async()=>{if((await getDoc(doc(participant.db,`group_rides/qa-request/participants/${participant.uid}`))).data().status!=='accepted')throw Error('Not accepted');});
await check('AUTH-SDK-member-request-rejection',async()=>{await setDoc(doc(member.db,`group_rides/qa-request/participants/${member.uid}`),{userId:member.uid,role:'participant',status:'requested'});await updateDoc(doc(owner.db,`group_rides/qa-request/participants/${member.uid}`),{status:'declined'});if((await getDoc(doc(member.db,`group_rides/qa-request/participants/${member.uid}`))).data().status!=='declined')throw Error('Not declined');});
await check('AUTH-SDK-open-join',async()=>{const b=writeBatch(member.db);b.set(doc(member.db,`group_rides/qa-open/participants/${member.uid}`),{userId:member.uid,role:'participant',status:'accepted'});b.update(doc(member.db,'group_rides/qa-open'),{participantCount:2,accessUids:[organizer.uid,member.uid],updatedAt:now});await b.commit();});
const presence={userId:participant.uid,groupRideId:'qa-request',latitude:34.6,longitude:-83.9,updatedAt:now,expiresAt:new Date(Date.now()+60000),sharingEnabled:true,status:'active'};
await check('AUTH-SDK-accepted-presence-write',()=>setDoc(doc(participant.db,`group_rides/qa-request/live_presence/${participant.uid}`),presence));
await check('AUTH-SDK-accepted-presence-peer-read',async()=>{if(!(await getDoc(doc(organizer.db,`group_rides/qa-request/live_presence/${participant.uid}`))).exists())throw Error('Missing');});
await check('AUTH-SDK-admin-nonparticipant-presence-denied',()=>denied(()=>getDoc(doc(adm.db,`group_rides/qa-request/live_presence/${participant.uid}`))));
await check('AUTH-SDK-organizer-start-end',async()=>{await updateDoc(doc(organizer.db,'group_rides/qa-request'),{status:'underway',updatedAt:now});await updateDoc(doc(organizer.db,'group_rides/qa-request'),{status:'completed',updatedAt:now});});
await check('AUTH-SDK-ended-presence-write-denied',()=>denied(()=>setDoc(doc(participant.db,`group_rides/qa-request/live_presence/${participant.uid}`),presence)));
writeFileSync(new URL('auth.results.json',import.meta.url),JSON.stringify({projectId,kind:'Real Auth + Firestore emulators via SDK, not UI E2E',accounts:Object.fromEntries(Object.entries(users).map(([r,u])=>[r,{uid:u.uid,email:u.email}])),results},null,2));
await Promise.all(Object.values(users).map(u=>deleteApp(u.app)));await admin.terminate();process.exit(results.some(r=>r.status==='FAIL')?1:0);