From 180c963b67d0af49d5ab28db8c984f6fe98bee56 Mon Sep 17 00:00:00 2001 From: Florian Kostenzer Date: Mon, 13 Jul 2026 22:24:26 +0200 Subject: [PATCH] test: cover tool.ts uptime math (ParseUptime, formulas, UptimeCalculator) --- src/lib/server/tool.test.ts | 125 ++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/lib/server/tool.test.ts b/src/lib/server/tool.test.ts index 226ea624..5b05b53c 100644 --- a/src/lib/server/tool.test.ts +++ b/src/lib/server/tool.test.ts @@ -9,7 +9,13 @@ import { GetMinuteStartTimestampUTC, GetNowTimestampUTC, GetNowTimestampUTCInMs, + IsValidUptimeFormula, + ParsePercentage, + ParseUptime, + UnparsePercentage, + UptimeCalculator, } from "./tool"; +import type { TimestampStatusCount } from "./types/db"; // All fixed timestamps are at 12:00 UTC so that local-date-based functions // (GetDayStartTimestampUTC mixes local date parts with Date.UTC) produce the @@ -79,3 +85,122 @@ describe("timestamp helpers", () => { expect(BeginningOfMinute({ date, timeZone: "UTC" })).toBe(NOON_UTC); }); }); + +describe("ParseUptime", () => { + it("returns '-' when there is no data", () => { + expect(ParseUptime(0, 0)).toBe("-"); + }); + + it("returns '0' when nothing is up", () => { + expect(ParseUptime(0, 10)).toBe("0"); + }); + + it("returns whole numbers without decimals", () => { + expect(ParseUptime(10, 10)).toBe("100"); + expect(ParseUptime(5, 10)).toBe("50"); + }); + + it("keeps 4 decimal places and strips trailing zeros", () => { + expect(ParseUptime(1, 3)).toBe("33.3333"); + expect(ParseUptime(2, 3)).toBe("66.6667"); + expect(ParseUptime(1, 8)).toBe("12.5"); + expect(ParseUptime(999, 1000)).toBe("99.9"); + }); +}); + +describe("ParsePercentage / UnparsePercentage", () => { + it("formats numbers as percentage strings", () => { + expect(ParsePercentage(NaN)).toBe("-"); + expect(ParsePercentage(0)).toBe("0"); + expect(ParsePercentage(100)).toBe("100"); + expect(ParsePercentage(99.9)).toBe("99.9000"); + }); + + it("parses percentage strings back to numbers", () => { + expect(UnparsePercentage("99.9")).toBe(99.9); + expect(UnparsePercentage("0")).toBe(0); + expect(Number.isNaN(UnparsePercentage("-"))).toBe(true); + }); +}); + +describe("IsValidUptimeFormula", () => { + it("accepts single variables and operator chains", () => { + expect(IsValidUptimeFormula("up")).toBe(true); + expect(IsValidUptimeFormula("up + down")).toBe(true); + expect(IsValidUptimeFormula("up+down+degraded+maintenance")).toBe(true); + expect(IsValidUptimeFormula("up - down * maintenance / degraded")).toBe(true); + }); + + it("is case-insensitive", () => { + expect(IsValidUptimeFormula("UP + Maintenance")).toBe(true); + }); + + it("rejects malformed formulas", () => { + expect(IsValidUptimeFormula("")).toBe(false); + expect(IsValidUptimeFormula(" ")).toBe(false); + expect(IsValidUptimeFormula("up +")).toBe(false); + expect(IsValidUptimeFormula("+ up")).toBe(false); + expect(IsValidUptimeFormula("banana + up")).toBe(false); + expect(IsValidUptimeFormula("up ** down")).toBe(false); + expect(IsValidUptimeFormula("up down")).toBe(false); + expect(IsValidUptimeFormula("42")).toBe(false); + }); +}); + +describe("UptimeCalculator", () => { + const day = (overrides: Partial): TimestampStatusCount => ({ + ts: 0, + countOfUp: 0, + countOfDown: 0, + countOfDegraded: 0, + countOfMaintenance: 0, + avgLatency: 0, + maxLatency: 0, + minLatency: 0, + ...overrides, + }); + + it("computes uptime and latency aggregates with the default formulas", () => { + // Defaults (global-constants): numerator "up + maintenance + degraded", + // denominator "up + down + degraded + maintenance" + const data = [ + day({ countOfUp: 30, countOfDown: 10, avgLatency: 100, maxLatency: 150, minLatency: 50 }), + day({ countOfUp: 50, countOfDegraded: 10, avgLatency: 200, maxLatency: 300, minLatency: 80 }), + ]; + expect(UptimeCalculator(data)).toEqual({ + uptime: "90", // (80 up + 0 maintenance + 10 degraded) / 100 total + avgLatency: "150ms", // (100 + 200) / 2 + maxLatency: "300ms", + minLatency: "50ms", + }); + }); + + it("supports a custom numerator formula", () => { + const data = [day({ countOfUp: 30, countOfDown: 10 }), day({ countOfUp: 50, countOfDegraded: 10 })]; + expect(UptimeCalculator(data, "up").uptime).toBe("80"); + }); + + it("falls back to the default formula when the custom one is invalid", () => { + const data = [day({ countOfUp: 30, countOfDown: 10 }), day({ countOfUp: 50, countOfDegraded: 10 })]; + expect(UptimeCalculator(data, "banana + up").uptime).toBe("90"); + }); + + it("returns '-' uptime and empty latencies for no data", () => { + expect(UptimeCalculator([])).toEqual({ + uptime: "-", + avgLatency: "", + maxLatency: "", + minLatency: "", + }); + }); + + it("only counts entries with a positive avgLatency toward the average", () => { + const data = [ + day({ countOfUp: 10, avgLatency: 0 }), + day({ countOfUp: 10, avgLatency: 100, maxLatency: 100, minLatency: 100 }), + ]; + const result = UptimeCalculator(data); + expect(result.uptime).toBe("100"); + expect(result.avgLatency).toBe("100ms"); // (0 + 100) / 1 entry with latency > 0 + }); +});