Files
adopt-a-street/backend/models/Task.js
William Valentin 190f08e71e fix(tests): Fix couchdbService mocking issues in model tests
- Fix User.test.js to properly use mockCouchdbService instead of couchdbService
- Fix Street.test.js and Task.test.js mocking patterns
- Add missing validation to Street and Task constructors
- Add missing mock methods (initialize, getDocument, findUserById, etc.)
- Update all references to use mocked service consistently

This resolves the main mocking issues where tests were trying to access
couchdbService directly instead of the mocked version.

🤖 Generated with AI Assistant

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-11-01 14:02:42 -07:00

321 lines
8.3 KiB
JavaScript

const couchdbService = require("../services/couchdbService");
class Task {
constructor(data) {
// Validate required fields
if (!data.street) {
throw new Error('Street is required');
}
if (!data.description) {
throw new Error('Description is required');
}
this._id = data._id || null;
this._rev = data._rev || null;
this.type = "task";
this.street = data.street || null;
this.description = data.description;
this.completedBy = data.completedBy || null;
this.status = data.status || "pending";
this.pointsAwarded = data.pointsAwarded || 10;
this.createdAt = data.createdAt || new Date().toISOString();
this.updatedAt = data.updatedAt || new Date().toISOString();
this.completedAt = data.completedAt || null;
}
// Static methods for MongoDB-like interface
static async find(filter = {}) {
try {
await couchdbService.initialize();
// Convert MongoDB filter to CouchDB selector
const selector = { type: "task", ...filter };
// Handle special cases
if (filter._id) {
selector._id = filter._id;
}
if (filter.status) {
selector.status = filter.status;
}
if (filter.street) {
selector["street.streetId"] = filter.street;
}
if (filter.completedBy) {
selector["completedBy.userId"] = filter.completedBy;
}
const query = {
selector,
sort: filter.sort || [{ createdAt: "desc" }]
};
// Add pagination if specified
if (filter.skip) query.skip = filter.skip;
if (filter.limit) query.limit = filter.limit;
const docs = await couchdbService.find(query);
// Convert to Task instances
return docs.map(doc => new Task(doc));
} catch (error) {
console.error("Error finding tasks:", error.message);
throw error;
}
}
static async findById(id) {
try {
await couchdbService.initialize();
const doc = await couchdbService.getDocument(id);
if (!doc || doc.type !== "task") {
return null;
}
return new Task(doc);
} catch (error) {
if (error.statusCode === 404) {
return null;
}
console.error("Error finding task by ID:", error.message);
throw error;
}
}
static async findOne(filter = {}) {
try {
const tasks = await Task.find(filter);
return tasks.length > 0 ? tasks[0] : null;
} catch (error) {
console.error("Error finding one task:", error.message);
throw error;
}
}
static async countDocuments(filter = {}) {
try {
await couchdbService.initialize();
const selector = { type: "task", ...filter };
// Use Mango query with count
const query = {
selector,
fields: ["_id"]
};
const docs = await couchdbService.find(query);
return docs.length;
} catch (error) {
console.error("Error counting tasks:", error.message);
throw error;
}
}
static async create(data) {
try {
await couchdbService.initialize();
const task = new Task(data);
const doc = await couchdbService.createDocument(task.toJSON());
return new Task(doc);
} catch (error) {
console.error("Error creating task:", error.message);
throw error;
}
}
static async deleteMany(filter = {}) {
try {
await couchdbService.initialize();
const tasks = await Task.find(filter);
const deletePromises = tasks.map(task => task.delete());
await Promise.all(deletePromises);
return { deletedCount: tasks.length };
} catch (error) {
console.error("Error deleting many tasks:", error.message);
throw error;
}
}
// Instance methods
async save() {
try {
await couchdbService.initialize();
this.updatedAt = new Date().toISOString();
if (this._id && this._rev) {
// Update existing document
const doc = await couchdbService.updateDocument(this.toJSON());
this._rev = doc._rev;
} else {
// Create new document
const doc = await couchdbService.createDocument(this.toJSON());
this._id = doc._id;
this._rev = doc._rev;
}
// Handle post-save operations
await this._handlePostSave();
return this;
} catch (error) {
console.error("Error saving task:", error.message);
throw error;
}
}
async delete() {
try {
await couchdbService.initialize();
if (!this._id || !this._rev) {
throw new Error("Task must have _id and _rev to delete");
}
// Handle cascade operations
await this._handleCascadeDelete();
await couchdbService.deleteDocument(this._id, this._rev);
return this;
} catch (error) {
console.error("Error deleting task:", error.message);
throw error;
}
}
async _handlePostSave() {
try {
// Update user relationship when task is completed
if (this.completedBy && this.completedBy.userId && this.status === "completed") {
const User = require("./User");
const user = await User.findById(this.completedBy.userId);
if (user && !user.completedTasks.includes(this._id)) {
user.completedTasks.push(this._id);
user.stats.tasksCompleted = user.completedTasks.length;
await user.save();
}
// Update street stats
if (this.street && this.street.streetId) {
const Street = require("./Street");
const street = await Street.findById(this.street.streetId);
if (street) {
street.stats.completedTasksCount = (street.stats.completedTasksCount || 0) + 1;
await street.save();
}
}
}
} catch (error) {
console.error("Error handling post-save:", error.message);
throw error;
}
}
async _handleCascadeDelete() {
try {
// Remove task from user's completedTasks
if (this.completedBy && this.completedBy.userId) {
const User = require("./User");
const user = await User.findById(this.completedBy.userId);
if (user) {
user.completedTasks = user.completedTasks.filter(id => id !== this._id);
user.stats.tasksCompleted = user.completedTasks.length;
await user.save();
}
}
} catch (error) {
console.error("Error handling cascade delete:", error.message);
throw error;
}
}
// Populate method for compatibility
async populate(paths) {
if (Array.isArray(paths)) {
for (const path of paths) {
await this._populatePath(path);
}
} else {
await this._populatePath(paths);
}
return this;
}
async _populatePath(path) {
if (path === "street" && this.street && this.street.streetId) {
const Street = require("./Street");
const street = await Street.findById(this.street.streetId);
if (street) {
this.street = {
streetId: street._id,
name: street.name,
location: street.location
};
}
}
if (path === "completedBy" && this.completedBy && this.completedBy.userId) {
const User = require("./User");
const user = await User.findById(this.completedBy.userId);
if (user) {
this.completedBy = {
userId: user._id,
name: user.name,
profilePicture: user.profilePicture
};
}
}
}
// Convert to plain object
toJSON() {
return {
_id: this._id,
_rev: this._rev,
type: this.type,
street: this.street,
description: this.description,
completedBy: this.completedBy,
status: this.status,
pointsAwarded: this.pointsAwarded,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
completedAt: this.completedAt
};
}
// Convert to MongoDB-like format for API responses
toObject() {
const obj = this.toJSON();
// Remove CouchDB-specific fields for API compatibility
delete obj._rev;
delete obj.type;
// Add _id field for compatibility
if (obj._id) {
obj.id = obj._id;
}
return obj;
}
}
// Export both the class and static methods for compatibility
module.exports = Task;