Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 101 additions & 11 deletions apps/server/src/libs/test/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,43 +64,133 @@ mock.module("@openstatus/upstash", () => ({
},
}));

const tinybirdMockData = {
httpListBiweekly: [] as unknown[],
httpGetBiweekly: [] as unknown[],
};
const tinybirdMockCalls = {
httpListBiweekly: [] as unknown[],
httpGetBiweekly: [] as unknown[],
};

(globalThis as Record<string, unknown>).__tinybirdMockData = tinybirdMockData;
(globalThis as Record<string, unknown>).__tinybirdMockCalls = tinybirdMockCalls;

const emptyTinybirdResponse = () => Promise.resolve({ data: [] });

const tbMock = {
get legacy_httpStatus45d() {
return emptyTinybirdResponse;
},
get legacy_tcpStatus45d() {
return emptyTinybirdResponse;
},
get httpListBiweekly() {
return (params: unknown) => {
tinybirdMockCalls.httpListBiweekly.push(params);
return Promise.resolve({ data: tinybirdMockData.httpListBiweekly });
};
},
get httpGetBiweekly() {
return (params: unknown) => {
tinybirdMockCalls.httpGetBiweekly.push(params);
return Promise.resolve({ data: tinybirdMockData.httpGetBiweekly });
};
},
get httpMetricsDaily() {
return emptyTinybirdResponse;
},
get httpMetricsWeekly() {
return emptyTinybirdResponse;
},
get httpMetricsBiweekly() {
return emptyTinybirdResponse;
},
get tcpMetricsDaily() {
return emptyTinybirdResponse;
},
get tcpMetricsWeekly() {
return emptyTinybirdResponse;
},
get tcpMetricsBiweekly() {
return emptyTinybirdResponse;
},
get dnsMetricsDaily() {
return emptyTinybirdResponse;
},
get dnsMetricsWeekly() {
return emptyTinybirdResponse;
},
get dnsMetricsBiweekly() {
return emptyTinybirdResponse;
},
};

mock.module("@/libs/clients", () => ({
tb: tbMock,
redis: {
get: (key: string) => Promise.resolve(testRedisStore.get(key) ?? null),
set: (key: string, value: string) => {
testRedisStore.set(key, value);
return Promise.resolve("OK");
},
del: (key: string) => {
const existed = testRedisStore.has(key) ? 1 : 0;
testRedisStore.delete(key);
return Promise.resolve(existed);
},
getdel: (key: string) => {
const value = testRedisStore.get(key) ?? null;
testRedisStore.delete(key);
return Promise.resolve(value);
},
expire: (_key: string, _seconds: number) => Promise.resolve(1),
},
}));

mock.module("@openstatus/tinybird", () => ({
OSTinybird: class {
get legacy_httpStatus45d() {
return () => Promise.resolve({ data: [] });
return tbMock.legacy_httpStatus45d;
}
get legacy_tcpStatus45d() {
return () => Promise.resolve({ data: [] });
return tbMock.legacy_tcpStatus45d;
}
get httpListBiweekly() {
return tbMock.httpListBiweekly;
}
get httpGetBiweekly() {
return tbMock.httpGetBiweekly;
}
// HTTP metrics for GetMonitorSummary
get httpMetricsDaily() {
return () => Promise.resolve({ data: [] });
return tbMock.httpMetricsDaily;
}
get httpMetricsWeekly() {
return () => Promise.resolve({ data: [] });
return tbMock.httpMetricsWeekly;
}
get httpMetricsBiweekly() {
return () => Promise.resolve({ data: [] });
return tbMock.httpMetricsBiweekly;
}
// TCP metrics for GetMonitorSummary
get tcpMetricsDaily() {
return () => Promise.resolve({ data: [] });
return tbMock.tcpMetricsDaily;
}
get tcpMetricsWeekly() {
return () => Promise.resolve({ data: [] });
return tbMock.tcpMetricsWeekly;
}
get tcpMetricsBiweekly() {
return () => Promise.resolve({ data: [] });
return tbMock.tcpMetricsBiweekly;
}
// DNS metrics for GetMonitorSummary
get dnsMetricsDaily() {
return () => Promise.resolve({ data: [] });
return tbMock.dnsMetricsDaily;
}
get dnsMetricsWeekly() {
return () => Promise.resolve({ data: [] });
return tbMock.dnsMetricsWeekly;
}
get dnsMetricsBiweekly() {
return () => Promise.resolve({ data: [] });
return tbMock.dnsMetricsBiweekly;
}
},
}));
194 changes: 193 additions & 1 deletion apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
test,
} from "bun:test";
import { and, db, eq } from "@openstatus/db";
import {
auditLog,
Expand Down Expand Up @@ -37,6 +44,57 @@ let testDnsMonitorId: number;
let testMonitorToDeleteId: number;
let testMonitorWithStatusId: number;

type TinybirdMockData = {
httpListBiweekly: unknown[];
httpGetBiweekly: unknown[];
};

type TinybirdMockCalls = {
httpListBiweekly: unknown[];
httpGetBiweekly: unknown[];
};

function getTinybirdMocks() {
return {
data: (globalThis as typeof globalThis & {
__tinybirdMockData: TinybirdMockData;
}).__tinybirdMockData,
calls: (globalThis as typeof globalThis & {
__tinybirdMockCalls: TinybirdMockCalls;
}).__tinybirdMockCalls,
};
}

function responseLog(overrides: Record<string, unknown> = {}) {
return {
id: "log_1",
latency: 123,
statusCode: 503,
monitorId: String(testHttpMonitorId),
requestStatus: "error",
region: "iad",
cronTimestamp: 1777667323000,
trigger: "api",
timestamp: 1777667323740,
timing: {
dns: 1,
connect: 2,
tls: 3,
ttfb: 4,
transfer: 5,
},
...overrides,
};
}

beforeEach(() => {
const { data, calls } = getTinybirdMocks();
data.httpListBiweekly = [];
data.httpGetBiweekly = [];
calls.httpListBiweekly = [];
calls.httpGetBiweekly = [];
});

beforeAll(async () => {
// Clean up any existing test data
await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-http`));
Expand Down Expand Up @@ -2398,3 +2456,137 @@ describe("MonitorService.GetMonitorSummary", () => {
expect(res.status).toBe(401);
});
});

describe("MonitorService.ListMonitorResponseLogs", () => {
test("returns paginated response logs for an HTTP monitor", async () => {
const { data: mockData, calls } = getTinybirdMocks();
mockData.httpListBiweekly = [
responseLog({ id: "log_1", statusCode: 200, requestStatus: "success" }),
responseLog({ id: "log_2", statusCode: 503, requestStatus: "error" }),
responseLog({ id: "log_3", statusCode: 200, requestStatus: "success" }),
];

const res = await connectRequest(
"ListMonitorResponseLogs",
{
id: String(testHttpMonitorId),
limit: 2,
offset: 20,
},
{ "x-openstatus-key": "1" },
);

expect(res.status).toBe(200);

const result = await res.json();
expect(result.logs).toHaveLength(2);
expect(result.logs[0]).toMatchObject({
id: "log_1",
statusCode: 200,
monitorId: String(testHttpMonitorId),
requestStatus: "RESPONSE_LOG_REQUEST_STATUS_SUCCESS",
region: "iad",
trigger: "RESPONSE_LOG_TRIGGER_API",
timestamp: "1777667323740",
timing: { dns: 1, connect: 2, tls: 3, ttfb: 4, transfer: 5 },
});
expect(result.pagination.limit).toBe(2);
expect(result.pagination.offset).toBe(20);
expect(result.pagination.hasMore).toBe(true);
expect(result.pagination.nextOffset).toBe(22);
expect(calls.httpListBiweekly).toEqual([
{
monitorId: String(testHttpMonitorId),
fromDate: undefined,
toDate: undefined,
limit: 3,
offset: 20,
},
]);
});

test("returns not found for a monitor in another workspace", async () => {
const res = await connectRequest(
"ListMonitorResponseLogs",
{ id: "5" },
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
{ "x-openstatus-key": "1" },
);

expect(res.status).toBe(404);
});

test("rejects non-HTTP monitors", async () => {
const res = await connectRequest(
"ListMonitorResponseLogs",
{ id: String(testTcpMonitorId) },
{ "x-openstatus-key": "1" },
);

expect(res.status).toBe(400);
});
});

describe("MonitorService.GetMonitorResponseLog", () => {
test("returns response log details without exposing the response body", async () => {
const { data } = getTinybirdMocks();
data.httpGetBiweekly = [
responseLog({
id: "log_error",
url: "https://example.com/fail",
error: true,
message: "status assertion failed",
headers: {
"Content-Type": "text/plain",
"X-Trace-Id": "trace_123",
Authorization: "Bearer secret",
"X-Session-Id": "session_123",
},
assertions: '[{"type":"status","compare":"eq","target":200}]',
body: "sensitive body",
}),
];

const res = await connectRequest(
"GetMonitorResponseLog",
{ id: String(testHttpMonitorId), logId: "log_error" },
{ "x-openstatus-key": "1" },
);

expect(res.status).toBe(200);

const result = await res.json();
expect(result.log).toMatchObject({
url: "https://example.com/fail",
error: true,
message: "status assertion failed",
headers: {
"Content-Type": "text/plain",
"X-Trace-Id": "trace_123",
Authorization: "[redacted]",
"X-Session-Id": "[redacted]",
},
assertions: '[{"type":"status","compare":"eq","target":200}]',
});
expect(result.log.body).toBeUndefined();
});

test("returns not found when the response log does not exist", async () => {
const res = await connectRequest(
"GetMonitorResponseLog",
{ id: String(testHttpMonitorId), logId: "missing-log-id" },
{ "x-openstatus-key": "1" },
);

expect(res.status).toBe(404);
});

test("requires a log id", async () => {
const res = await connectRequest(
"GetMonitorResponseLog",
{ id: String(testHttpMonitorId) },
{ "x-openstatus-key": "1" },
);

expect(res.status).toBe(400);
});
});
28 changes: 28 additions & 0 deletions apps/server/src/routes/rpc/handlers/monitor/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const ErrorReason = {
MONITOR_RUN_CREATE_FAILED: "MONITOR_RUN_CREATE_FAILED",
MONITOR_INVALID_DATA: "MONITOR_INVALID_DATA",
MONITOR_TYPE_MISMATCH: "MONITOR_TYPE_MISMATCH",
RESPONSE_LOG_NOT_FOUND: "RESPONSE_LOG_NOT_FOUND",
RESPONSE_LOGS_NOT_ENABLED: "RESPONSE_LOGS_NOT_ENABLED",
RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
} as const;

Expand Down Expand Up @@ -123,6 +125,32 @@ export function monitorTypeMismatchError(
);
}

/**
* Creates a "response log not found" error.
*/
export function responseLogNotFoundError(
monitorId: string,
logId: string,
): ConnectError {
return createError(
"Response log not found",
Code.NotFound,
ErrorReason.RESPONSE_LOG_NOT_FOUND,
{ "monitor-id": monitorId, "log-id": logId },
);
}

/**
* Creates a "response logs not enabled" error.
*/
export function responseLogsNotEnabledError(): ConnectError {
return createError(
"Upgrade for response logs",
Code.ResourceExhausted,
ErrorReason.RESPONSE_LOGS_NOT_ENABLED,
);
}

/**
* Creates a "failed to parse monitor data" error.
*/
Expand Down
Loading