git-subtree-dir: relatorio git-subtree-mainline:e322cec92bgit-subtree-split:34bcb132f3
122 lines
5.8 KiB
JavaScript
122 lines
5.8 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const {
|
|
buildCategoryFilterClause,
|
|
buildIndicatorsQuery,
|
|
buildRankingCompletoQuery,
|
|
buildTicketContextQuery,
|
|
buildBaselineDailyQuery,
|
|
buildExtendedMetricsQuery,
|
|
} = require('../src/v2/queries');
|
|
const { assertReadOnlySql, FORBIDDEN_KEYWORDS } = require('../src/v2/sqlGuard');
|
|
const { resolvePeriod } = require('../src/v2/period');
|
|
const { loadConfig } = require('../src/v2/config');
|
|
|
|
const config = loadConfig();
|
|
const period = resolvePeriod('2026-08-20', new Date(2026, 7, 24), config);
|
|
|
|
test('assertReadOnlySql aceita SELECT e devolve a mesma string', () => {
|
|
const sql = 'SELECT * FROM glpi_tickets';
|
|
assert.equal(assertReadOnlySql(sql), sql);
|
|
});
|
|
|
|
test('assertReadOnlySql bloqueia qualquer verbo de escrita', () => {
|
|
for (const verbo of FORBIDDEN_KEYWORDS) {
|
|
assert.throws(() => assertReadOnlySql(`${verbo} INTO glpi_tickets ...`), /Query bloqueada/);
|
|
}
|
|
});
|
|
|
|
test('assertReadOnlySql nao gera falso positivo em nomes de coluna legitimos do GLPI (date_creation, updater...)', () => {
|
|
assert.doesNotThrow(() =>
|
|
assertReadOnlySql('SELECT date_creation, users_id_lastupdater FROM glpi_tickets')
|
|
);
|
|
});
|
|
|
|
test('buildCategoryFilterClause usa os IDs configurados (7,16,17,18) e nao verbo de escrita', () => {
|
|
const clause = buildCategoryFilterClause([7, 16, 17, 18]);
|
|
assert.match(clause, /id = 7/);
|
|
assert.match(clause, /id = 16/);
|
|
assert.match(clause, /id = 17/);
|
|
assert.match(clause, /id = 18/);
|
|
assert.doesNotThrow(() => assertReadOnlySql(clause));
|
|
});
|
|
|
|
test('buildCategoryFilterClause rejeita id invalido (defesa contra injecao via config)', () => {
|
|
assert.throws(() => buildCategoryFilterClause([7, 'DROP TABLE x']));
|
|
});
|
|
|
|
test('buildIndicatorsQuery e somente leitura e preserva a logica da V1 (abertos por date, fechados por closedate)', () => {
|
|
const sql = buildIndicatorsQuery(period, config);
|
|
assert.doesNotThrow(() => assertReadOnlySql(sql));
|
|
assert.match(sql, /is_deleted = false/);
|
|
assert.match(sql, /date >= '2026-08-20'/);
|
|
assert.match(sql, /closedate >= '2026-08-20'/);
|
|
assert.match(sql, /status NOT IN \(5, 6\)/);
|
|
assert.match(sql, /priority >= 4/);
|
|
});
|
|
|
|
test('buildIndicatorsQuery rejeita data em formato invalido (protecao contra interpolacao livre)', () => {
|
|
const periodoRuim = { ...period, data_inicio: "2026-08-20'; DROP TABLE glpi_tickets; --", data_fim: period.data_fim };
|
|
assert.throws(() => buildIndicatorsQuery(periodoRuim, config), /Data invalida/);
|
|
});
|
|
|
|
test('buildRankingCompletoQuery gera SQL somente leitura para categoria e cliente, e expoe o id (nao so o nome)', () => {
|
|
const sqlCategoria = buildRankingCompletoQuery(period, config, 'categoria');
|
|
const sqlCliente = buildRankingCompletoQuery(period, config, 'cliente');
|
|
assert.doesNotThrow(() => assertReadOnlySql(sqlCategoria));
|
|
assert.doesNotThrow(() => assertReadOnlySql(sqlCliente));
|
|
assert.match(sqlCategoria, /glpi_itilcategories/);
|
|
assert.match(sqlCliente, /glpi_entities/);
|
|
assert.match(sqlCategoria, /SELECT t\.itilcategories_id AS id,/);
|
|
assert.match(sqlCliente, /SELECT t\.entities_id AS id,/);
|
|
});
|
|
|
|
test('buildTicketContextQuery limita linhas via LIMIT e nao seleciona tabelas de escrita', () => {
|
|
const sql = buildTicketContextQuery(period, config);
|
|
assert.doesNotThrow(() => assertReadOnlySql(sql));
|
|
assert.match(sql, new RegExp(`LIMIT ${config.TICKET_CONTEXT_SQL_LIMIT}`));
|
|
assert.match(sql, /LEFT\(t\.content, 600\) AS description/);
|
|
});
|
|
|
|
test('buildBaselineDailyQuery agrupa por dia e por dimensao (categoria/cliente) sem verbo de escrita, e nao faz JOIN com tabela de dimensao (so glpi_tickets)', () => {
|
|
const janela = period.baseline.rolling;
|
|
const sql = buildBaselineDailyQuery(janela, config, 'categoria', period);
|
|
assert.doesNotThrow(() => assertReadOnlySql(sql));
|
|
assert.match(sql, /GROUP BY dia,/);
|
|
assert.doesNotMatch(sql, /LEFT JOIN glpi_itilcategories/);
|
|
assert.doesNotMatch(sql, /LEFT JOIN glpi_entities/);
|
|
});
|
|
|
|
test('buildBaselineDailyQuery restringe o agrupamento aos IDs ativos no periodo atual (evita agrupar a base inteira de entidades/categorias)', () => {
|
|
const janela = period.baseline.rolling;
|
|
const sqlCliente = buildBaselineDailyQuery(janela, config, 'cliente', period);
|
|
assert.doesNotThrow(() => assertReadOnlySql(sqlCliente));
|
|
// a subquery de IDs ativos usa o periodo ATUAL (data_inicio do relatorio), nao a janela historica
|
|
assert.match(sqlCliente, new RegExp(`t2\\.date >= '${period.data_inicio}'`));
|
|
assert.match(sqlCliente, /AND t\.entities_id IN \(/);
|
|
});
|
|
|
|
test('buildExtendedMetricsQuery e somente leitura mesmo usando campos ainda nao validados no schema real', () => {
|
|
const sql = buildExtendedMetricsQuery(period, config);
|
|
assert.doesNotThrow(() => assertReadOnlySql(sql));
|
|
assert.match(sql, /time_to_resolve/);
|
|
});
|
|
|
|
test('nenhuma query gerada para nenhum tipo de periodo conhecido contem verbo de escrita (varredura ampla)', () => {
|
|
const referencias = [new Date(2026, 7, 18), new Date(2026, 7, 19), new Date(2026, 7, 20), new Date(2026, 7, 21), new Date(2026, 7, 24)];
|
|
for (const ref of referencias) {
|
|
const p = resolvePeriod('', ref, config);
|
|
assert.doesNotThrow(() => assertReadOnlySql(buildIndicatorsQuery(p, config)));
|
|
assert.doesNotThrow(() => assertReadOnlySql(buildTicketContextQuery(p, config)));
|
|
assert.doesNotThrow(() => assertReadOnlySql(buildRankingCompletoQuery(p, config, 'categoria')));
|
|
assert.doesNotThrow(() => assertReadOnlySql(buildRankingCompletoQuery(p, config, 'cliente')));
|
|
assert.doesNotThrow(() => assertReadOnlySql(buildExtendedMetricsQuery(p, config)));
|
|
if (p.baseline.rolling) {
|
|
assert.doesNotThrow(() => assertReadOnlySql(buildBaselineDailyQuery(p.baseline.rolling, config, 'categoria', p)));
|
|
assert.doesNotThrow(() => assertReadOnlySql(buildBaselineDailyQuery(p.baseline.rolling, config, 'cliente', p)));
|
|
}
|
|
}
|
|
});
|