Files
MikrotikManager/backend/src/services/ping-measurement-sql.test.ts
T

58 lines
6.5 KiB
TypeScript

import assert from "node:assert/strict"
import { createServer } from "node:http"
import Fastify from "fastify"
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
import { pool, closePool } from "../db/index.js"
import { initDatabase } from "../db/bootstrap.js"
import { collectPingProbesOnce, collectPingForProbeIds, readProbeSamplesSince } from "./uptime-collector.js"
import { buildUptimeProbeObservations } from "./uptime-probe-observations.js"
import { buildSignalSnapshotFromCollectors } from "./alert-engine/signals-from-collectors.js"
import uptimeRoutes from "../routes/uptime.js"
const database = process.env.MMAPP_OWN_SQL_DB
if (!database) console.log("ping measurement SQL: skipped; explicit owned database required")
else {
assert(/^mm_accept_166_\d+$/.test(database));assert.equal(new URL(process.env.DATABASE_URL!).pathname,"/"+database)
assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name,database)
const prefix="own-measure173-"+Date.now()+"-",probeId=prefix+"probe"
let mode:"deny"|"empty"|"loss"|"zero"|"compound"|"malformed"="deny",requests=0,serverId:number|undefined,cases=0
const http=createServer((req,res)=>{requests++;res.setHeader("Content-Type","application/json");if(mode==="deny"){res.statusCode=401;res.end(JSON.stringify({error:401,message:"Unauthorized"}))}else res.end(JSON.stringify(mode==="empty"?[]:mode==="malformed"?[{time:"53bad"}]:[{sent:"4",received:mode==="loss"?"0":"4","packet-loss":mode==="loss"?"100":"0","avg-rtt":mode==="compound"?"53ms900us":"0ms"}]))})
await new Promise<void>(resolve=>http.listen(0,"127.0.0.1",resolve))
const address=http.address();assert(address&&typeof address==="object")
const app=Fastify();app.setValidatorCompiler(validatorCompiler);app.setSerializerCompiler(serializerCompiler);await app.register(uptimeRoutes)
try {
await initDatabase()
serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password,updated_at) VALUES($1,'127.0.0.1',$2,false,true,'fixture','fixture',$3) RETURNING id",[prefix+"server",address.port,new Date(Date.now()-3_600_000).toISOString()])).rows[0].id)
await pool.query("INSERT INTO uptime_settings(id,enabled,resources_enabled,ping_enabled,interval_sec,probe_interval_sec) VALUES(1,true,false,true,15,15) ON CONFLICT(id) DO UPDATE SET enabled=true,resources_enabled=false,ping_enabled=true,interval_sec=15,probe_interval_sec=15")
await pool.query("INSERT INTO uptime_probes(id,src_server_id,name,target,interval_sec,updated_at) VALUES($1,$2,$3,'192.0.2.1',1,$4)",[probeId,serverId,prefix+"probe",new Date(Date.now()-3_600_000).toISOString()])
const since=new Date(Date.now()-3_600_000).toISOString()
const latest=async()=>{const rows=await readProbeSamplesSince(since);return buildUptimeProbeObservations(rows.filter(r=>r.probeId===probeId)).samples.at(-1)!}
const snapshot=await collectPingProbesOnce();assert.equal(snapshot.probes[0].ok,false);assert.equal(snapshot.probes[0].status,"unknown");assert.equal(snapshot.probes[0].lossPct,null)
let sample=await latest();assert.equal(sample.status,"unknown");assert.equal(sample.loss,null);assert("error" in sample);cases++
await pool.query("INSERT INTO scheduler_runs(id,job_key,started_at,finished_at,status,result_json) VALUES($1,'uptime_ping',$2,$3,'error',$4::jsonb)",[prefix+"run",snapshot.sampledAt,new Date().toISOString(),JSON.stringify(snapshot)])
assert.deepEqual((await buildSignalSnapshotFromCollectors()).probes,[])
let response=await app.inject({method:"GET",url:"/uptime/overview?range=1h"});assert.equal(response.statusCode,200);let dto=response.json().probes.find((p:{id:string})=>p.id===probeId);assert.equal(dto.status,"unknown");assert.equal(dto.loss,null);cases++
mode="empty";assert.equal((await collectPingForProbeIds([probeId])).polled,1);sample=await latest();assert.equal(sample.status,"unknown");assert.equal(sample.loss,null);cases++
mode="loss";await collectPingForProbeIds([probeId]);sample=await latest();assert.equal(sample.status,"down");assert.equal(sample.loss,100);assert.equal(sample.rtt,null);cases++
mode="zero";await collectPingForProbeIds([probeId]);sample=await latest();assert.equal(sample.status,"up");assert.equal(sample.loss,0);assert.equal(sample.rtt,0);cases++
response=await app.inject({method:"GET",url:"/uptime/overview?range=1h"});dto=response.json().probes.find((p:{id:string})=>p.id===probeId);assert.equal(dto.status,"up");assert.equal(dto.loss,0);cases++
mode="compound";await collectPingForProbeIds([probeId]);sample=await latest();assert.equal(sample.rtt,54);assert.equal(sample.loss,0);cases++
mode="malformed";await collectPingForProbeIds([probeId]);sample=await latest();assert.equal(sample.status,"unknown");assert.equal(sample.rtt,null);assert.equal(sample.loss,null);cases++
// Reclassify exact legacy scheduled failures through retained provenance, without guessing real loss.
const oldTime=new Date(Date.now()-120_000).toISOString()
await pool.query("INSERT INTO uptime_probe_samples(probe_id,sampled_at,status,loss_pct) VALUES($1,$2,'down',100)",[probeId,oldTime])
await pool.query("INSERT INTO scheduler_runs(id,job_key,started_at,finished_at,status,result_json) VALUES($1,'uptime_ping',$2,$2,'error',$3::jsonb)",[prefix+"legacy",oldTime,JSON.stringify({v:1,job:"uptime_ping",sampledAt:oldTime,probes:[{probeId,ok:false,error:"legacy API denial"}]})])
const old=(await readProbeSamplesSince(since)).find(r=>r.probeId===probeId&&Date.parse(r.sampledAt)===Date.parse(oldTime))!;assert.equal(old.measurementKnown,false);cases++
await pool.query("UPDATE servers SET enabled=false WHERE id=$1",[serverId]);const before=requests
for(const url of[`/uptime/sources/${serverId}/interfaces`,`/uptime/speed-test/endpoints/${serverId}/interfaces`]){response=await app.inject({method:"GET",url});assert.equal(response.statusCode,409)}
assert.equal((await collectPingForProbeIds([probeId])).polled,0);assert.equal(requests,before);cases++
console.log(`ping measurement owned SQL: ${cases} cases; real HTTP client/401/empty/manual/loss/zero/API/provenance/disabled guards passed`)
}finally{
await app.close();await new Promise<void>(resolve=>http.close(()=>resolve()))
await pool.query("DELETE FROM scheduler_runs WHERE id LIKE $1",[prefix+"%"])
await pool.query("DELETE FROM uptime_probe_samples WHERE probe_id=$1",[probeId]);await pool.query("DELETE FROM uptime_probes WHERE id=$1",[probeId])
if(serverId!==undefined)await pool.query("DELETE FROM servers WHERE id=$1",[serverId])
await closePool()
}
}