fix: ajusta countAllActive para excluir grupos archivados e inactivos

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
brobert 2 weeks ago
parent 7ba2770422
commit af6c170115

@ -605,8 +605,14 @@ export class TaskService {
const row = this.dbInstance
.prepare(`
SELECT COUNT(*) AS cnt
FROM tasks
WHERE COALESCE(completed, 0) = 0 AND completed_at IS NULL
FROM tasks t
WHERE COALESCE(t.completed, 0) = 0 AND t.completed_at IS NULL
AND (t.group_id IS NULL OR EXISTS (
SELECT 1 FROM groups g2
WHERE g2.id = t.group_id
AND COALESCE(g2.active,1)=1
AND COALESCE(g2.archived,0)=0
))
`)
.get() as any;
return Number(row?.cnt || 0);

@ -5,12 +5,14 @@ import { AllowedGroups } from '../../../src/services/allowed-groups';
describe('AdminService - comandos básicos', () => {
const envBackup = process.env;
let memdb: any;
beforeEach(() => {
process.env = { ...envBackup, NODE_ENV: 'test', ADMIN_USERS: '34600123456' };
const memdb = makeMemDb();
memdb = makeMemDb();
(AdminService as any).dbInstance = memdb;
(AllowedGroups as any).dbInstance = memdb;
AllowedGroups.resetForTests();
});
it('rechaza a usuarios no admin', async () => {
@ -58,4 +60,63 @@ describe('AdminService - comandos básicos', () => {
expect(out.length).toBe(1);
expect(AllowedGroups.isAllowed('g2@g.us')).toBe(true);
});
it('archivar-aquí: marca archived=1, active=0; revoca tokens; desactiva membresías; bloquea allowed_groups', async () => {
// Sembrar grupo, token, membresía y allowed
memdb.exec(`INSERT INTO groups (id, community_id, name, active, archived, last_verified) VALUES ('g1@g.us','comm-1','G1',1,0,strftime('%Y-%m-%d %H:%M:%f','now'))`);
memdb.exec(`INSERT INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('g1@g.us','allowed',strftime('%Y-%m-%d %H:%M:%f','now'),strftime('%Y-%m-%d %H:%M:%f','now'))`);
memdb.exec(`INSERT INTO users (id, first_seen, last_seen) VALUES ('34600123456',strftime('%Y-%m-%d %H:%M:%f','now'),strftime('%Y-%m-%d %H:%M:%f','now'))`);
memdb.exec(`INSERT INTO calendar_tokens (type, user_id, group_id, token_hash, created_at) VALUES ('group','34600123456','g1@g.us','h1',strftime('%Y-%m-%d %H:%M:%f','now'))`);
memdb.exec(`INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at) VALUES ('g1@g.us','34600123456',0,1,strftime('%Y-%m-%d %H:%M:%f','now'),strftime('%Y-%m-%d %H:%M:%f','now'))`);
const out = await AdminService.handle({
sender: '34600123456',
groupId: 'g1@g.us',
message: '/admin archivar-aquí'
});
expect(out.length).toBe(1);
const g = memdb.query(`SELECT active, archived FROM groups WHERE id='g1@g.us'`).get() as any;
expect(Number(g.active)).toBe(0);
expect(Number(g.archived)).toBe(1);
const tok = memdb.query(`SELECT revoked_at FROM calendar_tokens WHERE group_id='g1@g.us'`).get() as any;
expect(tok && tok.revoked_at).toBeTruthy();
const gm = memdb.query(`SELECT is_active FROM group_members WHERE group_id='g1@g.us'`).get() as any;
expect(Number(gm.is_active)).toBe(0);
// allowed_groups bloqueado
const ag = memdb.query(`SELECT status FROM allowed_groups WHERE group_id='g1@g.us'`).get() as any;
expect(String(ag.status)).toBe('blocked');
});
it('borrar-aquí: borra tasks, assignments, grupo y allowed_groups', async () => {
memdb.exec(`INSERT INTO groups (id, community_id, name, active) VALUES ('g2@g.us','comm-1','G2',1)`);
memdb.exec(`INSERT INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('g2@g.us','allowed',strftime('%Y-%m-%d %H:%M:%f','now'),strftime('%Y-%m-%d %H:%M:%f','now'))`);
memdb.exec(`INSERT INTO users (id, first_seen, last_seen) VALUES ('34600123456',strftime('%Y-%m-%d %H:%M:%f','now'),strftime('%Y-%m-%d %H:%M:%f','now'))`);
const r1 = memdb.query(`INSERT INTO tasks (description, group_id, created_by) VALUES ('t1','g2@g.us','34600123456') RETURNING id`).get() as any;
const r2 = memdb.query(`INSERT INTO tasks (description, group_id, created_by) VALUES ('t2','g2@g.us','34600123456') RETURNING id`).get() as any;
memdb.exec(`INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (${Number(r1.id)}, '34600123456', '34600123456')`);
memdb.exec(`INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (${Number(r2.id)}, '34600123456', '34600123456')`);
const out = await AdminService.handle({
sender: '34600123456',
groupId: 'g2@g.us',
message: '/admin borrar-aquí'
});
expect(out.length).toBe(1);
const tcount = memdb.query(`SELECT COUNT(*) AS c FROM tasks WHERE group_id='g2@g.us'`).get() as any;
expect(Number(tcount.c)).toBe(0);
const g = memdb.query(`SELECT 1 FROM groups WHERE id='g2@g.us'`).get() as any;
expect(g).toBeUndefined();
const ag = memdb.query(`SELECT 1 FROM allowed_groups WHERE group_id='g2@g.us'`).get() as any;
expect(ag).toBeUndefined();
const acount = memdb.query(`SELECT COUNT(*) AS c FROM task_assignments`).get() as any;
expect(Number(acount.c)).toBe(0);
});
});

@ -1,5 +1,8 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import Database from 'bun:sqlite';
import { initializeDatabase, ensureUserExists } from '../../../src/db';
import { GroupSyncService } from '../../../src/services/group-sync';
import { ResponseQueue } from '../../../src/services/response-queue';
const envBackup = { ...process.env };
let originalSyncMembers: any;
@ -81,4 +84,42 @@ describe('GroupSyncService - scheduler de miembros', () => {
GroupSyncService.stopGroupsScheduler();
expect(called).toBeGreaterThanOrEqual(1);
});
test('al desactivarse un grupo en sync: revoca tokens, desactiva membresía y notifica admins', async () => {
process.env = { ...envBackup, NODE_ENV: 'development', ADMIN_USERS: '34600123456' };
const memdb = new Database(':memory:');
initializeDatabase(memdb);
(GroupSyncService as any).dbInstance = memdb;
(ResponseQueue as any).dbInstance = memdb;
// Sembrar grupo activo con miembro y token de calendario
memdb.exec(`INSERT INTO groups (id, community_id, name, active, archived, last_verified) VALUES ('g1@g.us','comm-1','G1',1,0,strftime('%Y-%m-%d %H:%M:%f','now'))`);
const uid = ensureUserExists('34600123456', memdb)!;
memdb.exec(`INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at) VALUES ('g1@g.us','${uid}',0,1,strftime('%Y-%m-%d %H:%M:%f','now'),strftime('%Y-%m-%d %H:%M:%f','now'))`);
memdb.exec(`INSERT INTO calendar_tokens (type, user_id, group_id, token_hash, created_at) VALUES ('group','${uid}','g1@g.us','h1',strftime('%Y-%m-%d %H:%M:%f','now'))`);
// Stub: API devuelve 0 grupos → el existente pasa a inactivo
const originalFetch = (GroupSyncService as any).fetchGroupsFromAPI;
(GroupSyncService as any).fetchGroupsFromAPI = async () => [];
try {
await GroupSyncService.syncGroups(true);
} finally {
// Restaurar stub
(GroupSyncService as any).fetchGroupsFromAPI = originalFetch;
}
// Tokens revocados
const tok = memdb.query(`SELECT revoked_at FROM calendar_tokens WHERE group_id='g1@g.us'`).get() as any;
expect(tok && tok.revoked_at).toBeTruthy();
// Membresías desactivadas
const mem = memdb.query(`SELECT is_active FROM group_members WHERE group_id='g1@g.us'`).get() as any;
expect(Number(mem?.is_active || 0)).toBe(0);
// Notificación encolada a admins
const msg = memdb.query(`SELECT message FROM response_queue ORDER BY id DESC LIMIT 1`).get() as any;
expect(msg && String(msg.message)).toContain('/admin archivar-grupo g1@g.us');
});
});

@ -60,4 +60,26 @@ describe('TaskService - listAllActive', () => {
const rows = TaskService.listAllActive(2);
expect(rows.length).toBe(2);
});
it('excluye tareas de grupos archivados o inactivos en listAllActive y countAllActive', () => {
seedGroup('g1@g.us', 'G1');
seedGroup('g2@g.us', 'G2');
const c = '34600123456';
createTask('G1 A', '2025-11-01', 'g1@g.us', c);
createTask('G2 A', '2025-11-02', 'g2@g.us', c);
// Archivar g1 -> solo debe aparecer G2 A
memdb.prepare(`UPDATE groups SET archived = 1 WHERE id = ?`).run('g1@g.us');
let rows = TaskService.listAllActive(10);
expect(rows.map(r => r.description)).toEqual(['G2 A']);
expect(TaskService.countAllActive()).toBe(1);
// Reactivar g1 y desactivar g2 -> solo debe aparecer G1 A
memdb.prepare(`UPDATE groups SET archived = 0 WHERE id = ?`).run('g1@g.us');
memdb.prepare(`UPDATE groups SET active = 0 WHERE id = ?`).run('g2@g.us');
rows = TaskService.listAllActive(10);
expect(rows.map(r => r.description)).toEqual(['G1 A']);
expect(TaskService.countAllActive()).toBe(1);
});
});

@ -92,4 +92,33 @@ describe('API - /api/integrations/feeds', () => {
expect(typeof bodyRotate.url === 'string' && bodyRotate.url.endsWith('.ics')).toBe(true);
expect(bodyRotate.url).not.toBe(previousGroupUrl);
});
it('el feed de grupo devuelve 410 cuando el grupo está archivado y 200 cuando está activo', async () => {
server = await serverPromise;
const sidHash = await sidHashPromise;
// Asegurar sesión
db.exec(`
INSERT OR REPLACE INTO web_sessions (id, user_id, session_hash, created_at, last_seen_at, expires_at)
VALUES ('sess-2', '${USER}', '${sidHash}', '${toIsoSql()}', '${toIsoSql()}', '${toIsoSql(new Date(Date.now() + 2 * 60 * 60 * 1000))}')
`);
// Obtener URL de grupo
const res = await fetch(`${BASE}/api/integrations/feeds`, {
headers: { cookie: `sid=${SID}` }
});
expect(res.status).toBe(200);
const body = await res.json();
const groupFeed = (body.groups || []).find((g: any) => g.groupId === GROUP);
expect(groupFeed).toBeDefined();
// Activo: debe devolver 200
const ok1 = await fetch(groupFeed.url);
expect(ok1.status).toBe(200);
// Archivar grupo y verificar 410
db.exec(`UPDATE groups SET archived = 1 WHERE id = '${GROUP}'`);
const gone = await fetch(groupFeed.url);
expect(gone.status).toBe(410);
});
});

@ -227,4 +227,111 @@ describe('Web API - GET /api/me/tasks', () => {
const ids = recent.items.map((it: any) => it.id);
expect(ids.includes(taskId)).toBe(true);
});
it('oculta tareas de grupo archivado aunque esté asignada y allowed', async () => {
// Abrir la misma DB del servidor para sembrar datos adicionales
const db2 = new Database(dbPath);
const nowIso = toIsoSql(new Date());
// Grupo activo inicialmente
db2.prepare(`
INSERT INTO groups (id, community_id, name, active, last_verified, archived)
VALUES (?, 'comm-1', 'Group ARCH', 1, ?, 0)
`).run('arch@g.us', nowIso);
db2.prepare(`INSERT OR REPLACE INTO allowed_groups (group_id, status, updated_at) VALUES ('arch@g.us','allowed',?)`).run(nowIso);
db2.prepare(`
INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
VALUES ('arch@g.us', ?, 0, 1, ?, ?)
`).run(USER, nowIso, nowIso);
// Tarea asignada al usuario en ese grupo
const r = db2.prepare(`
INSERT INTO tasks (description, due_date, group_id, created_by, display_code)
VALUES ('delta archivada', '2025-03-01', 'arch@g.us', ?, 150)
`).run(USER) as any;
const tid = Number(r.lastInsertRowid);
db2.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, ?, ?)`).run(tid, USER, USER);
// Archivar el grupo
db2.prepare(`UPDATE groups SET archived = 1 WHERE id = 'arch@g.us'`).run();
db2.close();
const q = encodeURIComponent('delta');
const res = await fetch(`${server!.baseUrl}/api/me/tasks?limit=50&search=${q}`, {
headers: { Cookie: `sid=${sid}` }
});
expect(res.status).toBe(200);
const json = await res.json();
const descs = json.items.map((it: any) => String(it.description));
expect(descs.some((d: string) => d.includes('delta archivada'))).toBe(false);
});
it('oculta tareas de grupo inactivo (active=0) aunque esté asignada y allowed', async () => {
const db2 = new Database(dbPath);
const nowIso = toIsoSql(new Date());
db2.prepare(`
INSERT INTO groups (id, community_id, name, active, last_verified, archived)
VALUES ('inact@g.us', 'comm-1', 'Group INACT', 1, ?, 0)
`).run(nowIso);
db2.prepare(`INSERT OR REPLACE INTO allowed_groups (group_id, status, updated_at) VALUES ('inact@g.us','allowed',?)`).run(nowIso);
db2.prepare(`
INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
VALUES ('inact@g.us', ?, 0, 1, ?, ?)
`).run(USER, nowIso, nowIso);
const r = db2.prepare(`
INSERT INTO tasks (description, due_date, group_id, created_by, display_code)
VALUES ('omega inactiva', '2025-04-01', 'inact@g.us', ?, 151)
`).run(USER) as any;
const tid = Number(r.lastInsertRowid);
db2.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, ?, ?)`).run(tid, USER, USER);
// Desactivar el grupo
db2.prepare(`UPDATE groups SET active = 0 WHERE id = 'inact@g.us'`).run();
db2.close();
const q = encodeURIComponent('omega inactiva');
const res = await fetch(`${server!.baseUrl}/api/me/tasks?limit=50&search=${q}`, {
headers: { Cookie: `sid=${sid}` }
});
expect(res.status).toBe(200);
const json = await res.json();
expect(json.items.length).toBe(0);
});
it('recent no muestra tareas completadas de grupos archivados', async () => {
const db2 = new Database(dbPath);
const nowIso = toIsoSql(new Date());
db2.prepare(`
INSERT INTO groups (id, community_id, name, active, last_verified, archived)
VALUES ('rec@g.us', 'comm-1', 'Group REC', 1, ?, 0)
`).run(nowIso);
db2.prepare(`INSERT OR REPLACE INTO allowed_groups (group_id, status, updated_at) VALUES ('rec@g.us','allowed',?)`).run(nowIso);
db2.prepare(`
INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
VALUES ('rec@g.us', ?, 0, 1, ?, ?)
`).run(USER, nowIso, nowIso);
const r = db2.prepare(`
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at, display_code)
VALUES ('epsilon reciente', '2025-05-01', 'rec@g.us', ?, 1, ?, 152)
`).run(USER, nowIso) as any;
const tid = Number(r.lastInsertRowid);
db2.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, ?, ?)`).run(tid, USER, USER);
// Archivar el grupo
db2.prepare(`UPDATE groups SET archived = 1 WHERE id = 'rec@g.us'`).run();
db2.close();
const q = encodeURIComponent('epsilon reciente');
const res = await fetch(`${server!.baseUrl}/api/me/tasks?status=recent&limit=50&search=${q}`, {
headers: { Cookie: `sid=${sid}` }
});
expect(res.status).toBe(200);
const json = await res.json();
expect(json.items.length).toBe(0);
});
});

Loading…
Cancel
Save