test: Firebase mock harness + createCampaign characterization
- src/__mocks__/firebase/*: jest manual mocks (app/auth/firestore) - src/__mocks__/firebase/_mock-db.js: in-memory DB + call recorder - src/setupTests.js: jest-dom, env stubs, crypto polyfill, DB reset - src/App.characterization.test.js: createCampaign -> setDoc path/payload locked - src/storage/contract.js (renamed from .test.js, helper not suite) 21 tests green (memory 19 + createCampaign 2).
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
// Mock in-memory Firestore for jest tests.
|
||||
// Reset via resetMockDb() in setupTests.js beforeEach.
|
||||
|
||||
const state = {
|
||||
docs: new Map(), // path -> data
|
||||
subscribers: new Map(), // path -> Set<cb>
|
||||
counter: 0,
|
||||
calls: [], // recorded SDK calls
|
||||
};
|
||||
|
||||
export const MOCK_DB = {
|
||||
get(path) { return state.docs.has(path) ? clone(state.docs.get(path)) : null; },
|
||||
set(path, data) {
|
||||
state.docs.set(path, clone(data));
|
||||
this._notify(path);
|
||||
},
|
||||
merge(path, patch) {
|
||||
const cur = state.docs.has(path) ? state.docs.get(path) : {};
|
||||
const next = { ...cur, ...clone(patch) };
|
||||
state.docs.set(path, next);
|
||||
this._notify(path);
|
||||
},
|
||||
delete(path) {
|
||||
state.docs.delete(path);
|
||||
this._notify(path);
|
||||
},
|
||||
collection(collPath) {
|
||||
const out = [];
|
||||
for (const [p, data] of state.docs) {
|
||||
const parent = p.split('/').slice(0, -1).join('/');
|
||||
if (parent === collPath) out.push({ id: p.split('/').pop(), data: clone(data) });
|
||||
}
|
||||
return out;
|
||||
},
|
||||
subscribe(path, cb) {
|
||||
if (!state.subscribers.has(path)) state.subscribers.set(path, new Set());
|
||||
state.subscribers.get(path).add(cb);
|
||||
return () => state.subscribers.get(path)?.delete(cb);
|
||||
},
|
||||
_notify(path) {
|
||||
// notify exact doc path subscribers
|
||||
if (state.subscribers.has(path)) state.subscribers.get(path).forEach(cb => cb());
|
||||
// notify parent collection subscribers
|
||||
const parent = path.split('/').slice(0, -1).join('/');
|
||||
if (parent && state.subscribers.has(parent)) state.subscribers.get(parent).forEach(cb => cb());
|
||||
},
|
||||
nextId() { state.counter += 1; return String(state.counter).padStart(3, '0'); },
|
||||
_state: state,
|
||||
};
|
||||
|
||||
export function recordCall(entry) {
|
||||
state.calls.push({ ...entry, ts: Date.now() });
|
||||
}
|
||||
|
||||
export function resetMockDb() {
|
||||
state.docs.clear();
|
||||
state.subscribers.clear();
|
||||
state.calls.length = 0;
|
||||
state.counter = 0;
|
||||
}
|
||||
|
||||
export function getCalls() {
|
||||
return [...state.calls];
|
||||
}
|
||||
|
||||
function clone(v) {
|
||||
if (v === null || v === undefined) return v;
|
||||
return JSON.parse(JSON.stringify(v));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest manual mock: firebase/app
|
||||
const fakeApp = { name: '[fake-firebase-app]', options: {} };
|
||||
export function initializeApp(config) { return fakeApp; }
|
||||
export const getApp = () => fakeApp;
|
||||
export const getApps = () => [fakeApp];
|
||||
@@ -0,0 +1,11 @@
|
||||
// jest manual mock: firebase/auth
|
||||
const fakeUser = { uid: 'test-user-123', isAnonymous: true };
|
||||
const fakeAuth = { currentUser: fakeUser };
|
||||
|
||||
export function getAuth() { return fakeAuth; }
|
||||
export function signInAnonymously(auth) { return Promise.resolve({ user: fakeUser }); }
|
||||
export function signInWithCustomToken(auth, token) { return Promise.resolve({ user: fakeUser }); }
|
||||
export function onAuthStateChanged(auth, cb) {
|
||||
cb(fakeUser);
|
||||
return () => {};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// jest manual mock: firebase/firestore
|
||||
// Records all calls so tests can assert path/payload/semantics.
|
||||
// Global __firestoreCalls reset per test (see setupTests.js).
|
||||
|
||||
import { MOCK_DB, recordCall } from './_mock-db.js';
|
||||
|
||||
const ref = (path) => ({ __ref: true, path, id: path.split('/').pop() });
|
||||
|
||||
export function getFirestore() { return { __db: true }; }
|
||||
export function doc(db, path, extra) {
|
||||
const p = extra ? `${path}/${extra}` : path;
|
||||
return ref(p);
|
||||
}
|
||||
export function collection(db, path) { return ref(path); }
|
||||
export function query(refOrColl, ...constraints) { return { ref: refOrColl, constraints }; }
|
||||
export function orderBy(field, dir) { return { __type: 'orderBy', field, dir }; }
|
||||
export function limit(n) { return { __type: 'limit', n }; }
|
||||
|
||||
// writes
|
||||
export async function setDoc(docRef, data, opts) {
|
||||
recordCall({ fn: 'setDoc', path: docRef.path, data: clone(data), opts: opts || null });
|
||||
MOCK_DB.set(docRef.path, clone(data));
|
||||
return undefined;
|
||||
}
|
||||
export async function updateDoc(docRef, patch) {
|
||||
recordCall({ fn: 'updateDoc', path: docRef.path, data: clone(patch) });
|
||||
MOCK_DB.merge(docRef.path, clone(patch));
|
||||
return undefined;
|
||||
}
|
||||
export async function deleteDoc(docRef) {
|
||||
recordCall({ fn: 'deleteDoc', path: docRef.path });
|
||||
MOCK_DB.delete(docRef.path);
|
||||
return undefined;
|
||||
}
|
||||
export async function addDoc(collRef, data) {
|
||||
const id = `auto_${MOCK_DB.nextId()}`;
|
||||
const path = `${collRef.path}/${id}`;
|
||||
recordCall({ fn: 'addDoc', path: collRef.path, data: clone(data) });
|
||||
MOCK_DB.set(path, clone(data));
|
||||
return { id, path };
|
||||
}
|
||||
export async function writeBatch(db) {
|
||||
const ops = [];
|
||||
return {
|
||||
set: (r, d) => ops.push({ op: 'set', path: r.path, data: clone(d) }),
|
||||
update: (r, d) => ops.push({ op: 'update', path: r.path, data: clone(d) }),
|
||||
delete: (r) => ops.push({ op: 'delete', path: r.path }),
|
||||
commit: async () => {
|
||||
ops.forEach(o => {
|
||||
recordCall({ fn: `batch.${o.op}`, path: o.path, data: o.data });
|
||||
if (o.op === 'set') MOCK_DB.set(o.path, o.data);
|
||||
else if (o.op === 'update') MOCK_DB.merge(o.path, o.data);
|
||||
else if (o.op === 'delete') MOCK_DB.delete(o.path);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// reads (return from in-memory mock DB)
|
||||
export async function getDoc(docRef) {
|
||||
const data = MOCK_DB.get(docRef.path);
|
||||
return { exists: () => data !== null, id: docRef.id, data: () => data };
|
||||
}
|
||||
export async function getDocs(collRefOrQuery) {
|
||||
const collPath = collRefOrQuery.ref ? collRefOrQuery.ref.path : collRefOrQuery.path;
|
||||
const docs = MOCK_DB.collection(collPath);
|
||||
return { docs: docs.map(d => ({ id: d.id, data: () => d.data })) };
|
||||
}
|
||||
|
||||
// realtime — emit from mock DB, capture unsub
|
||||
export function onSnapshot(refOrQuery, onSuccess, onError) {
|
||||
const path = refOrQuery.path || (refOrQuery.ref && refOrQuery.ref.path);
|
||||
// fire immediately with current state
|
||||
const emit = () => {
|
||||
if (refOrQuery.__ref && refOrQuery.path && path.split('/').length % 2 === 0) {
|
||||
const data = MOCK_DB.get(path);
|
||||
onSuccess({
|
||||
exists: () => data !== null,
|
||||
id: path.split('/').pop(),
|
||||
data: () => data,
|
||||
});
|
||||
} else {
|
||||
const docs = MOCK_DB.collection(path);
|
||||
onSuccess({ docs: docs.map(d => ({ id: d.id, data: () => d.data })) });
|
||||
}
|
||||
};
|
||||
emit();
|
||||
// register for future changes on this path
|
||||
const unsub = MOCK_DB.subscribe(path, emit);
|
||||
return unsub;
|
||||
}
|
||||
|
||||
function clone(v) {
|
||||
if (v === null || v === undefined) return v;
|
||||
return JSON.parse(JSON.stringify(v));
|
||||
}
|
||||
Reference in New Issue
Block a user