You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
251 lines
9.9 KiB
TypeScript
251 lines
9.9 KiB
TypeScript
import { beforeEach, describe, expect, it, afterEach } from 'bun:test';
|
|
import Database from 'bun:sqlite';
|
|
import { initializeDatabase } from '../../../src/db';
|
|
import { MaintenanceService } from '../../../src/services/maintenance';
|
|
import { toIsoSqlUTC } from '../../../src/utils/datetime';
|
|
import { Metrics } from '../../../src/services/metrics';
|
|
|
|
function makeMem(): any {
|
|
const db = new Database(':memory:');
|
|
initializeDatabase(db);
|
|
return db;
|
|
}
|
|
|
|
describe('MaintenanceService', () => {
|
|
let memdb: any;
|
|
|
|
beforeEach(() => {
|
|
memdb = makeMem();
|
|
});
|
|
|
|
it('cleanupInactiveMembersOnce elimina miembros inactivos más antiguos que el umbral', async () => {
|
|
// Grupo y usuarios
|
|
memdb.exec(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('g1@g.us','comm','G',1);`);
|
|
memdb.exec(`INSERT OR IGNORE INTO users (id) VALUES ('111');`);
|
|
memdb.exec(`INSERT OR IGNORE INTO users (id) VALUES ('222');`);
|
|
|
|
// last_seen_at muy antiguo → debe borrarse
|
|
memdb.exec(`
|
|
INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
|
|
VALUES ('g1@g.us','111',0,0,'2020-01-01 00:00:00','2020-01-01 00:00:00');
|
|
`);
|
|
// last_seen_at reciente → no debe borrarse
|
|
const recent = toIsoSqlUTC(new Date(Date.now() - 12 * 60 * 60 * 1000));
|
|
memdb.exec(`
|
|
INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
|
|
VALUES ('g1@g.us','222',0,0,'${recent}','${recent}');
|
|
`);
|
|
|
|
const deleted = await MaintenanceService.cleanupInactiveMembersOnce(memdb, 1);
|
|
expect(deleted).toBe(1);
|
|
|
|
const rows = memdb.prepare(`SELECT user_id FROM group_members ORDER BY user_id`).all() as any[];
|
|
const remaining = rows.map(r => r.user_id);
|
|
expect(remaining).toEqual(['222']);
|
|
});
|
|
|
|
it('cleanupInactiveMembersOnce no hace nada si retención <= 0', async () => {
|
|
const deleted = await MaintenanceService.cleanupInactiveMembersOnce(memdb, 0);
|
|
expect(deleted).toBe(0);
|
|
});
|
|
|
|
it('reconcileAliasUsersOnce fusiona alias a usuario real en tablas principales', async () => {
|
|
// Sembrar usuarios
|
|
memdb.exec(`INSERT OR IGNORE INTO users (id) VALUES ('alias-1');`);
|
|
memdb.exec(`INSERT OR IGNORE INTO users (id) VALUES ('real-1');`);
|
|
|
|
// Tarea creada por alias y asignaciones usando alias
|
|
const res = memdb.prepare(`
|
|
INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at)
|
|
VALUES ('T1', NULL, NULL, 'alias-1', 0, NULL)
|
|
`).run() as any;
|
|
const taskId = Number(res.lastInsertRowid);
|
|
memdb.exec(`
|
|
INSERT OR IGNORE INTO task_assignments (task_id, user_id, assigned_by)
|
|
VALUES (${taskId}, 'alias-1', 'alias-1');
|
|
`);
|
|
|
|
// Tabla de alias
|
|
memdb.exec(`
|
|
INSERT OR IGNORE INTO user_aliases (alias, user_id, source)
|
|
VALUES ('alias-1', 'real-1', 'test');
|
|
`);
|
|
|
|
const merged = await MaintenanceService.reconcileAliasUsersOnce(memdb);
|
|
expect(merged).toBeGreaterThanOrEqual(1);
|
|
|
|
const tRow = memdb.prepare(`SELECT created_by FROM tasks WHERE id = ?`).get(taskId) as any;
|
|
expect(String(tRow.created_by)).toBe('real-1');
|
|
|
|
const aRows = memdb.prepare(`SELECT user_id, assigned_by FROM task_assignments WHERE task_id = ?`).all(taskId) as any[];
|
|
expect(aRows.length).toBe(1);
|
|
expect(String(aRows[0].user_id)).toBe('real-1');
|
|
expect(String(aRows[0].assigned_by)).toBe('real-1');
|
|
});
|
|
|
|
it('reconcileAliasUsersOnce retorna 0 si no existe la tabla user_aliases', async () => {
|
|
try {
|
|
memdb.exec(`DROP TABLE IF EXISTS user_aliases;`);
|
|
} catch {}
|
|
const merged = await MaintenanceService.reconcileAliasUsersOnce(memdb);
|
|
expect(merged).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('MaintenanceService - Evolution health check', () => {
|
|
let originalFetch: any;
|
|
const ENV_KEYS = ['METRICS_ENABLED', 'EVOLUTION_API_URL', 'EVOLUTION_API_INSTANCE', 'EVOLUTION_API_KEY', 'HEALTH_CHECK_RESTART_COOLDOWN_MS', 'HEALTH_CHECK_INTERVAL_MS'];
|
|
const savedEnv: Record<string, string | undefined> = {};
|
|
|
|
beforeEach(() => {
|
|
// Guardar y configurar entorno mínimo para habilitar métricas y health-check
|
|
for (const k of ENV_KEYS) savedEnv[k] = process.env[k];
|
|
process.env.METRICS_ENABLED = 'true';
|
|
process.env.EVOLUTION_API_URL = 'http://evo';
|
|
process.env.EVOLUTION_API_INSTANCE = 'inst';
|
|
process.env.EVOLUTION_API_KEY = 'key';
|
|
process.env.HEALTH_CHECK_RESTART_COOLDOWN_MS = '0'; // facilitar intentos de reinicio en tests
|
|
|
|
// Resetear métricas y estado interno
|
|
Metrics.reset();
|
|
(MaintenanceService as any)._lastEvolutionState = null;
|
|
(MaintenanceService as any)._lastStateChangeTs = 0;
|
|
(MaintenanceService as any)._lastRestartAttempt = 0;
|
|
|
|
// Guardar fetch original
|
|
originalFetch = globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restaurar entorno
|
|
for (const k of ENV_KEYS) {
|
|
if (savedEnv[k] == null) delete (process.env as any)[k];
|
|
else process.env[k] = savedEnv[k];
|
|
}
|
|
// Restaurar fetch
|
|
globalThis.fetch = originalFetch;
|
|
// Reset de métricas tras cada caso
|
|
Metrics.reset();
|
|
});
|
|
|
|
it('registra y actualiza métricas en transición de estado (open → closed)', async () => {
|
|
let currentState = 'open';
|
|
let restartStatus = 200;
|
|
|
|
globalThis.fetch = async (url: any, init?: any) => {
|
|
const u = String(url);
|
|
if (u.includes('/instance/connectionState/')) {
|
|
return new Response(JSON.stringify({ instance: { state: currentState } }), { status: 200 });
|
|
}
|
|
if (u.includes('/instance/restart/')) {
|
|
return new Response('', { status: restartStatus });
|
|
}
|
|
return new Response('', { status: 404 });
|
|
};
|
|
|
|
// Primer muestreo: open
|
|
await (MaintenanceService as any).performEvolutionHealthCheck();
|
|
// Segundo muestreo: closed (debe disparar transición)
|
|
currentState = 'closed';
|
|
await (MaintenanceService as any).performEvolutionHealthCheck();
|
|
|
|
const stats = JSON.parse(Metrics.render('json'));
|
|
const lg = stats.labeledGauges || {};
|
|
const lc = stats.labeledCounters || {};
|
|
|
|
expect(lg.evolution_instance_state['instance="inst",state="closed"']).toBe(1);
|
|
expect(lg.evolution_instance_state['instance="inst",state="open"']).toBe(0);
|
|
expect(typeof lg.evolution_instance_last_state_change_ts['instance="inst"']).toBe('number');
|
|
expect(lc.evolution_instance_state_changes_total['instance="inst"']).toBe(1);
|
|
});
|
|
|
|
it('no incrementa cambios si el estado se repite (open → open)', async () => {
|
|
let currentState = 'open';
|
|
|
|
globalThis.fetch = async (url: any, init?: any) => {
|
|
const u = String(url);
|
|
if (u.includes('/instance/connectionState/')) {
|
|
return new Response(JSON.stringify({ instance: { state: currentState } }), { status: 200 });
|
|
}
|
|
if (u.includes('/instance/restart/')) {
|
|
return new Response('', { status: 200 });
|
|
}
|
|
return new Response('', { status: 404 });
|
|
};
|
|
|
|
await (MaintenanceService as any).performEvolutionHealthCheck();
|
|
await (MaintenanceService as any).performEvolutionHealthCheck();
|
|
|
|
const stats = JSON.parse(Metrics.render('json'));
|
|
const lg = stats.labeledGauges || {};
|
|
const lc = stats.labeledCounters || {};
|
|
expect(lg.evolution_instance_state['instance="inst",state="open"']).toBe(1);
|
|
const changes = lc.evolution_instance_state_changes_total;
|
|
expect(!changes || Object.values(changes).reduce((a: number, b: any) => a + Number(b || 0), 0) === 0).toBe(true);
|
|
});
|
|
|
|
it('registra error y marca estado "unreachable" ante HTTP no OK', async () => {
|
|
globalThis.fetch = async (url: any, init?: any) => {
|
|
const u = String(url);
|
|
if (u.includes('/instance/connectionState/')) {
|
|
return new Response('err', { status: 500 });
|
|
}
|
|
return new Response('', { status: 404 });
|
|
};
|
|
|
|
await (MaintenanceService as any).performEvolutionHealthCheck();
|
|
|
|
const stats = JSON.parse(Metrics.render('json'));
|
|
const lg = stats.labeledGauges || {};
|
|
const lc = stats.labeledCounters || {};
|
|
expect(lg.evolution_instance_state['instance="inst",state="unreachable"']).toBe(1);
|
|
expect(lc.evolution_health_check_errors_total['instance="inst"']).toBe(1);
|
|
});
|
|
|
|
it('incrementa attempts y success al reiniciar cuando el estado no es open', async () => {
|
|
let currentState = 'closed';
|
|
let restartStatus = 200;
|
|
|
|
globalThis.fetch = async (url: any, init?: any) => {
|
|
const u = String(url);
|
|
if (u.includes('/instance/connectionState/')) {
|
|
return new Response(JSON.stringify({ instance: { state: currentState } }), { status: 200 });
|
|
}
|
|
if (u.includes('/instance/restart/')) {
|
|
return new Response('', { status: restartStatus });
|
|
}
|
|
return new Response('', { status: 404 });
|
|
};
|
|
|
|
await (MaintenanceService as any).performEvolutionHealthCheck();
|
|
|
|
const stats = JSON.parse(Metrics.render('json'));
|
|
const lc = stats.labeledCounters || {};
|
|
expect(lc.evolution_instance_restart_attempts_total['instance="inst"']).toBe(1);
|
|
expect(lc.evolution_instance_restart_success_total['instance="inst"']).toBe(1);
|
|
});
|
|
|
|
it('no incrementa success si el reinicio falla', async () => {
|
|
let currentState = 'closed';
|
|
let restartStatus = 500;
|
|
|
|
globalThis.fetch = async (url: any, init?: any) => {
|
|
const u = String(url);
|
|
if (u.includes('/instance/connectionState/')) {
|
|
return new Response(JSON.stringify({ instance: { state: currentState } }), { status: 200 });
|
|
}
|
|
if (u.includes('/instance/restart/')) {
|
|
return new Response('', { status: restartStatus });
|
|
}
|
|
return new Response('', { status: 404 });
|
|
};
|
|
|
|
await (MaintenanceService as any).performEvolutionHealthCheck();
|
|
|
|
const stats = JSON.parse(Metrics.render('json'));
|
|
const lc = stats.labeledCounters || {};
|
|
expect(lc.evolution_instance_restart_attempts_total['instance="inst"']).toBe(1);
|
|
expect(!lc.evolution_instance_restart_success_total || lc.evolution_instance_restart_success_total['instance="inst"'] == null).toBe(true);
|
|
});
|
|
});
|