@ -285,3 +285,151 @@ describe('TaskService.createTask', () => {
expect ( count . c ) . toBe ( 0 ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// Access control for completeTask
// ---------------------------------------------------------------------------
describe ( 'TaskService.completeTask - access control' , ( ) = > {
let memDb : Database ;
const ADMIN = '1000000001' ;
const USER_ASSIGNED = '2000000001' ;
const USER_NOT_ASSIGNED = '2000000002' ;
const USER_GROUP_MEMBER = '2000000003' ;
const USER_GROUP_OUTSIDER = '2000000004' ;
const GROUP_ID = 'testgroup@g.us' ;
beforeAll ( ( ) = > {
memDb = new Database ( ':memory:' ) ;
initializeDatabase ( memDb ) ;
setDb ( memDb ) ;
} ) ;
afterAll ( ( ) = > {
try { resetDb ( ) ; memDb . close ( ) ; } catch { }
} ) ;
beforeEach ( ( ) = > {
process . env . NODE_ENV = 'test' ;
setDb ( memDb ) ;
try { memDb . exec ( 'DELETE FROM task_assignments' ) ; } catch { }
try { memDb . exec ( 'DELETE FROM tasks' ) ; } catch { }
try { memDb . exec ( 'DELETE FROM group_members' ) ; } catch { }
try { memDb . exec ( 'DELETE FROM users' ) ; } catch { }
try { memDb . exec ( 'DELETE FROM groups' ) ; } catch { }
delete process . env . ADMIN_USERS ;
} ) ;
function setupGroup ( ) : void {
memDb . prepare ( `
INSERT OR IGNORE INTO groups ( id , community_id , name , active )
VALUES ( ? , 'comm-1' , 'Test Group' , 1 )
` ).run(GROUP_ID);
}
function createTask ( opts : { description? : string ; groupId? : string | null ; createdBy : string ; assignees? : string [ ] } ) : number {
const creator = ensureUserExists ( opts . createdBy , memDb ) ! ;
const taskId = TaskService . createTask (
{
description : opts.description || 'Test task' ,
due_date : null ,
group_id : opts.groupId ? ? null ,
created_by : creator ,
} ,
( opts . assignees || [ ] ) . map ( uid = > ( { user_id : ensureUserExists ( uid , memDb ) ! , assigned_by : creator } ) )
) ;
return taskId ;
}
// --- Personal tasks (no group) ---
it ( 'permite completar tarea personal si el usuario está asignado' , ( ) = > {
const taskId = createTask ( { createdBy : USER_ASSIGNED , assignees : [ USER_ASSIGNED ] } ) ;
const res = TaskService . completeTask ( taskId , USER_ASSIGNED ) ;
expect ( res . status ) . toBe ( 'updated' ) ;
} ) ;
it ( 'rechaza tarea personal si el usuario no está asignado' , ( ) = > {
const taskId = createTask ( { createdBy : USER_ASSIGNED , assignees : [ USER_ASSIGNED ] } ) ;
const res = TaskService . completeTask ( taskId , USER_NOT_ASSIGNED ) ;
expect ( res . status ) . toBe ( 'forbidden' ) ;
} ) ;
it ( 'rechaza tarea personal sin asignatarios para no-asignados' , ( ) = > {
const taskId = createTask ( { createdBy : USER_ASSIGNED } ) ;
const res = TaskService . completeTask ( taskId , USER_NOT_ASSIGNED ) ;
expect ( res . status ) . toBe ( 'forbidden' ) ;
} ) ;
// --- Group tasks ---
it ( 'permite completar tarea de grupo si el usuario está asignado' , ( ) = > {
setupGroup ( ) ;
const taskId = createTask ( { createdBy : USER_ASSIGNED , groupId : GROUP_ID , assignees : [ USER_ASSIGNED ] } ) ;
const res = TaskService . completeTask ( taskId , USER_ASSIGNED ) ;
expect ( res . status ) . toBe ( 'updated' ) ;
} ) ;
it ( 'permite completar tarea de grupo si el usuario es miembro activo (aunque no esté asignado)' , ( ) = > {
setupGroup ( ) ;
ensureUserExists ( USER_GROUP_MEMBER , memDb ) ;
memDb . prepare ( ` INSERT OR REPLACE INTO group_members (group_id, user_id, is_active) VALUES (?, ?, 1) ` ) . run ( GROUP_ID , USER_GROUP_MEMBER ) ;
const taskId = createTask ( { createdBy : USER_ASSIGNED , groupId : GROUP_ID , assignees : [ USER_ASSIGNED ] } ) ;
const res = TaskService . completeTask ( taskId , USER_GROUP_MEMBER ) ;
expect ( res . status ) . toBe ( 'updated' ) ;
} ) ;
it ( 'rechaza tarea de grupo si el usuario no está asignado ni es miembro' , ( ) = > {
setupGroup ( ) ;
const taskId = createTask ( { createdBy : USER_ASSIGNED , groupId : GROUP_ID , assignees : [ USER_ASSIGNED ] } ) ;
const res = TaskService . completeTask ( taskId , USER_GROUP_OUTSIDER ) ;
expect ( res . status ) . toBe ( 'forbidden' ) ;
} ) ;
it ( 'rechaza tarea de grupo si el usuario es miembro inactivo y no está asignado' , ( ) = > {
setupGroup ( ) ;
ensureUserExists ( USER_GROUP_MEMBER , memDb ) ;
memDb . prepare ( ` INSERT OR REPLACE INTO group_members (group_id, user_id, is_active) VALUES (?, ?, 0) ` ) . run ( GROUP_ID , USER_GROUP_MEMBER ) ;
const taskId = createTask ( { createdBy : USER_ASSIGNED , groupId : GROUP_ID , assignees : [ USER_ASSIGNED ] } ) ;
const res = TaskService . completeTask ( taskId , USER_GROUP_MEMBER ) ;
expect ( res . status ) . toBe ( 'forbidden' ) ;
} ) ;
// --- Admin override ---
it ( 'permite a un admin completar tarea personal sin estar asignado' , ( ) = > {
process . env . ADMIN_USERS = ADMIN ;
const taskId = createTask ( { createdBy : USER_ASSIGNED , assignees : [ USER_ASSIGNED ] } ) ;
const res = TaskService . completeTask ( taskId , ADMIN ) ;
expect ( res . status ) . toBe ( 'updated' ) ;
} ) ;
it ( 'permite a un admin completar tarea de grupo sin estar asignado ni ser miembro' , ( ) = > {
process . env . ADMIN_USERS = ADMIN ;
setupGroup ( ) ;
const taskId = createTask ( { createdBy : USER_ASSIGNED , groupId : GROUP_ID , assignees : [ USER_ASSIGNED ] } ) ;
const res = TaskService . completeTask ( taskId , ADMIN ) ;
expect ( res . status ) . toBe ( 'updated' ) ;
} ) ;
it ( 'permite a un admin con ID raw (con sufijo @s.whatsapp.net) completar' , ( ) = > {
process . env . ADMIN_USERS = ADMIN ;
const taskId = createTask ( { createdBy : USER_ASSIGNED , assignees : [ USER_ASSIGNED ] } ) ;
const res = TaskService . completeTask ( taskId , ADMIN + '@s.whatsapp.net' ) ;
expect ( res . status ) . toBe ( 'updated' ) ;
} ) ;
// --- Already completed / not found (unchanged behavior) ---
it ( 'devuelve already si la tarea ya estaba completada' , ( ) = > {
const taskId = createTask ( { createdBy : USER_ASSIGNED , assignees : [ USER_ASSIGNED ] } ) ;
TaskService . completeTask ( taskId , USER_ASSIGNED ) ;
const res = TaskService . completeTask ( taskId , USER_ASSIGNED ) ;
expect ( res . status ) . toBe ( 'already' ) ;
} ) ;
it ( 'devuelve not_found si la tarea no existe' , ( ) = > {
const res = TaskService . completeTask ( 999999 , USER_ASSIGNED ) ;
expect ( res . status ) . toBe ( 'not_found' ) ;
} ) ;
} ) ;