fix(integrations): make client/device counts nullable and test real degradation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1026890e33
commit
57286d83d2
4 files changed
+30
-25
No files matched your search
@@ -153,17 +153,31 @@ describe("UnifiOsIntegration getUnifiOsSummaryAsync", () => {
|
|||||||
expect(summary.wan.isp?.measuredAt).toEqual(new Date("2026-09-08T09:10:00Z"));
|
expect(summary.wan.isp?.measuredAt).toEqual(new Date("2026-09-08T09:10:00Z"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("degrades only the affected block when one local endpoint fails", async () => {
|
test("degrades only the clients block when the clients endpoint fails", async () => {
|
||||||
mockFetch.mockImplementation(async (url) => {
|
mockFetch.mockImplementation(async (url) => {
|
||||||
const raw = String(url);
|
const raw = String(url);
|
||||||
if (raw.includes("/vpn/servers")) return jsonResponse({ error: "boom" }, 500);
|
if (raw.includes("/clients")) return jsonResponse({ error: "boom" }, 500);
|
||||||
return routeLocal(raw) ?? jsonResponse({}, 404);
|
return routeLocal(raw) ?? jsonResponse({}, 404);
|
||||||
});
|
});
|
||||||
|
|
||||||
const summary = await createIntegration(localOnlySecrets).getUnifiOsSummaryAsync();
|
const summary = await createIntegration(localOnlySecrets).getUnifiOsSummaryAsync();
|
||||||
|
|
||||||
expect(summary.clients.total).toBe(2);
|
expect(summary.clients).toBeNull();
|
||||||
expect(summary.devices.total).toBe(2);
|
expect(summary.devices).toEqual({ total: 2, online: 1, offline: 1, firmwareUpdatable: 1 });
|
||||||
|
expect(summary.gateway).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("degrades only the devices block when the devices endpoint fails", async () => {
|
||||||
|
mockFetch.mockImplementation(async (url) => {
|
||||||
|
const raw = String(url);
|
||||||
|
if (raw.includes("/devices") && !raw.includes("/statistics")) return jsonResponse({ error: "boom" }, 500);
|
||||||
|
return routeLocal(raw) ?? jsonResponse({}, 404);
|
||||||
|
});
|
||||||
|
|
||||||
|
const summary = await createIntegration(localOnlySecrets).getUnifiOsSummaryAsync();
|
||||||
|
|
||||||
|
expect(summary.devices).toBeNull();
|
||||||
|
expect(summary.clients).toEqual({ total: 2, wired: 1, wireless: 1, vpn: 0, guests: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("keeps clients and devices when the gateway cannot be identified", async () => {
|
test("keeps clients and devices when the gateway cannot be identified", async () => {
|
||||||
@@ -192,6 +206,6 @@ describe("UnifiOsIntegration getUnifiOsSummaryAsync", () => {
|
|||||||
expect(summary.gateway).toBeNull();
|
expect(summary.gateway).toBeNull();
|
||||||
expect(summary.wan.throughput).toBeNull();
|
expect(summary.wan.throughput).toBeNull();
|
||||||
expect(summary.wan.state).toBe("unknown");
|
expect(summary.wan.state).toBe("unknown");
|
||||||
expect(summary.clients.total).toBe(2);
|
expect(summary.clients).toEqual({ total: 2, wired: 1, wireless: 1, vpn: 0, guests: 0 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
unifiOsDeviceStatisticsSchema,
|
unifiOsDeviceStatisticsSchema,
|
||||||
unifiOsInfoSchema,
|
unifiOsInfoSchema,
|
||||||
unifiOsSiteSchema,
|
unifiOsSiteSchema,
|
||||||
unifiOsVpnServerSchema,
|
|
||||||
} from "./unifi-os-types";
|
} from "./unifi-os-types";
|
||||||
|
|
||||||
export type UnifiOsFetch = (url: string, init?: RequestInit & { timeout?: number }) => Promise<Response>;
|
export type UnifiOsFetch = (url: string, init?: RequestInit & { timeout?: number }) => Promise<Response>;
|
||||||
@@ -27,7 +26,6 @@ const PARSE_ERROR_MESSAGE = "Invalid UniFi OS response";
|
|||||||
const clientPageSchema = createUnifiOsPageSchema(unifiOsClientSchema);
|
const clientPageSchema = createUnifiOsPageSchema(unifiOsClientSchema);
|
||||||
const devicePageSchema = createUnifiOsPageSchema(unifiOsDeviceSchema);
|
const devicePageSchema = createUnifiOsPageSchema(unifiOsDeviceSchema);
|
||||||
const sitePageSchema = createUnifiOsPageSchema(unifiOsSiteSchema);
|
const sitePageSchema = createUnifiOsPageSchema(unifiOsSiteSchema);
|
||||||
const vpnServerPageSchema = createUnifiOsPageSchema(unifiOsVpnServerSchema);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client de l'API d'intégration locale d'UniFi OS.
|
* Client de l'API d'intégration locale d'UniFi OS.
|
||||||
@@ -70,11 +68,6 @@ export class UnifiOsApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getVpnServersAsync(siteId: string): Promise<{ enabled: boolean }[]> {
|
|
||||||
const page = await this.requestAsync(`/sites/${encodeURIComponent(siteId)}/vpn/servers`, vpnServerPageSchema);
|
|
||||||
return page.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sous le plafond de 200 clients, une seule page suffit et le comptage se fait
|
* Sous le plafond de 200 clients, une seule page suffit et le comptage se fait
|
||||||
* en mémoire. Au-delà, rapatrier tous les enregistrements pour n'en tirer que
|
* en mémoire. Au-delà, rapatrier tous les enregistrements pour n'en tirer que
|
||||||
|
|||||||
@@ -8,16 +8,9 @@ import type { UnifiOsSummaryIntegration } from "../interfaces/unifi-os-summary/u
|
|||||||
import type { UnifiOsFetch } from "./unifi-os-api";
|
import type { UnifiOsFetch } from "./unifi-os-api";
|
||||||
import { UnifiOsApi } from "./unifi-os-api";
|
import { UnifiOsApi } from "./unifi-os-api";
|
||||||
import { UnifiSmApi } from "./unifi-sm-api";
|
import { UnifiSmApi } from "./unifi-sm-api";
|
||||||
import type { UnifiOsDevice, UnifiOsDeviceStatistics, UnifiOsSummary } from "./unifi-os-types";
|
import type { UnifiOsDeviceStatistics, UnifiOsSummary } from "./unifi-os-types";
|
||||||
import { findGateway, summarizeDevices } from "./unifi-os-types";
|
import { findGateway, summarizeDevices } from "./unifi-os-types";
|
||||||
|
|
||||||
const EMPTY_CLIENT_COUNTS = { total: 0, wired: 0, wireless: 0, vpn: 0, guests: 0 } as const;
|
|
||||||
const EMPTY_DEVICE_COUNTS = { total: 0, online: 0, offline: 0, firmwareUpdatable: 0 } as const;
|
|
||||||
|
|
||||||
/** `Promise.allSettled` renvoie `rejected` sans valeur ; on retombe sur un défaut. */
|
|
||||||
const settledValue = <T>(result: PromiseSettledResult<T>, fallback: T): T =>
|
|
||||||
result.status === "fulfilled" ? result.value : fallback;
|
|
||||||
|
|
||||||
export class UnifiOsIntegration extends Integration implements UnifiOsSummaryIntegration {
|
export class UnifiOsIntegration extends Integration implements UnifiOsSummaryIntegration {
|
||||||
public async getUnifiOsSummaryAsync(): Promise<UnifiOsSummary> {
|
public async getUnifiOsSummaryAsync(): Promise<UnifiOsSummary> {
|
||||||
return await this.buildSummaryAsync(fetchWithTrustedCertificatesAsync as unknown as UnifiOsFetch);
|
return await this.buildSummaryAsync(fetchWithTrustedCertificatesAsync as unknown as UnifiOsFetch);
|
||||||
@@ -59,8 +52,10 @@ export class UnifiOsIntegration extends Integration implements UnifiOsSummaryInt
|
|||||||
api.getDevicesAsync(site.id),
|
api.getDevicesAsync(site.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const clients = settledValue(clientsResult, { ...EMPTY_CLIENT_COUNTS });
|
// `null` marks a failed call so the UI never mistakes it for a real zero
|
||||||
const devices = settledValue<UnifiOsDevice[]>(devicesResult, []);
|
// count (a site with genuinely no clients or devices).
|
||||||
|
const clients = clientsResult.status === "fulfilled" ? clientsResult.value : null;
|
||||||
|
const devices = devicesResult.status === "fulfilled" ? devicesResult.value : [];
|
||||||
const gatewayDevice = findGateway(devices);
|
const gatewayDevice = findGateway(devices);
|
||||||
|
|
||||||
// Les statistiques dépendent du gateway : sans gateway identifié, il n'y a
|
// Les statistiques dépendent du gateway : sans gateway identifié, il n'y a
|
||||||
@@ -87,7 +82,7 @@ export class UnifiOsIntegration extends Integration implements UnifiOsSummaryInt
|
|||||||
isp,
|
isp,
|
||||||
},
|
},
|
||||||
clients,
|
clients,
|
||||||
devices: devicesResult.status === "fulfilled" ? summarizeDevices(devices) : { ...EMPTY_DEVICE_COUNTS },
|
devices: devicesResult.status === "fulfilled" ? summarizeDevices(devices) : null,
|
||||||
gateway:
|
gateway:
|
||||||
gatewayDevice === null
|
gatewayDevice === null
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -148,8 +148,11 @@ export interface UnifiOsSummary {
|
|||||||
throughput: { txBps: number; rxBps: number } | null;
|
throughput: { txBps: number; rxBps: number } | null;
|
||||||
isp: UnifiOsIspMetrics | null;
|
isp: UnifiOsIspMetrics | null;
|
||||||
};
|
};
|
||||||
clients: UnifiOsClientCounts;
|
/** `null` when the call failed — never a zero count, which the UI could not
|
||||||
devices: UnifiOsDeviceCounts;
|
* tell apart from a site with genuinely no clients. */
|
||||||
|
clients: UnifiOsClientCounts | null;
|
||||||
|
/** `null` when the call failed, for the same reason. */
|
||||||
|
devices: UnifiOsDeviceCounts | null;
|
||||||
gateway: UnifiOsGateway | null;
|
gateway: UnifiOsGateway | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user