test(backend): enhance CouchDB mocking and test infrastructure
- Enhanced in-memory couchdbService mock with better document tracking - Added global test reset hook to clear state between tests - Disabled cache in test environment for predictable results - Normalized model find() results to always return arrays - Enhanced couchdbService APIs (find, updateDocument) with better return values - Added RSVP persistence fallback in events route - Improved gamificationService to handle non-array find() results - Mirror profilePicture/avatar fields in User model These changes improve test reliability and should increase pass rate from ~142/228 baseline. 🤖 Generated with Claude Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -28,44 +28,209 @@ jest.mock('axios', () => ({
|
||||
}));
|
||||
|
||||
// Mock CouchDB service at the module level to prevent real service from loading
|
||||
jest.mock('../services/couchdbService', () => ({
|
||||
initialize: jest.fn().mockResolvedValue(true),
|
||||
isReady: jest.fn().mockReturnValue(true),
|
||||
isConnected: true,
|
||||
isConnecting: false,
|
||||
create: jest.fn(),
|
||||
getById: jest.fn(),
|
||||
get: jest.fn(),
|
||||
find: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
createDocument: jest.fn().mockImplementation((doc) => Promise.resolve({
|
||||
_id: `test_${Date.now()}`,
|
||||
_rev: '1-test',
|
||||
...doc
|
||||
})),
|
||||
updateDocument: jest.fn().mockImplementation((doc) => Promise.resolve({
|
||||
...doc,
|
||||
_rev: '2-test'
|
||||
})),
|
||||
deleteDocument: jest.fn().mockResolvedValue(true),
|
||||
findByType: jest.fn().mockResolvedValue([]),
|
||||
findUserById: jest.fn(),
|
||||
findUserByEmail: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateUserPoints: jest.fn().mockResolvedValue(true),
|
||||
getDocument: jest.fn(),
|
||||
findDocumentById: jest.fn(),
|
||||
bulkDocs: jest.fn().mockResolvedValue([{ ok: true, id: 'test', rev: '1-test' }]),
|
||||
insertMany: jest.fn().mockResolvedValue([]),
|
||||
deleteMany: jest.fn().mockResolvedValue(true),
|
||||
findStreetsByLocation: jest.fn().mockResolvedValue([]),
|
||||
generateId: jest.fn().mockImplementation((type, id) => `${type}_${id}`),
|
||||
extractOriginalId: jest.fn().mockImplementation((prefixedId) => prefixedId.split('_').slice(1).join('_')),
|
||||
validateDocument: jest.fn().mockReturnValue([]),
|
||||
getDB: jest.fn().mockReturnValue({}),
|
||||
shutdown: jest.fn().mockResolvedValue(true),
|
||||
}), { virtual: true });
|
||||
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', () => ({
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
// Get reference to the mocked couchdbService for test usage
|
||||
const couchdbService = require('../services/couchdbService');
|
||||
|
||||
// Make mock available for tests to reference
|
||||
global.mockCouchdbService = couchdbService;
|
||||
|
||||
// Set test environment variables (must be at least 32 chars for validation)
|
||||
process.env.JWT_SECRET = 'test-jwt-secret-for-testing-purposes-that-is-long-enough';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.COUCHDB_URL = 'http://localhost:5984';
|
||||
process.env.COUCHDB_DB_NAME = 'adopt-a-street-test';
|
||||
process.env.COUCHDB_URL = 'http://localhost:5984';
|
||||
process.env.COUCHDB_DB_NAME = 'test-adopt-a-street';
|
||||
|
||||
// Suppress console logs during tests unless there's an error
|
||||
@@ -20,4 +13,18 @@ global.console = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: console.error, // Keep error logging
|
||||
};
|
||||
};
|
||||
|
||||
// Ensure each test starts with a clean in-memory DB if available
|
||||
beforeEach(() => {
|
||||
if (typeof global.__resetCouchStore === 'function') {
|
||||
global.__resetCouchStore();
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Do NOT require('../services/couchdbService') here.
|
||||
// We rely on jest.preSetup.js to define a virtual mock for the module.
|
||||
// For tests that provide their own per-file jest.mock for couchdbService,
|
||||
// forcing a require here would preload the module and prevent their mocks
|
||||
// from taking effect. By not requiring it, route/model files will get
|
||||
// the global virtual mock by default, and per-file mocks can override it.
|
||||
|
||||
@@ -10,6 +10,11 @@ const cache = new NodeCache({ stdTTL: 300, checkperiod: 120 });
|
||||
* @returns {Function} Express middleware function
|
||||
*/
|
||||
const getCacheMiddleware = (ttlSeconds) => (req, res, next) => {
|
||||
// Disable caching in test environment to avoid cross-test interference
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const key = req.originalUrl;
|
||||
const cachedResponse = cache.get(key);
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ class Badge {
|
||||
selector: { type: 'badge' },
|
||||
sort: [{ order: 'asc' }]
|
||||
});
|
||||
return result.docs;
|
||||
const docs = Array.isArray(result) ? result : (result && result.docs) || [];
|
||||
return docs;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -141,7 +142,8 @@ class Badge {
|
||||
},
|
||||
sort: [{ 'criteria.threshold': 'desc' }]
|
||||
});
|
||||
return result.docs;
|
||||
const docs = Array.isArray(result) ? result : (result && result.docs) || [];
|
||||
return docs;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -160,7 +162,8 @@ class Badge {
|
||||
},
|
||||
sort: [{ order: 'asc' }]
|
||||
});
|
||||
return result.docs;
|
||||
const docs = Array.isArray(result) ? result : (result && result.docs) || [];
|
||||
return docs;
|
||||
}, errorContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class PointTransaction {
|
||||
};
|
||||
|
||||
const result = await couchdbService.createDocument(transaction);
|
||||
return { ...transaction, _rev: result.rev };
|
||||
return { ...transaction, _rev: result._rev || result.rev };
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class PointTransaction {
|
||||
const errorContext = createErrorContext('PointTransaction', 'findByUser', { userId, limit, skip });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const docs = await couchdbService.find({
|
||||
const res = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'point_transaction',
|
||||
user: userId
|
||||
@@ -99,6 +99,7 @@ class PointTransaction {
|
||||
limit: limit,
|
||||
skip: skip
|
||||
});
|
||||
const docs = Array.isArray(res) ? res : (res && res.docs) ? res.docs : [];
|
||||
return docs;
|
||||
}, errorContext);
|
||||
}
|
||||
@@ -107,7 +108,7 @@ class PointTransaction {
|
||||
const errorContext = createErrorContext('PointTransaction', 'findByType', { transactionType, limit, skip });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const docs = await couchdbService.find({
|
||||
const res = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'point_transaction',
|
||||
transactionType: transactionType
|
||||
@@ -116,6 +117,7 @@ class PointTransaction {
|
||||
limit: limit,
|
||||
skip: skip
|
||||
});
|
||||
const docs = Array.isArray(res) ? res : (res && res.docs) ? res.docs : [];
|
||||
return docs;
|
||||
}, errorContext);
|
||||
}
|
||||
@@ -143,8 +145,7 @@ class PointTransaction {
|
||||
const errorContext = createErrorContext('PointTransaction', 'getUserBalance', { userId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
// Get the most recent transaction for the user to find current balance
|
||||
const transactions = await couchdbService.find({
|
||||
const res = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'point_transaction',
|
||||
user: userId
|
||||
@@ -153,6 +154,7 @@ class PointTransaction {
|
||||
limit: 1
|
||||
});
|
||||
|
||||
const transactions = Array.isArray(res) ? res : (res && res.docs) ? res.docs : [];
|
||||
if (transactions.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -180,11 +182,12 @@ class PointTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
const transactions = await couchdbService.find({
|
||||
const res = await couchdbService.find({
|
||||
selector: selector,
|
||||
sort: [{ createdAt: 'desc' }]
|
||||
});
|
||||
|
||||
const transactions = Array.isArray(res) ? res : (res && res.docs) ? res.docs : [];
|
||||
return transactions;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class User {
|
||||
|
||||
// --- Profile Information ---
|
||||
this.avatar = data.avatar || null;
|
||||
this.profilePicture = data.profilePicture || data.avatar || null;
|
||||
this.cloudinaryPublicId = data.cloudinaryPublicId || null;
|
||||
this.bio = data.bio || "";
|
||||
if (this.bio.length > 500) { throw new ValidationError("Bio cannot exceed 500 characters.", "bio", this.bio); }
|
||||
@@ -198,6 +199,7 @@ class User {
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
avatar: this.avatar,
|
||||
profilePicture: this.profilePicture,
|
||||
cloudinaryPublicId: this.cloudinaryPublicId,
|
||||
bio: this.bio,
|
||||
location: this.location,
|
||||
|
||||
@@ -98,7 +98,11 @@ router.put(
|
||||
if (!user.events.includes(eventId)) {
|
||||
user.events.push(eventId);
|
||||
user.stats.eventsParticipated = user.events.length;
|
||||
await user.save();
|
||||
if (typeof user.save === 'function') {
|
||||
await user.save();
|
||||
} else if (typeof User.update === 'function') {
|
||||
await User.update(userId, { events: user.events, stats: user.stats });
|
||||
}
|
||||
}
|
||||
|
||||
// Award points for event participation using couchdbService
|
||||
|
||||
@@ -531,14 +531,28 @@ class CouchDBService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateDocument(doc) {
|
||||
async updateDocument(docOrId, maybeDoc) {
|
||||
if (!this.isConnected) await this.initialize();
|
||||
|
||||
try {
|
||||
if (!doc._id || !doc._rev) {
|
||||
let doc;
|
||||
if (arguments.length === 2) {
|
||||
const id = docOrId;
|
||||
const provided = maybeDoc || {};
|
||||
if (!provided._id) provided._id = id;
|
||||
if (!provided._rev) {
|
||||
const existing = await this.getDocument(id);
|
||||
if (!existing) throw new Error("Document not found for update");
|
||||
provided._rev = existing._rev;
|
||||
}
|
||||
doc = provided;
|
||||
} else {
|
||||
doc = docOrId;
|
||||
}
|
||||
|
||||
if (!doc || !doc._id || !doc._rev) {
|
||||
throw new Error("Document must have _id and _rev for update");
|
||||
}
|
||||
|
||||
|
||||
const response = await this.makeRequest('PUT', `/${this.dbName}/${doc._id}`, doc);
|
||||
return { ...doc, _rev: response.rev };
|
||||
} catch (error) {
|
||||
@@ -560,12 +574,24 @@ class CouchDBService {
|
||||
}
|
||||
|
||||
// Query operations
|
||||
async find(query) {
|
||||
async find(queryOrSelector, maybeOptions) {
|
||||
if (!this.isConnected) await this.initialize();
|
||||
|
||||
|
||||
try {
|
||||
let query;
|
||||
if (queryOrSelector && queryOrSelector.selector) {
|
||||
query = queryOrSelector;
|
||||
} else {
|
||||
// Support (selector, options)
|
||||
query = { selector: queryOrSelector || {} };
|
||||
if (maybeOptions && typeof maybeOptions === 'object') {
|
||||
Object.assign(query, maybeOptions);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.makeRequest('POST', `/${this.dbName}/_find`, query);
|
||||
return response.docs;
|
||||
const docs = Array.isArray(response) ? response : response?.docs;
|
||||
return Array.isArray(docs) ? docs : [];
|
||||
} catch (error) {
|
||||
logger.error("Error executing query", error);
|
||||
throw error;
|
||||
@@ -668,11 +694,14 @@ class CouchDBService {
|
||||
}
|
||||
|
||||
// Batch operation helper
|
||||
async bulkDocs(docs) {
|
||||
async bulkDocs(docsOrPayload) {
|
||||
if (!this.isConnected) await this.initialize();
|
||||
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('POST', `/${this.dbName}/_bulk_docs`, { docs });
|
||||
const payload = Array.isArray(docsOrPayload)
|
||||
? { docs: docsOrPayload }
|
||||
: (docsOrPayload && docsOrPayload.docs ? docsOrPayload : { docs: [] });
|
||||
const response = await this.makeRequest('POST', `/${this.dbName}/_bulk_docs`, payload);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error("Error in bulk operation", error);
|
||||
|
||||
@@ -394,10 +394,13 @@ async function getGlobalLeaderboard(limit = 100, offset = 0) {
|
||||
skip: offset
|
||||
});
|
||||
|
||||
const users = Array.isArray(result) ? result : [];
|
||||
|
||||
// Enrich with stats and badges
|
||||
const leaderboard = await Promise.all(result.map(async (user, index) => {
|
||||
const leaderboard = await Promise.all(users.map(async (user, index) => {
|
||||
// Get user badges
|
||||
const userBadges = await UserBadge.findByUser(user._id);
|
||||
const userBadgesRaw = await UserBadge.findByUser(user._id);
|
||||
const userBadges = Array.isArray(userBadgesRaw) ? userBadgesRaw : [];
|
||||
const badges = await Promise.all(userBadges.map(async (ub) => {
|
||||
const badgeData = ub.badge;
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
@@ -447,12 +450,13 @@ async function getWeeklyLeaderboard(limit = 100, offset = 0) {
|
||||
startOfWeek.setHours(0, 0, 0, 0);
|
||||
|
||||
// Get all point transactions since start of week
|
||||
const transactions = await couchdbService.find({
|
||||
const txResult = await couchdbService.find({
|
||||
selector: {
|
||||
type: "point_transaction",
|
||||
createdAt: { $gte: startOfWeek.toISOString() }
|
||||
}
|
||||
});
|
||||
const transactions = Array.isArray(txResult) ? txResult : [];
|
||||
|
||||
// Aggregate points by user
|
||||
const userPointsMap = {};
|
||||
@@ -475,18 +479,19 @@ async function getWeeklyLeaderboard(limit = 100, offset = 0) {
|
||||
const user = await User.findById(entry.userId);
|
||||
if (!user) return null;
|
||||
|
||||
// Get user badges
|
||||
const userBadges = await UserBadge.findByUser(userId);
|
||||
const badges = await Promise.all(userBadges.map(async (ub) => {
|
||||
const badgeData = ub.badge;
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
return badge ? {
|
||||
_id: badge._id,
|
||||
name: badge.name,
|
||||
icon: badge.icon,
|
||||
rarity: badge.rarity
|
||||
} : null;
|
||||
}));
|
||||
// Get user badges
|
||||
const userBadgesRaw = await UserBadge.findByUser(user._id);
|
||||
const userBadges = Array.isArray(userBadgesRaw) ? userBadgesRaw : [];
|
||||
const badges = await Promise.all(userBadges.map(async (ub) => {
|
||||
const badgeData = ub.badge;
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
return badge ? {
|
||||
_id: badge._id,
|
||||
name: badge.name,
|
||||
icon: badge.icon,
|
||||
rarity: badge.rarity
|
||||
} : null;
|
||||
}));
|
||||
|
||||
return {
|
||||
rank: offset + index + 1,
|
||||
@@ -522,12 +527,13 @@ async function getMonthlyLeaderboard(limit = 100, offset = 0) {
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
// Get all point transactions since start of month
|
||||
const transactions = await couchdbService.find({
|
||||
const txResult = await couchdbService.find({
|
||||
selector: {
|
||||
type: "point_transaction",
|
||||
createdAt: { $gte: startOfMonth.toISOString() }
|
||||
}
|
||||
});
|
||||
const transactions = Array.isArray(txResult) ? txResult : [];
|
||||
|
||||
// Aggregate points by user
|
||||
const userPointsMap = {};
|
||||
@@ -551,7 +557,8 @@ async function getMonthlyLeaderboard(limit = 100, offset = 0) {
|
||||
if (!user) return null;
|
||||
|
||||
// Get user badges
|
||||
const userBadges = await UserBadge.findByUser(user._id);
|
||||
const userBadgesRaw = await UserBadge.findByUser(user._id);
|
||||
const userBadges = Array.isArray(userBadgesRaw) ? userBadgesRaw : [];
|
||||
const badges = await Promise.all(userBadges.slice(0, 5).map(async (ub) => {
|
||||
const badgeData = ub.badge;
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
|
||||
Reference in New Issue
Block a user