// This file runs before any modules are loaded // Set test environment variables FIRST (before any module loads) // Must be at least 32 chars for validation process.env.NODE_ENV = 'test'; process.env.JWT_SECRET = 'test-jwt-secret-for-testing-purposes-that-is-long-enough'; process.env.COUCHDB_URL = 'http://localhost:5984'; process.env.COUCHDB_DB_NAME = 'test-adopt-a-street'; process.env.PORT = '5001'; // Mock dotenv to prevent .env file from overriding test values jest.mock('dotenv', () => ({ config: jest.fn() })); // Mock axios first since couchdbService uses it jest.mock('axios', () => ({ create: jest.fn(() => ({ get: jest.fn().mockResolvedValue({ data: {} }), put: jest.fn().mockResolvedValue({ data: { ok: true } }), post: jest.fn().mockResolvedValue({ data: { ok: true } }), delete: jest.fn().mockResolvedValue({ data: { ok: true } }), })), get: jest.fn().mockResolvedValue({ data: {} }), put: jest.fn().mockResolvedValue({ data: { ok: true } }), post: jest.fn().mockResolvedValue({ data: { ok: true } }), delete: jest.fn().mockResolvedValue({ data: { ok: true } }), })); // Mock CouchDB service at the module level to prevent real service from loading jest.mock('../services/couchdbService', () => { const store = new Map(); // _id -> doc let connected = true; const clone = (o) => (o ? JSON.parse(JSON.stringify(o)) : o); const ensureId = (doc) => { if (!doc._id) doc._id = `test_${Date.now()}_${Math.random().toString(36).slice(2,8)}`; return doc._id; }; const nextRev = (rev) => { const n = parseInt((rev || '0').split('-')[0], 10) + 1; return `${n}-test`; }; const matchSelector = (doc, selector = {}) => { const ops = { $gt: (a, b) => a > b, $gte: (a, b) => a >= b, $in: (a, arr) => Array.isArray(arr) && arr.includes(a), $elemMatch: (arr, sub) => Array.isArray(arr) && arr.some((el) => Object.entries(sub).every(([k, v]) => el && el[k] === v)), }; return Object.entries(selector).every(([key, cond]) => { // Support nested keys like 'post.postId' const value = key.split('.').reduce((acc, k) => (acc ? acc[k] : undefined), doc); if (cond && typeof cond === 'object' && !Array.isArray(cond)) { return Object.entries(cond).every(([op, cmp]) => { if (ops[op]) return ops[op](value, cmp); return value && value[op] === cmp; // nested equality }); } return value === cond; }); }; const api = {}; // Connection api.initialize = jest.fn().mockResolvedValue(true); api.isReady = jest.fn(() => connected); api.isConnected = connected; api.isConnecting = false; // Basic helpers api.getDB = jest.fn(() => ({})); api.shutdown = jest.fn().mockResolvedValue(true); // Implement validation with type and required fields checks api.validateDocument = jest.fn((doc, requiredFields = []) => { const errors = []; if (!doc || typeof doc !== 'object') { errors.push('Document must be an object'); return errors; } if (!doc.type || typeof doc.type !== 'string' || doc.type.trim() === '') { errors.push('Document must have a valid type'); } if (Array.isArray(requiredFields)) { for (const field of requiredFields) { if (!(field in doc) || doc[field] === undefined || doc[field] === null || (typeof doc[field] === 'string' && doc[field].trim() === '')) { errors.push(`Missing required field: ${field}`); } } } return errors; }); api.generateId = jest.fn((type, id) => `${type}_${id}`); api.extractOriginalId = jest.fn((prefixedId) => prefixedId.split('_').slice(1).join('_')); // CRUD api.createDocument = jest.fn(async (doc) => { const toSave = clone(doc) || {}; const id = ensureId(toSave); toSave._rev = nextRev(null); store.set(id, clone(toSave)); const saved = clone(toSave); // Return both Couch-like fields and convenience fields for tests return { ...saved, id, rev: saved._rev }; }); api.getDocument = jest.fn(async (id) => clone(store.get(id)) || null); // Aliases for compatibility with code/tests api.get = api.getDocument; api.findDocumentById = api.getDocument; api.updateDocument = jest.fn(async (docOrId, maybeDoc) => { let doc = maybeDoc ? { ...maybeDoc, _id: docOrId } : docOrId; if (!doc || !doc._id) throw new Error('Document must have _id'); const existing = store.get(doc._id); if (!doc._rev) { if (!existing) throw new Error('Document not found for update'); doc._rev = existing._rev; } const next = { ...existing, ...clone(doc), _rev: nextRev(existing ? existing._rev : doc._rev) }; store.set(doc._id, clone(next)); return clone(next); }); api.deleteDocument = jest.fn(async (id /*, rev */) => { store.delete(id); return { ok: true, id, rev: nextRev('0-test') }; }); // Additional alias methods expected by some models/tests api.destroy = api.deleteDocument; api.create = jest.fn(async (doc) => api.createDocument(doc)); api.getById = jest.fn(async (id) => api.getDocument(id)); api.update = jest.fn(async (id, document) => { const existing = store.get(id); if (!existing) throw new Error('Document not found for update'); const merged = { ...existing, ...clone(document), _id: id, _rev: existing._rev }; return api.updateDocument(merged); }); api.delete = jest.fn(async (id) => { store.delete(id); return true; }); // Query api.find = jest.fn(async (queryOrSelector, maybeOptions) => { let query; if (queryOrSelector && queryOrSelector.selector) query = queryOrSelector; else query = { selector: queryOrSelector || {} , ...(maybeOptions || {}) }; let results = Array.from(store.values()).filter((doc) => matchSelector(doc, query.selector || {})); // sort: [{ field: 'asc'|'desc' }] if (Array.isArray(query.sort) && query.sort.length > 0) { const [sortSpec] = query.sort; const [field, order] = Object.entries(sortSpec)[0]; results.sort((a, b) => { const av = field.split('.').reduce((acc, k) => (acc ? acc[k] : undefined), a); const bv = field.split('.').reduce((acc, k) => (acc ? acc[k] : undefined), b); if (av === bv) return 0; const cmp = av > bv ? 1 : -1; return order === 'desc' ? -cmp : cmp; }); } const skip = Number.isInteger(query.skip) ? query.skip : 0; const limit = Number.isInteger(query.limit) ? query.limit : undefined; const sliced = limit !== undefined ? results.slice(skip, skip + limit) : results.slice(skip); const arr = clone(sliced); // Provide both array and .docs shape for compatibility arr.docs = arr; return arr; }); api.findOne = jest.fn(async (selector) => { const docs = await api.find({ selector, limit: 1 }); return (Array.isArray(docs) ? docs[0] : docs.docs[0]) || null; }); api.findByType = jest.fn(async (type, selector = {}, options = {}) => { return api.find({ selector: { type, ...selector }, ...options }); }); // Stub badge awarding to avoid unexpected side effects in tests that don't assert it api.checkAndAwardBadges = jest.fn().mockResolvedValue([]); api.countDocuments = jest.fn(async (selector = {}) => { const docs = await api.find({ selector }); return docs.length; }); api.bulkDocs = jest.fn(async (docsOrPayload) => { const payload = Array.isArray(docsOrPayload) ? { docs: docsOrPayload } : (docsOrPayload || { docs: [] }); const out = []; for (const d of payload.docs) { if (!d._id || !store.has(d._id)) { const created = await api.createDocument(d); out.push({ ok: true, id: created._id, rev: created._rev }); } else { const updated = await api.updateDocument(d); out.push({ ok: true, id: updated._id, rev: updated._rev }); } } return out; }); // User helpers used in code api.findUserByEmail = jest.fn(async (email) => { const users = await api.find({ selector: { type: 'user', email } }); return users[0] || null; }); api.findUserById = jest.fn(async (userId) => api.getById(userId)); api.updateUserPoints = jest.fn(async (userId, pointsChange) => { const user = await api.findUserById(userId); if (!user) throw new Error('User not found'); const newPoints = Math.max(0, (user.points || 0) + pointsChange); return api.update(userId, { ...user, points: newPoints }); }); // Domain helpers used by tests api.findStreetsByLocation = jest.fn().mockResolvedValue([]); // Testing utility to reset in-memory store api.__reset = jest.fn(() => { store.clear(); }); // Expose mock and reset globally for tests that need direct access global.mockCouchdbService = api; global.__resetCouchStore = () => { try { store.clear(); } catch (e) {} }; return api; }, { virtual: true }); // Mock Cloudinary jest.mock('cloudinary', () => ({ v2: { config: jest.fn(), uploader: { upload: jest.fn().mockResolvedValue({ secure_url: 'https://cloudinary.com/test/image.jpg', public_id: 'test_public_id', width: 500, height: 500, format: 'jpg' }), destroy: jest.fn().mockResolvedValue({ result: 'ok' }) } } }));