From bdf2619816045270b603a3d76950758a09f6beb3 Mon Sep 17 00:00:00 2001 From: Srujan Date: Tue, 23 Jun 2026 19:00:22 +0300 Subject: [PATCH] feat: include fingerprints in poll-alerts payload and patch visible alert rows (#6591) --- .github/workflows/run-e2e-tests.yml | 10 +- .../hooks/__tests__/useAlertPolling.test.ts | 28 +++ keep-ui/utils/hooks/useAlertPolling.ts | 55 +++++- .../ui/__tests__/useAlertsTableData.test.ts | 128 ++++++++++++- .../alerts-table/ui/useAlertsTableData.ts | 173 ++++++++++++++++-- keep/api/consts.py | 8 + keep/api/routes/alerts.py | 37 +++- keep/api/tasks/process_event_task.py | 13 +- tests/test_alerts_batch.py | 26 +++ tests/test_poll_alerts_payload.py | 11 ++ 10 files changed, 452 insertions(+), 37 deletions(-) create mode 100644 keep-ui/utils/hooks/__tests__/useAlertPolling.test.ts create mode 100644 tests/test_alerts_batch.py create mode 100644 tests/test_poll_alerts_payload.py diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 15ee46cb57..e1d7db417b 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -299,6 +299,10 @@ jobs: - name: Dump logs if: always() run: | + if [ ! -f tests/e2e_tests/docker-compose-modified.yml ]; then + echo "docker-compose-modified.yml not found; skipping log collection" + exit 0 + fi docker compose -p keep --project-directory . -f tests/e2e_tests/docker-compose-modified.yml logs keep-backend > backend_logs-${{ inputs.db-type }}.txt docker compose -p keep --project-directory . -f tests/e2e_tests/docker-compose-modified.yml logs keep-frontend > frontend_logs-${{ inputs.db-type }}.txt docker compose -p keep --project-directory . -f tests/e2e_tests/docker-compose-modified.yml logs keep-backend-db-auth > backend_logs-${{ inputs.db-type }}-db-auth.txt @@ -330,4 +334,8 @@ jobs: - name: Tear down environment if: always() run: | - docker compose -p keep --project-directory . -f tests/e2e_tests/docker-compose-modified.yml down + if [ -f tests/e2e_tests/docker-compose-modified.yml ]; then + docker compose -p keep --project-directory . -f tests/e2e_tests/docker-compose-modified.yml down + else + echo "docker-compose-modified.yml not found; skipping tear down" + fi diff --git a/keep-ui/utils/hooks/__tests__/useAlertPolling.test.ts b/keep-ui/utils/hooks/__tests__/useAlertPolling.test.ts new file mode 100644 index 0000000000..40d0a588e4 --- /dev/null +++ b/keep-ui/utils/hooks/__tests__/useAlertPolling.test.ts @@ -0,0 +1,28 @@ +import { parsePollAlertsPayload } from "../useAlertPolling"; + +describe("parsePollAlertsPayload", () => { + it("returns empty array for missing payload", () => { + expect(parsePollAlertsPayload(null)).toEqual([]); + expect(parsePollAlertsPayload(undefined)).toEqual([]); + }); + + it("parses fingerprints from object payload", () => { + expect( + parsePollAlertsPayload({ fingerprints: ["fp-1", "fp-2"] }) + ).toEqual(["fp-1", "fp-2"]); + }); + + it("parses fingerprints from legacy string payload", () => { + expect(parsePollAlertsPayload('{"fingerprints":["fp-1"]}')).toEqual([ + "fp-1", + ]); + }); + + it("filters invalid fingerprint values", () => { + expect( + parsePollAlertsPayload({ + fingerprints: ["fp-1", "", 2, null, "fp-2"], + }) + ).toEqual(["fp-1", "fp-2"]); + }); +}); diff --git a/keep-ui/utils/hooks/useAlertPolling.ts b/keep-ui/utils/hooks/useAlertPolling.ts index 3d7f23caa8..f6ace31340 100644 --- a/keep-ui/utils/hooks/useAlertPolling.ts +++ b/keep-ui/utils/hooks/useAlertPolling.ts @@ -3,25 +3,64 @@ import { useWebsocket } from "@/utils/hooks/usePusher"; import { Observable } from "rxjs"; import { v4 as generateGuid } from "uuid"; +type PollAlertsPayload = { + fingerprints?: string[]; +}; + +export function parsePollAlertsPayload(data: unknown): string[] { + if (!data) { + return []; + } + + let payload: unknown = data; + if (typeof data === "string") { + try { + payload = JSON.parse(data); + } catch { + return []; + } + } + + if ( + typeof payload === "object" && + payload !== null && + "fingerprints" in payload + ) { + const fingerprints = (payload as PollAlertsPayload).fingerprints; + if (!Array.isArray(fingerprints)) { + return []; + } + + return fingerprints.filter( + (fingerprint): fingerprint is string => + typeof fingerprint === "string" && fingerprint.length > 0 + ); + } + + return []; +} + export const useAlertPolling = (isEnabled: boolean) => { const { bind, unbind } = useWebsocket(); - const [pollAlerts, setPollAlerts] = useState(null); - - console.log("useAlertPolling: Initializing"); + const [data, setData] = useState(null); + const [fingerprints, setFingerprints] = useState([]); useEffect(() => { if (!isEnabled) { - console.log("useAlertPolling: Disabling polling"); return; } - const subscription = new Observable((subscriber) => { - const callback = () => subscriber.next(true); + const subscription = new Observable((subscriber) => { + const callback = (eventData: unknown) => subscriber.next(eventData); bind("poll-alerts", callback); return () => unbind("poll-alerts", callback); - }).subscribe(() => setPollAlerts(generateGuid())); + }).subscribe((eventData) => { + setData(generateGuid()); + setFingerprints(parsePollAlertsPayload(eventData)); + }); + return () => subscription.unsubscribe(); }, [isEnabled, bind, unbind]); - return { data: pollAlerts }; + return { data, fingerprints }; }; diff --git a/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts b/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts index 3faa901862..8a50010eaf 100644 --- a/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts +++ b/keep-ui/widgets/alerts-table/ui/__tests__/useAlertsTableData.test.ts @@ -5,6 +5,7 @@ import { } from "../useAlertsTableData"; import { AlertsQuery, useAlerts } from "@/entities/alerts/model"; import { useAlertPolling } from "@/utils/hooks/useAlertPolling"; +import { useApi } from "@/shared/lib/hooks/useApi"; import { AbsoluteTimeFrame, AllTimeFrame, @@ -17,14 +18,26 @@ jest.mock("@/entities/alerts/model", () => ({ jest.mock("@/utils/hooks/useAlertPolling", () => ({ useAlertPolling: jest.fn(), })); +jest.mock("@/shared/lib/hooks/useApi", () => ({ + useApi: jest.fn(), +})); jest.mock("uuid", () => ({ v4: () => "mock-uuid" })); const mockUseLastAlerts = jest.fn(); +const mockApiGet = jest.fn(); +const mockApiPost = jest.fn(); (useAlerts as jest.Mock).mockReturnValue({ useLastAlerts: mockUseLastAlerts }); +(useApi as jest.Mock).mockReturnValue({ + isReady: () => true, + get: mockApiGet, + post: mockApiPost, +}); const mockMutate = jest.fn(); -const defaultAlerts = [{ id: 1, name: "Alert 1" }]; +const defaultAlerts = [ + { id: 1, fingerprint: "fp-1", name: "Alert 1" }, +]; const defaultQuery: AlertsTableDataQuery = { searchCel: "test", filterCel: "filter", @@ -36,6 +49,7 @@ const defaultQuery: AlertsTableDataQuery = { beforeEach(() => { jest.clearAllMocks(); + jest.useFakeTimers(); mockUseLastAlerts.mockReturnValue({ data: defaultAlerts, totalCount: 1, @@ -44,7 +58,15 @@ beforeEach(() => { error: null, queryTimeInSeconds: 1, }); - (useAlertPolling as jest.Mock).mockReturnValue({ data: null }); + (useAlertPolling as jest.Mock).mockReturnValue({ data: null, fingerprints: [] }); + mockApiPost.mockResolvedValue([ + { + id: 1, + fingerprint: "fp-1", + name: "Updated Alert", + lastReceived: "2025-01-01T00:00:00.000Z", + }, + ]); }); describe("useAlertsTableData", () => { @@ -80,11 +102,105 @@ describe("useAlertsTableData", () => { }); it("updates alerts when polling token changes", () => { - (useAlertPolling as jest.Mock).mockReturnValue({ data: "token" }); + (useAlertPolling as jest.Mock).mockReturnValue({ + data: "token", + fingerprints: [], + }); const { result } = renderHook(() => useAlertsTableData(defaultQuery)); expect(result.current.alertsChangeToken).toBe("token"); }); + it("patches visible alerts via batch when all polled fingerprints are visible", async () => { + (useAlertPolling as jest.Mock).mockReturnValue({ + data: "token-with-fingerprints", + fingerprints: ["fp-1"], + }); + + const { result, unmount } = renderHook(() => + useAlertsTableData(defaultQuery) + ); + + await act(async () => { + await Promise.resolve(); + }); + + expect(mockApiPost).toHaveBeenCalledTimes(1); + expect(mockApiPost).toHaveBeenCalledWith("/alerts/batch", ["fp-1"]); + expect(result.current.alerts).toEqual([ + { + id: 1, + fingerprint: "fp-1", + name: "Updated Alert", + lastReceived: new Date("2025-01-01T00:00:00.000Z"), + }, + ]); + expect(result.current.facetsPanelRefreshToken).toBe("mock-uuid"); + + unmount(); + }); + + it("falls back to full refresh when any polled fingerprint is not visible", async () => { + (useAlertPolling as jest.Mock).mockReturnValue({ + data: "token-with-hidden", + fingerprints: ["fp-1", "fp-hidden"], + }); + + const { unmount } = renderHook(() => useAlertsTableData(defaultQuery)); + + await act(async () => { + await Promise.resolve(); + }); + + expect(mockApiPost).not.toHaveBeenCalled(); + unmount(); + }); + + it("removes rows for fingerprints absent from batch response", async () => { + const twoAlerts = [ + { id: 1, fingerprint: "fp-1", name: "Alert 1" }, + { id: 2, fingerprint: "fp-2", name: "Alert 2" }, + ]; + mockUseLastAlerts.mockReturnValue({ + data: twoAlerts, + totalCount: 2, + isLoading: false, + mutate: mockMutate, + error: null, + queryTimeInSeconds: 1, + }); + (useAlertPolling as jest.Mock).mockReturnValue({ + data: "token-evict", + fingerprints: ["fp-1", "fp-2"], + }); + mockApiPost.mockResolvedValue([ + { + id: 1, + fingerprint: "fp-1", + name: "Updated Alert 1", + lastReceived: "2025-01-01T00:00:00.000Z", + }, + ]); + + const { result, unmount } = renderHook(() => + useAlertsTableData(defaultQuery) + ); + + await act(async () => { + await Promise.resolve(); + }); + + expect(result.current.alerts).toEqual([ + { + id: 1, + fingerprint: "fp-1", + name: "Updated Alert 1", + lastReceived: new Date("2025-01-01T00:00:00.000Z"), + }, + ]); + + unmount(); + }); + it("generates correct facetsCel for absolute timeFrame", () => { const { result } = renderHook(() => useAlertsTableData({ @@ -221,6 +337,7 @@ describe("useAlertsTableData", () => { }); (useAlertPolling as jest.Mock).mockReturnValueOnce({ data: "polling-token", + fingerprints: [], }); const { result } = renderHook(() => useAlertsTableData(defaultQuery)); @@ -241,7 +358,10 @@ describe("useAlertsTableData", () => { error: null, queryTimeInSeconds: 1, }); - (useAlertPolling as jest.Mock).mockReturnValue({ data: "polling-token" }); + (useAlertPolling as jest.Mock).mockReturnValue({ + data: "polling-token", + fingerprints: [], + }); const { result, rerender } = renderHook( ({ query }) => useAlertsTableData(query), { diff --git a/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts b/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts index 08d926bc47..bc07950c4a 100644 --- a/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts +++ b/keep-ui/widgets/alerts-table/ui/useAlertsTableData.ts @@ -1,8 +1,10 @@ import { TimeFrameV2 } from "@/components/ui/DateRangePickerV2"; import { AlertDto, AlertsQuery, useAlerts } from "@/entities/alerts/model"; +import { useApi } from "@/shared/lib/hooks/useApi"; import { useAlertPolling } from "@/utils/hooks/useAlertPolling"; +import { toDateObjectWithFallback } from "@/utils/helpers"; import { v4 as uuidv4 } from "uuid"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; export interface AlertsTableDataQuery { searchCel: string; @@ -32,7 +34,69 @@ function getDateRangeCel(timeFrame: TimeFrameV2 | null): string | null { return ""; } +function getAlertSortValue( + alert: AlertDto, + sortBy: string +): string | number { + const value = (alert as unknown as Record)[sortBy]; + if (value instanceof Date) { + return value.getTime(); + } + if (typeof value === "string") { + return value.toLowerCase(); + } + if (typeof value === "number") { + return value; + } + return ""; +} + +function sortAlerts( + alerts: AlertDto[], + sortOptions?: { sortBy: string; sortDirection?: "ASC" | "DESC" }[] +): AlertDto[] { + if (!sortOptions?.length) { + return alerts; + } + + const [{ sortBy, sortDirection = "ASC" }] = sortOptions; + const direction = sortDirection === "DESC" ? -1 : 1; + + return [...alerts].sort((left, right) => { + const leftValue = getAlertSortValue(left, sortBy); + const rightValue = getAlertSortValue(right, sortBy); + + if (leftValue < rightValue) { + return -1 * direction; + } + if (leftValue > rightValue) { + return 1 * direction; + } + return 0; + }); +} + +function mergeAndEvict( + existing: AlertDto[], + incoming: AlertDto[], + evictedFingerprints: string[] +): AlertDto[] { + const evicted = new Set(evictedFingerprints); + const merged = new Map( + existing + .filter((alert) => !evicted.has(alert.fingerprint)) + .map((alert) => [alert.fingerprint, alert]) + ); + + for (const alert of incoming) { + merged.set(alert.fingerprint, alert); + } + + return Array.from(merged.values()); +} + export const useAlertsTableData = (query: AlertsTableDataQuery | undefined) => { + const api = useApi(); const { useLastAlerts } = useAlerts(); const [shouldRefreshDate, setShouldRefreshDate] = useState(false); @@ -100,21 +164,55 @@ export const useAlertsTableData = (query: AlertsTableDataQuery | undefined) => { useEffect(() => updateAlertsCelDateRange(), [query?.timeFrame]); - const { data: alertsChangeToken } = useAlertPolling(!isPaused); + const { data: alertsChangeToken, fingerprints: polledFingerprints } = + useAlertPolling(!isPaused); - useEffect(() => { - // When refresh token comes, this code allows polling for certain time and then stops. - // Will start polling again when new refresh token comes. - // Why? Because events are throttled on BE side but we want to refresh the data frequently - // when keep gets ingested with data, and it requires control when to refresh from the UI side. - if (alertsChangeToken) { - setShouldRefreshDate(true); - const timeout = setTimeout(() => { - setShouldRefreshDate(false); - }, 15000); - return () => clearTimeout(timeout); - } - }, [alertsChangeToken]); + const [alertsToReturn, setAlertsToReturn] = useState< + AlertDto[] | undefined + >(); + const alertsToReturnRef = useRef(alertsToReturn); + alertsToReturnRef.current = alertsToReturn; + + const patchVisibleAlerts = useCallback( + async ( + fingerprints: string[], + visibleAlerts: AlertDto[], + sortOptions?: AlertsTableDataQuery["sortOptions"] + ) => { + if (!api.isReady() || fingerprints.length === 0 || visibleAlerts.length === 0) { + return; + } + + try { + const batchAlerts = (await api.post( + "/alerts/batch", + fingerprints + )) as AlertDto[]; + const updates = batchAlerts.map((alert) => ({ + ...alert, + lastReceived: toDateObjectWithFallback(alert.lastReceived), + })); + + const returnedFingerprints = new Set( + updates.map((alert) => alert.fingerprint) + ); + const evicted = fingerprints.filter( + (fingerprint) => !returnedFingerprints.has(fingerprint) + ); + + setAlertsToReturn((current) => + sortAlerts( + mergeAndEvict(current ?? visibleAlerts, updates, evicted), + sortOptions + ) + ); + setFacetsPanelRefreshToken(uuidv4()); + } catch { + setShouldRefreshDate(true); + } + }, + [api] + ); useEffect(() => { if (isPaused) { @@ -182,9 +280,6 @@ export const useAlertsTableData = (query: AlertsTableDataQuery | undefined) => { revalidateOnMount: true, }); - const [alertsToReturn, setAlertsToReturn] = useState< - AlertDto[] | undefined - >(); useEffect(() => { if (!alerts) { return; @@ -201,6 +296,48 @@ export const useAlertsTableData = (query: AlertsTableDataQuery | undefined) => { setAlertsToReturn(alertsLoading ? undefined : alerts); }, [isPaused, alertsLoading, alerts]); + useEffect(() => { + // When refresh token comes, this code allows polling for certain time and then stops. + // Will start polling again when new refresh token comes. + // Why? Because events are throttled on BE side but we want to refresh the data frequently + // when keep gets ingested with data, and it requires control when to refresh from the UI side. + if (!alertsChangeToken) { + return; + } + + if (polledFingerprints.length > 0 && !isPaused) { + const visibleAlerts = alertsToReturnRef.current ?? alerts; + const visibleFingerprints = new Set( + (visibleAlerts ?? []).map((alert) => alert.fingerprint) + ); + const allVisible = polledFingerprints.every((fingerprint) => + visibleFingerprints.has(fingerprint) + ); + + if (allVisible && visibleAlerts?.length) { + void patchVisibleAlerts( + polledFingerprints, + visibleAlerts, + query?.sortOptions + ); + return; + } + } + + setShouldRefreshDate(true); + const timeout = setTimeout(() => { + setShouldRefreshDate(false); + }, 15000); + return () => clearTimeout(timeout); + }, [ + alertsChangeToken, + polledFingerprints, + isPaused, + alerts, + patchVisibleAlerts, + query?.sortOptions, + ]); + return { alerts: alertsToReturn, totalCount, diff --git a/keep/api/consts.py b/keep/api/consts.py index 0419687e30..f2f8ce9e00 100644 --- a/keep/api/consts.py +++ b/keep/api/consts.py @@ -54,3 +54,11 @@ OPENAI_MODEL_NAME = os.environ.get("OPENAI_MODEL_NAME", "gpt-4o-2024-08-06") KEEP_CORRELATION_ENABLED = os.environ.get("KEEP_CORRELATION_ENABLED", "true") == "true" + +FINGERPRINT_PAYLOAD_LIMIT = 100 + + +def fingerprints_for_poll_payload(fingerprints: list[str]) -> list[str]: + if len(fingerprints) <= FINGERPRINT_PAYLOAD_LIMIT: + return fingerprints + return [] diff --git a/keep/api/routes/alerts.py b/keep/api/routes/alerts.py index 476027767c..890a86b621 100644 --- a/keep/api/routes/alerts.py +++ b/keep/api/routes/alerts.py @@ -20,7 +20,7 @@ from keep.api.arq_pool import get_pool from keep.api.bl.enrichments_bl import EnrichmentsBl -from keep.api.consts import KEEP_ARQ_QUEUE_BASIC +from keep.api.consts import KEEP_ARQ_QUEUE_BASIC, fingerprints_for_poll_payload from keep.api.core.alerts import ( get_alert_facets, get_alert_facets_data, @@ -279,6 +279,27 @@ def get_all_alerts( return enriched_alerts_dto +@router.post("/batch", description="Get alerts by fingerprints") +def get_alerts_by_fingerprints_batch( + fingerprints: list[str], + authenticated_entity: AuthenticatedEntity = Depends( + IdentityManagerFactory.get_auth_verifier(["read:alert"]) + ), +) -> list[AlertDto]: + tenant_id = authenticated_entity.tenant_id + if not fingerprints: + return [] + + last_alerts = get_last_alerts_by_fingerprints(tenant_id, fingerprints) + alert_ids = [last_alert.alert_id for last_alert in last_alerts] + if not alert_ids: + return [] + + db_alerts = get_alerts_by_ids(tenant_id, alert_ids) + db_alerts = enrich_alerts_with_incidents(tenant_id, db_alerts) + return convert_db_alerts_to_dto_alerts(db_alerts, with_incidents=True) + + @router.get("/{fingerprint}/history", description="Get alert history") def get_alert_history( fingerprint: str, @@ -972,7 +993,7 @@ def batch_enrich_alerts( pusher_client.trigger( f"private-{tenant_id}", "poll-alerts", - "{}", + {"fingerprints": fingerprints_for_poll_payload(fingerprints)}, ) logger.info("Told client to poll alerts") except Exception: @@ -1121,7 +1142,11 @@ def _enrich_alert( pusher_client.trigger( f"private-{tenant_id}", "poll-alerts", - "{}", + { + "fingerprints": fingerprints_for_poll_payload( + [enrich_data.fingerprint] + ) + }, ) logger.info("Told client to poll alerts") except Exception: @@ -1238,7 +1263,11 @@ def unenrich_alert( pusher_client.trigger( f"private-{tenant_id}", "poll-alerts", - "{}", + { + "fingerprints": fingerprints_for_poll_payload( + [enrich_data.fingerprint] + ) + }, ) logger.info("Told client to poll alerts") except Exception: diff --git a/keep/api/tasks/process_event_task.py b/keep/api/tasks/process_event_task.py index e6e36f0130..d74362e92b 100644 --- a/keep/api/tasks/process_event_task.py +++ b/keep/api/tasks/process_event_task.py @@ -21,7 +21,7 @@ from keep.api.bl.enrichments_bl import EnrichmentsBl from keep.api.bl.incidents_bl import IncidentBl from keep.api.bl.maintenance_windows_bl import MaintenanceWindowsBl -from keep.api.consts import KEEP_CORRELATION_ENABLED, MAINTENANCE_WINDOW_ALERT_STRATEGY +from keep.api.consts import KEEP_CORRELATION_ENABLED, MAINTENANCE_WINDOW_ALERT_STRATEGY, fingerprints_for_poll_payload from keep.api.core.db import ( bulk_upsert_alert_fields, enrich_alerts_with_incidents, @@ -589,10 +589,19 @@ def __handle_formatted_events( # Tell the client to poll alerts if pusher_cache.should_notify(tenant_id, "poll-alerts"): try: + alert_fingerprints = [ + event.fingerprint + for event in enriched_formatted_events + if event.fingerprint + ] pusher_client.trigger( f"private-{tenant_id}", "poll-alerts", - "{}", + { + "fingerprints": fingerprints_for_poll_payload( + alert_fingerprints + ) + }, ) logger.info("Told client to poll alerts") except Exception: diff --git a/tests/test_alerts_batch.py b/tests/test_alerts_batch.py new file mode 100644 index 0000000000..26c8b12dee --- /dev/null +++ b/tests/test_alerts_batch.py @@ -0,0 +1,26 @@ +from datetime import datetime, timezone + +import pytest + +from keep.api.models.alert import AlertStatus +from tests.fixtures.client import client, test_app # noqa: F401 + + +@pytest.mark.parametrize("test_app", ["NO_AUTH"], indirect=True) +def test_get_alerts_batch_by_fingerprints( + db_session, client, test_app, create_alert +): + timestamp = datetime.now(timezone.utc) + create_alert("fp-batch-1", AlertStatus.FIRING, timestamp) + create_alert("fp-batch-2", AlertStatus.FIRING, timestamp) + + response = client.post( + "/alerts/batch", + headers={"x-api-key": "some-key"}, + json=["fp-batch-1", "fp-batch-2", "fp-missing"], + ) + + assert response.status_code == 200 + results = response.json() + returned_fingerprints = {alert["fingerprint"] for alert in results} + assert returned_fingerprints == {"fp-batch-1", "fp-batch-2"} diff --git a/tests/test_poll_alerts_payload.py b/tests/test_poll_alerts_payload.py new file mode 100644 index 0000000000..f8f8dd3a49 --- /dev/null +++ b/tests/test_poll_alerts_payload.py @@ -0,0 +1,11 @@ +from keep.api.consts import FINGERPRINT_PAYLOAD_LIMIT, fingerprints_for_poll_payload + + +def test_fingerprints_for_poll_payload_within_limit(): + fingerprints = [f"fp-{index}" for index in range(FINGERPRINT_PAYLOAD_LIMIT)] + assert fingerprints_for_poll_payload(fingerprints) == fingerprints + + +def test_fingerprints_for_poll_payload_above_limit_returns_empty(): + fingerprints = [f"fp-{index}" for index in range(FINGERPRINT_PAYLOAD_LIMIT + 1)] + assert fingerprints_for_poll_payload(fingerprints) == []