Openstatus www.openstatus.dev

Add more tests vibe coding ftw (#1699)

* Initial plan

* Add test files for missing API endpoints

Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>

* Fix import ordering in test files

Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>

* Add test cases with different payloads for each endpoint

- Add TCP monitor tests for run and trigger endpoints
- Add multiple region tests for run and trigger endpoints
- Add different status tests for status report updates
- Add result validation tests for monitor results endpoint
- Total: 8 new test cases across 4 test files

Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>

* Add 11 test cases with different payloads to post_tcp.test.ts

- Test different ports (80, 443, 8080)
- Test different frequencies (30s, 1m, 5m, 10m, 30m, 1h)
- Test with/without optional fields (timeout, retry, degradedAfter, description)
- Test OpenTelemetry configuration
- Test multiple regions
- Test public/private monitors
- Test active/inactive monitors
- Test invalid host and port
- Test deprecated regions
- Total: 14 test cases (was 3, added 11)

Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>

* Add test cases with different payloads to post_http and statusReportUpdates

HTTP Monitor Tests (post_http.test.ts):
- Different HTTP methods: GET, PUT, PATCH, DELETE
- Different frequencies: 30s, 1h
- Different assertion types: textBody, multiple assertions
- Configuration variants: timeout, retry, degradedAfter
- OpenTelemetry configuration with custom headers
- Minimal payload (only required fields)
- Invalid URL and deprecated regions validation
Total: 16 test cases (was 3, added 13)

Status Report Update Tests (post.test.ts):
- All status types: investigating, identified, monitoring, resolved
- With/without explicit date (testing defaults)
- Past dates and long messages
- Different status report IDs
- Invalid payloads: invalid status, missing message, missing statusReportId, empty message
Total: 14 test cases (was 3, added 11)

Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>

* Fix date manipulation in statusReportUpdates test

Use explicit time subtraction instead of setDate for clearer 24-hour subtraction

Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>

* Fix empty message validation in StatusReportUpdate schema (#1703)

* Initial plan

* Fix validation issues in statusReportUpdates schema and deprecated endpoint

- Add .min(1) validation to message field to reject empty messages
- Fix deprecated endpoint schema to omit statusReportId from request body
- Fix deprecated endpoint to return correct StatusReport schema
- Fix test to use correct workspace for cross-workspace access check

Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>

* Remove redundant database update in deprecated endpoint

- Consolidate status and updatedAt updates into single query
- Remove duplicate update operation that was updating the same fields

Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>

* ci: apply automated fixes

* fix test

* ci: apply automated fixes

* fixing test

* fix test

* ci: apply automated fixes

* add more test

* ci: apply automated fixes

* todo test

* ci: apply automated fixes

* todo test

* ci: apply automated fixes

* todo test

* ci: apply automated fixes

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thibaultleouay <13894054+thibaultleouay@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Thibault Le Ouay Ducasse <thibaultleouay@gmail.com>

+2016 -95
+1 -1
apps/server/.env.test
··· 12 12 SQLD_HTTP_AUTH=basic:token 13 13 SCREENSHOT_SERVICE_URL=http://your.endpoint 14 14 NEXT_PUBLIC_OPENPANEL_CLIENT_ID=test 15 - OPENPANEL_CLIENT_SECRET=test 15 + OPENPANEL_CLIENT_SECRET=test
+2 -2
apps/server/package.json
··· 14 14 "dependencies": { 15 15 "@hono/sentry": "1.2.2", 16 16 "@hono/zod-openapi": "1.1.5", 17 - "@hono/zod-validator": "0.2.2", 17 + "@hono/zod-validator": "0.7.6", 18 18 "@logtape/logtape": "1.1.2", 19 19 "@logtape/sentry": "1.1.2", 20 20 "@openstatus/analytics": "workspace:*", ··· 31 31 "@t3-oss/env-core": "0.13.10", 32 32 "@unkey/api": "2.2.0", 33 33 "@upstash/qstash": "2.6.2", 34 - "hono": "4.5.3", 34 + "hono": "4.11.3", 35 35 "nanoid": "5.0.7", 36 36 "percentile": "1.6.0", 37 37 "validator": "13.12.0",
+84 -24
apps/server/src/routes/v1/check/http/post.test.ts
··· 81 81 }); 82 82 }); 83 83 84 - test.todo("Create a multiple check ", async () => { 84 + test("Create a multiple check", async () => { 85 85 const data = { 86 86 url: "https://www.openstatus.dev", 87 87 regions: ["ams", "gru"], ··· 89 89 body: '{"hello":"world"}', 90 90 headers: [{ key: "key", value: "value" }], 91 91 }; 92 - mockFetch.mockReturnValue( 93 - Promise.resolve( 94 - new Response( 95 - '{"status":200,"latency":100,"body":"Hello World","headers":{"Content-Type":"application/json"},"timestamp":1234567890,"timing":{"dnsStart":1,"dnsDone":2,"connectStart":3,"connectDone":4,"tlsHandshakeStart":5,"tlsHandshakeDone":6,"firstByteStart":7,"firstByteDone":8,"transferStart":9,"transferDone":10},"region":"ams"}', 96 - { status: 200, headers: { "content-type": "application/json" } }, 97 - ), 98 - ), 99 - ); 100 92 101 - const res = await app.request("/v1/check", { 93 + const amsResponse = { 94 + status: 200, 95 + latency: 100, 96 + body: "Hello from ams", 97 + headers: { "Content-Type": "application/json" }, 98 + timestamp: 1234567890, 99 + timing: { 100 + dnsStart: 1, 101 + dnsDone: 2, 102 + connectStart: 3, 103 + connectDone: 4, 104 + tlsHandshakeStart: 5, 105 + tlsHandshakeDone: 6, 106 + firstByteStart: 7, 107 + firstByteDone: 8, 108 + transferStart: 9, 109 + transferDone: 10, 110 + }, 111 + region: "ams", 112 + }; 113 + 114 + const gruResponse = { 115 + status: 200, 116 + latency: 150, 117 + body: "Hello from gru", 118 + headers: { "Content-Type": "application/json" }, 119 + timestamp: 1234567891, 120 + timing: { 121 + dnsStart: 11, 122 + dnsDone: 12, 123 + connectStart: 13, 124 + connectDone: 14, 125 + tlsHandshakeStart: 15, 126 + tlsHandshakeDone: 16, 127 + firstByteStart: 17, 128 + firstByteDone: 18, 129 + transferStart: 19, 130 + transferDone: 20, 131 + }, 132 + region: "gru", 133 + }; 134 + 135 + mockFetch 136 + .mockResolvedValueOnce( 137 + new Response(JSON.stringify(amsResponse), { 138 + status: 200, 139 + headers: { "content-type": "application/json" }, 140 + }), 141 + ) 142 + .mockResolvedValueOnce( 143 + new Response(JSON.stringify(gruResponse), { 144 + status: 200, 145 + headers: { "content-type": "application/json" }, 146 + }), 147 + ); 148 + 149 + const res = await app.request("/v1/check/http", { 102 150 method: "POST", 103 151 headers: { 104 152 "x-openstatus-key": "1", ··· 124 172 transferDone: 10, 125 173 transferStart: 9, 126 174 }, 175 + { 176 + connectDone: 14, 177 + connectStart: 13, 178 + dnsDone: 12, 179 + dnsStart: 11, 180 + firstByteDone: 18, 181 + firstByteStart: 17, 182 + tlsHandshakeDone: 16, 183 + tlsHandshakeStart: 15, 184 + transferDone: 20, 185 + transferStart: 19, 186 + }, 127 187 ], 128 188 response: { 129 - body: "Hello World", 189 + body: "Hello from gru", 130 190 headers: { 131 191 "Content-Type": "application/json", 132 192 }, 133 - latency: 100, 134 - region: "ams", 193 + latency: 150, 194 + region: "gru", 135 195 status: 200, 136 - timestamp: 1234567890, 196 + timestamp: 1234567891, 137 197 timing: { 138 - connectDone: 4, 139 - connectStart: 3, 140 - dnsDone: 2, 141 - dnsStart: 1, 142 - firstByteDone: 8, 143 - firstByteStart: 7, 144 - tlsHandshakeDone: 6, 145 - tlsHandshakeStart: 5, 146 - transferDone: 10, 147 - transferStart: 9, 198 + connectDone: 14, 199 + connectStart: 13, 200 + dnsDone: 12, 201 + dnsStart: 11, 202 + firstByteDone: 18, 203 + firstByteStart: 17, 204 + tlsHandshakeDone: 16, 205 + tlsHandshakeStart: 15, 206 + transferDone: 20, 207 + transferStart: 19, 148 208 }, 149 209 }, 150 210 });
+10
apps/server/src/routes/v1/incidents/get.test.ts
··· 21 21 expect(res.status).toBe(401); 22 22 }); 23 23 24 + test("invalid incident id should return 400", async () => { 25 + const res = await app.request("/v1/incident/invalid-id", { 26 + headers: { 27 + "x-openstatus-key": "1", 28 + }, 29 + }); 30 + 31 + expect(res.status).toBe(400); 32 + }); 33 + 24 34 test("invalid incident id should return 404", async () => { 25 35 const res = await app.request("/v1/incident/2", { 26 36 headers: {
+28 -1
apps/server/src/routes/v1/incidents/put.test.ts
··· 60 60 }); 61 61 62 62 const result = (await res.json()) as Record<string, unknown>; 63 - // expect(result.message).toBe("invalid_date in 'acknowledgedAt': Invalid date"); 64 63 expect(result.message).toBe( 65 64 "invalid_type in 'acknowledgedAt': Invalid input: expected date, received Date", 66 65 ); 66 + expect(res.status).toBe(400); 67 + }); 68 + 69 + test("invalid incident id should return 400", async () => { 70 + const res = await app.request("/v1/incident/invalid-id", { 71 + method: "PUT", 72 + headers: { 73 + "x-openstatus-key": "1", 74 + "Content-Type": "application/json", 75 + }, 76 + body: JSON.stringify({ 77 + acknowledgedAt: new Date().toISOString(), 78 + }), 79 + }); 80 + 81 + expect(res.status).toBe(400); 82 + }); 83 + 84 + test("empty body should return 400", async () => { 85 + const res = await app.request("/v1/incident/2", { 86 + method: "PUT", 87 + headers: { 88 + "x-openstatus-key": "1", 89 + "Content-Type": "application/json", 90 + }, 91 + body: JSON.stringify({}), 92 + }); 93 + 67 94 expect(res.status).toBe(400); 68 95 }); 69 96
+8 -1
apps/server/src/routes/v1/incidents/put.ts
··· 25 25 schema: IncidentSchema.pick({ 26 26 acknowledgedAt: true, 27 27 resolvedAt: true, 28 - }).partial(), 28 + }) 29 + .partial() 30 + .refine( 31 + (data) => 32 + data.acknowledgedAt !== undefined || 33 + data.resolvedAt !== undefined, 34 + "Either acknowledgedAt or resolvedAt must be provided", 35 + ), 29 36 }, 30 37 }, 31 38 },
+1
apps/server/src/routes/v1/incidents/schema.ts
··· 4 4 id: z 5 5 .string() 6 6 .min(1) 7 + .regex(/^\d+$/, "ID must be a numeric string") 7 8 .openapi({ 8 9 param: { 9 10 name: "id",
+10
apps/server/src/routes/v1/maintenances/get.test.ts
··· 20 20 expect(res.status).toBe(401); 21 21 }); 22 22 23 + test("invalid maintenance id should return 400", async () => { 24 + const res = await app.request("/v1/maintenance/invalid-id", { 25 + headers: { 26 + "x-openstatus-key": "1", 27 + }, 28 + }); 29 + 30 + expect(res.status).toBe(400); 31 + }); 32 + 23 33 test("invalid maintenance id should return 404", async () => { 24 34 const res = await app.request("/v1/maintenance/999", { 25 35 headers: {
+107 -1
apps/server/src/routes/v1/maintenances/post.test.ts
··· 2 2 import { app } from "@/index"; 3 3 import { MaintenanceSchema } from "./schema"; 4 4 5 + test("create a valid maintenance without monitorIds", async () => { 6 + const from = new Date(); 7 + const to = new Date(from.getTime() + 3600000); // 1 hour later 8 + 9 + const res = await app.request("/v1/maintenance", { 10 + method: "POST", 11 + headers: { 12 + "x-openstatus-key": "1", 13 + "content-type": "application/json", 14 + }, 15 + body: JSON.stringify({ 16 + title: "Another Maintenance", 17 + message: "Scheduled maintenance without monitors", 18 + from: from.toISOString(), 19 + to: to.toISOString(), 20 + pageId: 1, 21 + }), 22 + }); 23 + 24 + const result = MaintenanceSchema.safeParse(await res.json()); 25 + 26 + expect(res.status).toBe(200); 27 + expect(result.success).toBe(true); 28 + expect(result.data?.monitorIds?.length).toBe(0); 29 + }); 30 + 31 + test("create a maintenance with `from` date after `to` date should return 400", async () => { 32 + const to = new Date(); 33 + const from = new Date(to.getTime() + 3600000); // from is 1 hour after to 34 + 35 + const res = await app.request("/v1/maintenance", { 36 + method: "POST", 37 + headers: { 38 + "x-openstatus-key": "1", 39 + "content-type": "application/json", 40 + }, 41 + body: JSON.stringify({ 42 + title: "Invalid Dates", 43 + message: "Test message", 44 + from: from.toISOString(), 45 + to: to.toISOString(), 46 + pageId: 1, 47 + }), 48 + }); 49 + 50 + expect(res.status).toBe(400); 51 + }); 52 + 53 + test("create a maintenance with non-existent monitorIds should return 400", async () => { 54 + const from = new Date(); 55 + const to = new Date(from.getTime() + 3600000); 56 + 57 + const res = await app.request("/v1/maintenance", { 58 + method: "POST", 59 + headers: { 60 + "x-openstatus-key": "1", 61 + "content-type": "application/json", 62 + }, 63 + body: JSON.stringify({ 64 + title: "Invalid Monitors", 65 + message: "Test message", 66 + from: from.toISOString(), 67 + to: to.toISOString(), 68 + monitorIds: [9999], // Non-existent monitor ID 69 + pageId: 1, 70 + }), 71 + }); 72 + 73 + expect(res.status).toBe(400); 74 + }); 75 + 76 + test("create a maintenance with non-existent pageId should return 400", async () => { 77 + const from = new Date(); 78 + const to = new Date(from.getTime() + 3600000); 79 + 80 + const res = await app.request("/v1/maintenance", { 81 + method: "POST", 82 + headers: { 83 + "x-openstatus-key": "1", 84 + "content-type": "application/json", 85 + }, 86 + body: JSON.stringify({ 87 + title: "Invalid Page", 88 + message: "Test message", 89 + from: from.toISOString(), 90 + to: to.toISOString(), 91 + monitorIds: [1], 92 + pageId: 9999, // Non-existent page ID 93 + }), 94 + }); 95 + 96 + expect(res.status).toBe(400); 97 + }); 98 + 99 + test("create a maintenance with empty body should return 400", async () => { 100 + const res = await app.request("/v1/maintenance", { 101 + method: "POST", 102 + headers: { 103 + "x-openstatus-key": "1", 104 + "content-type": "application/json", 105 + }, 106 + body: JSON.stringify({}), 107 + }); 108 + 109 + expect(res.status).toBe(400); 110 + }); 111 + 5 112 test("create a valid maintenance", async () => { 6 113 const from = new Date(); 7 114 const to = new Date(from.getTime() + 3600000); // 1 hour later ··· 21 128 pageId: 1, 22 129 }), 23 130 }); 24 - 25 131 const result = MaintenanceSchema.safeParse(await res.json()); 26 132 27 133 expect(res.status).toBe(200);
+7
apps/server/src/routes/v1/maintenances/post.ts
··· 70 70 }); 71 71 } 72 72 73 + if (input.from > input.to) { 74 + throw new OpenStatusApiError({ 75 + code: "BAD_REQUEST", 76 + message: "`date.from` cannot be after `date.to`", 77 + }); 78 + } 79 + 73 80 const _page = await db 74 81 .select() 75 82 .from(page)
+131
apps/server/src/routes/v1/maintenances/put.test.ts
··· 41 41 expect(result.data?.monitorIds?.length).toBe(2); 42 42 }); 43 43 44 + test("invalid maintenance id should return 400", async () => { 45 + const res = await app.request("/v1/maintenance/invalid-id", { 46 + method: "PUT", 47 + headers: { 48 + "x-openstatus-key": "1", 49 + "Content-Type": "application/json", 50 + }, 51 + body: JSON.stringify({ 52 + title: "Not Found", 53 + }), 54 + }); 55 + 56 + expect(res.status).toBe(400); 57 + }); 58 + 59 + test("update only the title", async () => { 60 + const res = await app.request("/v1/maintenance/1", { 61 + method: "PUT", 62 + headers: { 63 + "x-openstatus-key": "1", 64 + "Content-Type": "application/json", 65 + }, 66 + body: JSON.stringify({ 67 + title: "Only Title Updated", 68 + }), 69 + }); 70 + 71 + const result = MaintenanceSchema.safeParse(await res.json()); 72 + 73 + expect(res.status).toBe(200); 74 + expect(result.success).toBe(true); 75 + expect(result.data?.title).toBe("Only Title Updated"); 76 + }); 77 + 78 + test("update only the message", async () => { 79 + const res = await app.request("/v1/maintenance/1", { 80 + method: "PUT", 81 + headers: { 82 + "x-openstatus-key": "1", 83 + "Content-Type": "application/json", 84 + }, 85 + body: JSON.stringify({ 86 + message: "Only Message Updated", 87 + }), 88 + }); 89 + 90 + const result = MaintenanceSchema.safeParse(await res.json()); 91 + 92 + expect(res.status).toBe(200); 93 + expect(result.success).toBe(true); 94 + expect(result.data?.message).toBe("Only Message Updated"); 95 + }); 96 + 97 + test.todo("update only the dates", async () => { 98 + const from = new Date(); 99 + const to = new Date(from.getTime() + 7200000); // 2 hours later 100 + 101 + const res = await app.request("/v1/maintenance/1", { 102 + method: "PUT", 103 + headers: { 104 + "x-openstatus-key": "1", 105 + "Content-Type": "application/json", 106 + }, 107 + body: JSON.stringify({ 108 + from: from.toISOString(), 109 + to: to.toISOString(), 110 + }), 111 + }); 112 + 113 + const result = MaintenanceSchema.safeParse(await res.json()); 114 + 115 + expect(res.status).toBe(200); 116 + expect(result.success).toBe(true); 117 + expect(result.data?.from).toEqual(from); 118 + expect(result.data?.to).toEqual(to); 119 + }); 120 + 121 + test.todo( 122 + "update maintenance with `from` date after `to` date should return 400", 123 + async () => { 124 + const to = new Date(); 125 + const from = new Date(to.getTime() + 3600000); // from is 1 hour after to 126 + 127 + const res = await app.request("/v1/maintenance/1", { 128 + method: "PUT", 129 + headers: { 130 + "x-openstatus-key": "1", 131 + "Content-Type": "application/json", 132 + }, 133 + body: JSON.stringify({ 134 + from: from.toISOString(), 135 + to: to.toISOString(), 136 + }), 137 + }); 138 + 139 + expect(res.status).toBe(400); 140 + }, 141 + ); 142 + 143 + test("remove all maintenance monitors", async () => { 144 + const res = await app.request("/v1/maintenance/1", { 145 + method: "PUT", 146 + headers: { 147 + "x-openstatus-key": "1", 148 + "Content-Type": "application/json", 149 + }, 150 + body: JSON.stringify({ 151 + monitorIds: [], 152 + }), 153 + }); 154 + 155 + const result = MaintenanceSchema.safeParse(await res.json()); 156 + 157 + expect(res.status).toBe(200); 158 + expect(result.success).toBe(true); 159 + expect(result.data?.monitorIds?.length).toBe(0); 160 + }); 161 + 162 + test.todo("empty body should return 400", async () => { 163 + const res = await app.request("/v1/maintenance/1", { 164 + method: "PUT", 165 + headers: { 166 + "x-openstatus-key": "1", 167 + "Content-Type": "application/json", 168 + }, 169 + body: JSON.stringify({}), 170 + }); 171 + 172 + expect(res.status).toBe(400); 173 + }); 174 + 44 175 test("invalid maintenance id should return 404", async () => { 45 176 const res = await app.request("/v1/maintenance/999", { 46 177 method: "PUT",
+10
apps/server/src/routes/v1/maintenances/put.ts
··· 101 101 } 102 102 } 103 103 104 + const inputFrom = input?.from ?? _maintenance.from; 105 + const inputTo = input?.to ?? _maintenance?.to; 106 + 107 + if (inputFrom && inputTo && new Date(inputFrom) > new Date(inputTo)) { 108 + throw new OpenStatusApiError({ 109 + code: "BAD_REQUEST", 110 + message: "`date.from` cannot be after `date.to`", 111 + }); 112 + } 113 + 104 114 const updatedMaintenance = await db.transaction(async (tx) => { 105 115 const updated = await tx 106 116 .update(maintenance)
+4
apps/server/src/routes/v1/maintenances/schema.ts
··· 4 4 id: z 5 5 .string() 6 6 .min(1) 7 + .regex(/^\d+$/, "ID must be a numeric string") 7 8 .openapi({ 8 9 param: { 9 10 name: "id", ··· 42 43 pageId: z.number().openapi({ 43 44 description: "The id of the status page this maintenance belongs to", 44 45 }), 46 + }) 47 + .refine((maintenance) => maintenance.from <= maintenance.to, { 48 + error: "'from' date must be before 'to' date", 45 49 }) 46 50 .openapi("Maintenance"); 47 51
+2 -3
apps/server/src/routes/v1/monitors/post.test.ts
··· 33 33 }), 34 34 }); 35 35 36 + expect(res.status).toBe(200); 36 37 const result = MonitorSchema.safeParse(await res.json()); 37 - 38 - expect(res.status).toBe(200); 39 38 expect(result.success).toBe(true); 40 39 }); 41 40 42 - test("create a status report with invalid payload should return 400", async () => { 41 + test("create a monitor with invalid payload should return 400", async () => { 43 42 const res = await app.request("/v1/monitor", { 44 43 method: "POST", 45 44 headers: {
+372 -6
apps/server/src/routes/v1/monitors/post_http.test.ts
··· 11 11 "content-type": "application/json", 12 12 }, 13 13 body: JSON.stringify({ 14 + active: true, 15 + degradedAfter: 60, 16 + description: "This is a test", 14 17 frequency: "10m", 15 - name: "OpenStatus", 16 - description: "OpenStatus website", 17 - regions: ["ams", "gru"], 18 + name: "Test2", 19 + regions: ["iad"], 18 20 request: { 19 - url: "https://www.openstatus.dev", 21 + url: "https://api.openstatus.dev/health", 20 22 method: "POST", 21 23 body: '{"hello":"world"}', 22 24 headers: { "content-type": "application/json" }, 23 25 }, 24 - active: true, 25 - public: true, 26 26 assertions: [ 27 27 { 28 28 kind: "statusCode", ··· 31 31 }, 32 32 { kind: "header", compare: "not_eq", key: "key", target: "value" }, 33 33 ], 34 + retry: 3, 34 35 }), 35 36 }); 36 37 ··· 84 85 85 86 expect(res.status).toBe(401); 86 87 }); 88 + 89 + test("create HTTP monitor with GET method should return 200", async () => { 90 + const res = await app.request("/v1/monitor/http", { 91 + method: "POST", 92 + headers: { 93 + "x-openstatus-key": "1", 94 + "content-type": "application/json", 95 + }, 96 + body: JSON.stringify({ 97 + frequency: "5m", 98 + name: "GET Monitor", 99 + description: "Monitor with GET method", 100 + regions: ["ams"], 101 + request: { 102 + url: "https://api.openstatus.dev/health", 103 + method: "GET", 104 + }, 105 + active: true, 106 + public: false, 107 + }), 108 + }); 109 + 110 + const result = MonitorSchema.safeParse(await res.json()); 111 + expect(res.status).toBe(200); 112 + expect(result.success).toBe(true); 113 + }); 114 + 115 + test("create HTTP monitor with PUT method should return 200", async () => { 116 + const res = await app.request("/v1/monitor/http", { 117 + method: "POST", 118 + headers: { 119 + "x-openstatus-key": "1", 120 + "content-type": "application/json", 121 + }, 122 + body: JSON.stringify({ 123 + frequency: "10m", 124 + name: "PUT Monitor", 125 + description: "Monitor with PUT method", 126 + regions: ["gru"], 127 + request: { 128 + url: "https://api.example.com/resource", 129 + method: "PUT", 130 + body: '{"data":"updated"}', 131 + headers: { authorization: "Bearer token123" }, 132 + }, 133 + active: true, 134 + public: false, 135 + }), 136 + }); 137 + 138 + const result = MonitorSchema.safeParse(await res.json()); 139 + expect(res.status).toBe(200); 140 + expect(result.success).toBe(true); 141 + }); 142 + 143 + test("create HTTP monitor with textBody assertion should return 200", async () => { 144 + const res = await app.request("/v1/monitor/http", { 145 + method: "POST", 146 + headers: { 147 + "x-openstatus-key": "1", 148 + "content-type": "application/json", 149 + }, 150 + body: JSON.stringify({ 151 + frequency: "10m", 152 + name: "Text Body Assertion Monitor", 153 + description: "Monitor with text body assertion", 154 + regions: ["ams"], 155 + request: { 156 + url: "https://www.openstatus.dev", 157 + method: "GET", 158 + }, 159 + assertions: [ 160 + { 161 + kind: "textBody", 162 + compare: "contains", 163 + target: "OpenStatus", 164 + }, 165 + ], 166 + active: true, 167 + public: true, 168 + }), 169 + }); 170 + 171 + const result = MonitorSchema.safeParse(await res.json()); 172 + expect(res.status).toBe(200); 173 + expect(result.success).toBe(true); 174 + }); 175 + 176 + test("create HTTP monitor with multiple assertions should return 200", async () => { 177 + const res = await app.request("/v1/monitor/http", { 178 + method: "POST", 179 + headers: { 180 + "x-openstatus-key": "1", 181 + "content-type": "application/json", 182 + }, 183 + body: JSON.stringify({ 184 + frequency: "10m", 185 + name: "Multi Assertion Monitor", 186 + description: "Monitor with multiple assertions", 187 + regions: ["ams", "gru"], 188 + request: { 189 + url: "https://api.openstatus.dev", 190 + method: "GET", 191 + }, 192 + assertions: [ 193 + { 194 + kind: "statusCode", 195 + compare: "eq", 196 + target: 200, 197 + }, 198 + { 199 + kind: "header", 200 + compare: "contains", 201 + key: "content-type", 202 + target: "json", 203 + }, 204 + { 205 + kind: "textBody", 206 + compare: "contains", 207 + target: "success", 208 + }, 209 + ], 210 + active: true, 211 + public: true, 212 + }), 213 + }); 214 + 215 + const result = MonitorSchema.safeParse(await res.json()); 216 + expect(res.status).toBe(200); 217 + expect(result.success).toBe(true); 218 + }); 219 + 220 + test("create HTTP monitor with timeout and retry configuration should return 200", async () => { 221 + const res = await app.request("/v1/monitor/http", { 222 + method: "POST", 223 + headers: { 224 + "x-openstatus-key": "1", 225 + "content-type": "application/json", 226 + }, 227 + body: JSON.stringify({ 228 + frequency: "10m", 229 + name: "HTTP with custom config", 230 + description: "HTTP monitor with timeout and retry", 231 + regions: ["ams"], 232 + request: { 233 + url: "https://www.openstatus.dev", 234 + method: "GET", 235 + }, 236 + timeout: 60000, 237 + retry: 5, 238 + degradedAfter: 20000, 239 + active: true, 240 + public: false, 241 + }), 242 + }); 243 + 244 + const result = MonitorSchema.safeParse(await res.json()); 245 + expect(res.status).toBe(200); 246 + expect(result.success).toBe(true); 247 + }); 248 + 249 + test("create HTTP monitor with OpenTelemetry configuration should return 200", async () => { 250 + const res = await app.request("/v1/monitor/http", { 251 + method: "POST", 252 + headers: { 253 + "x-openstatus-key": "1", 254 + "content-type": "application/json", 255 + }, 256 + body: JSON.stringify({ 257 + frequency: "10m", 258 + name: "HTTP with OTEL", 259 + description: "HTTP monitor with OpenTelemetry", 260 + regions: ["ams"], 261 + request: { 262 + url: "https://www.openstatus.dev", 263 + method: "GET", 264 + }, 265 + openTelemetry: { 266 + endpoint: "https://otel.example.com/v1/traces", 267 + headers: { 268 + "x-api-key": "otel-key-123", 269 + "x-tenant-id": "tenant-456", 270 + }, 271 + }, 272 + active: true, 273 + public: false, 274 + }), 275 + }); 276 + 277 + const result = MonitorSchema.safeParse(await res.json()); 278 + expect(res.status).toBe(200); 279 + expect(result.success).toBe(true); 280 + }); 281 + 282 + test("create HTTP monitor with 30s frequency should return 200", async () => { 283 + const res = await app.request("/v1/monitor/http", { 284 + method: "POST", 285 + headers: { 286 + "x-openstatus-key": "1", 287 + "content-type": "application/json", 288 + }, 289 + body: JSON.stringify({ 290 + frequency: "30s", 291 + name: "Fast HTTP Check", 292 + description: "HTTP monitor with 30s frequency", 293 + regions: ["ams"], 294 + request: { 295 + url: "https://www.openstatus.dev", 296 + method: "GET", 297 + }, 298 + active: true, 299 + public: false, 300 + }), 301 + }); 302 + 303 + const result = MonitorSchema.safeParse(await res.json()); 304 + expect(res.status).toBe(200); 305 + expect(result.success).toBe(true); 306 + }); 307 + 308 + test("create HTTP monitor with 1h frequency should return 200", async () => { 309 + const res = await app.request("/v1/monitor/http", { 310 + method: "POST", 311 + headers: { 312 + "x-openstatus-key": "1", 313 + "content-type": "application/json", 314 + }, 315 + body: JSON.stringify({ 316 + frequency: "1h", 317 + name: "Hourly HTTP Check", 318 + description: "HTTP monitor with 1h frequency", 319 + regions: ["gru"], 320 + request: { 321 + url: "https://www.openstatus.dev", 322 + method: "GET", 323 + }, 324 + active: true, 325 + public: false, 326 + }), 327 + }); 328 + 329 + const result = MonitorSchema.safeParse(await res.json()); 330 + expect(res.status).toBe(200); 331 + expect(result.success).toBe(true); 332 + }); 333 + 334 + test("create HTTP monitor without optional fields should return 200", async () => { 335 + const res = await app.request("/v1/monitor/http", { 336 + method: "POST", 337 + headers: { 338 + "x-openstatus-key": "1", 339 + "content-type": "application/json", 340 + }, 341 + body: JSON.stringify({ 342 + frequency: "10m", 343 + name: "Minimal HTTP Monitor", 344 + regions: ["ams"], 345 + request: { 346 + url: "https://www.openstatus.dev", 347 + method: "GET", 348 + }, 349 + }), 350 + }); 351 + 352 + const result = MonitorSchema.safeParse(await res.json()); 353 + expect(res.status).toBe(200); 354 + expect(result.success).toBe(true); 355 + }); 356 + 357 + test("create HTTP monitor with PATCH method should return 200", async () => { 358 + const res = await app.request("/v1/monitor/http", { 359 + method: "POST", 360 + headers: { 361 + "x-openstatus-key": "1", 362 + "content-type": "application/json", 363 + }, 364 + body: JSON.stringify({ 365 + frequency: "10m", 366 + name: "PATCH Monitor", 367 + description: "Monitor with PATCH method", 368 + regions: ["ams"], 369 + request: { 370 + url: "https://api.example.com/resource", 371 + method: "PATCH", 372 + body: '{"field":"value"}', 373 + headers: { "content-type": "application/json" }, 374 + }, 375 + active: true, 376 + public: false, 377 + }), 378 + }); 379 + 380 + const result = MonitorSchema.safeParse(await res.json()); 381 + expect(res.status).toBe(200); 382 + expect(result.success).toBe(true); 383 + }); 384 + 385 + test("create HTTP monitor with DELETE method should return 200", async () => { 386 + const res = await app.request("/v1/monitor/http", { 387 + method: "POST", 388 + headers: { 389 + "x-openstatus-key": "1", 390 + "content-type": "application/json", 391 + }, 392 + body: JSON.stringify({ 393 + frequency: "10m", 394 + name: "DELETE Monitor", 395 + description: "Monitor with DELETE method", 396 + regions: ["ams"], 397 + request: { 398 + url: "https://api.example.com/resource/123", 399 + method: "DELETE", 400 + headers: { authorization: "Bearer token" }, 401 + }, 402 + active: true, 403 + public: false, 404 + }), 405 + }); 406 + 407 + const result = MonitorSchema.safeParse(await res.json()); 408 + expect(res.status).toBe(200); 409 + expect(result.success).toBe(true); 410 + }); 411 + 412 + test("create HTTP monitor with invalid URL should return 400", async () => { 413 + const res = await app.request("/v1/monitor/http", { 414 + method: "POST", 415 + headers: { 416 + "x-openstatus-key": "1", 417 + "content-type": "application/json", 418 + }, 419 + body: JSON.stringify({ 420 + frequency: "10m", 421 + name: "Invalid URL Monitor", 422 + regions: ["ams"], 423 + request: { 424 + url: "not-a-valid-url", 425 + method: "GET", 426 + }, 427 + }), 428 + }); 429 + 430 + expect(res.status).toBe(400); 431 + }); 432 + 433 + test("create HTTP monitor with deprecated regions should return 400", async () => { 434 + const res = await app.request("/v1/monitor/http", { 435 + method: "POST", 436 + headers: { 437 + "x-openstatus-key": "1", 438 + "content-type": "application/json", 439 + }, 440 + body: JSON.stringify({ 441 + frequency: "10m", 442 + name: "Deprecated Regions HTTP", 443 + regions: ["ams", "hkg", "waw"], 444 + request: { 445 + url: "https://www.openstatus.dev", 446 + method: "GET", 447 + }, 448 + }), 449 + }); 450 + 451 + expect(res.status).toBe(400); 452 + });
+277
apps/server/src/routes/v1/monitors/post_tcp.test.ts
··· 68 68 69 69 expect(res.status).toBe(401); 70 70 }); 71 + 72 + test("create TCP monitor with port 80 should return 200", async () => { 73 + const res = await app.request("/v1/monitor/tcp", { 74 + method: "POST", 75 + headers: { 76 + "x-openstatus-key": "1", 77 + "content-type": "application/json", 78 + }, 79 + body: JSON.stringify({ 80 + frequency: "5m", 81 + name: "HTTP Port Monitor", 82 + description: "Monitor port 80", 83 + regions: ["ams"], 84 + request: { 85 + host: "example.com", 86 + port: 80, 87 + }, 88 + active: true, 89 + public: false, 90 + }), 91 + }); 92 + 93 + const result = MonitorSchema.safeParse(await res.json()); 94 + expect(res.status).toBe(200); 95 + expect(result.success).toBe(true); 96 + }); 97 + 98 + test("create TCP monitor with custom port should return 200", async () => { 99 + const res = await app.request("/v1/monitor/tcp", { 100 + method: "POST", 101 + headers: { 102 + "x-openstatus-key": "1", 103 + "content-type": "application/json", 104 + }, 105 + body: JSON.stringify({ 106 + frequency: "1m", 107 + name: "Custom Port Monitor", 108 + description: "Monitor custom port 8080", 109 + regions: ["gru"], 110 + request: { 111 + host: "localhost", 112 + port: 8080, 113 + }, 114 + active: false, 115 + public: false, 116 + }), 117 + }); 118 + 119 + const result = MonitorSchema.safeParse(await res.json()); 120 + expect(res.status).toBe(200); 121 + expect(result.success).toBe(true); 122 + }); 123 + 124 + test("create TCP monitor with timeout and retry configuration should return 200", async () => { 125 + const res = await app.request("/v1/monitor/tcp", { 126 + method: "POST", 127 + headers: { 128 + "x-openstatus-key": "1", 129 + "content-type": "application/json", 130 + }, 131 + body: JSON.stringify({ 132 + frequency: "10m", 133 + name: "TCP with custom config", 134 + description: "TCP monitor with timeout and retry", 135 + regions: ["ams"], 136 + request: { 137 + host: "openstatus.dev", 138 + port: 443, 139 + }, 140 + timeout: 30000, 141 + retry: 5, 142 + degradedAfter: 10000, 143 + active: true, 144 + public: true, 145 + }), 146 + }); 147 + 148 + const result = MonitorSchema.safeParse(await res.json()); 149 + expect(res.status).toBe(200); 150 + expect(result.success).toBe(true); 151 + }); 152 + 153 + test("create TCP monitor with OpenTelemetry configuration should return 200", async () => { 154 + const res = await app.request("/v1/monitor/tcp", { 155 + method: "POST", 156 + headers: { 157 + "x-openstatus-key": "1", 158 + "content-type": "application/json", 159 + }, 160 + body: JSON.stringify({ 161 + frequency: "10m", 162 + name: "TCP with OTEL", 163 + description: "TCP monitor with OpenTelemetry", 164 + regions: ["ams"], 165 + request: { 166 + host: "openstatus.dev", 167 + port: 443, 168 + }, 169 + openTelemetry: { 170 + endpoint: "https://otel.example.com", 171 + headers: { 172 + "x-api-key": "test-key", 173 + }, 174 + }, 175 + active: true, 176 + public: false, 177 + }), 178 + }); 179 + 180 + const result = MonitorSchema.safeParse(await res.json()); 181 + expect(res.status).toBe(200); 182 + expect(result.success).toBe(true); 183 + }); 184 + 185 + test("create TCP monitor with multiple regions should return 200", async () => { 186 + const res = await app.request("/v1/monitor/tcp", { 187 + method: "POST", 188 + headers: { 189 + "x-openstatus-key": "1", 190 + "content-type": "application/json", 191 + }, 192 + body: JSON.stringify({ 193 + frequency: "30m", 194 + name: "Multi-region TCP", 195 + description: "TCP monitor across multiple regions", 196 + regions: ["ams", "gru", "syd"], 197 + request: { 198 + host: "openstatus.dev", 199 + port: 443, 200 + }, 201 + active: true, 202 + public: true, 203 + }), 204 + }); 205 + 206 + const result = MonitorSchema.safeParse(await res.json()); 207 + expect(res.status).toBe(200); 208 + expect(result.success).toBe(true); 209 + }); 210 + 211 + test("create TCP monitor with 30s frequency should return 200", async () => { 212 + const res = await app.request("/v1/monitor/tcp", { 213 + method: "POST", 214 + headers: { 215 + "x-openstatus-key": "1", 216 + "content-type": "application/json", 217 + }, 218 + body: JSON.stringify({ 219 + frequency: "30s", 220 + name: "Fast TCP Check", 221 + description: "TCP monitor with 30s frequency", 222 + regions: ["ams"], 223 + request: { 224 + host: "openstatus.dev", 225 + port: 443, 226 + }, 227 + active: true, 228 + public: false, 229 + }), 230 + }); 231 + 232 + const result = MonitorSchema.safeParse(await res.json()); 233 + expect(res.status).toBe(200); 234 + expect(result.success).toBe(true); 235 + }); 236 + 237 + test("create TCP monitor with 1h frequency should return 200", async () => { 238 + const res = await app.request("/v1/monitor/tcp", { 239 + method: "POST", 240 + headers: { 241 + "x-openstatus-key": "1", 242 + "content-type": "application/json", 243 + }, 244 + body: JSON.stringify({ 245 + frequency: "1h", 246 + name: "Hourly TCP Check", 247 + description: "TCP monitor with 1h frequency", 248 + regions: ["gru"], 249 + request: { 250 + host: "example.com", 251 + port: 443, 252 + }, 253 + active: true, 254 + public: false, 255 + }), 256 + }); 257 + 258 + const result = MonitorSchema.safeParse(await res.json()); 259 + expect(res.status).toBe(200); 260 + expect(result.success).toBe(true); 261 + }); 262 + 263 + test("create TCP monitor without optional fields should return 200", async () => { 264 + const res = await app.request("/v1/monitor/tcp", { 265 + method: "POST", 266 + headers: { 267 + "x-openstatus-key": "1", 268 + "content-type": "application/json", 269 + }, 270 + body: JSON.stringify({ 271 + frequency: "10m", 272 + name: "Minimal TCP Monitor", 273 + regions: ["ams"], 274 + request: { 275 + host: "openstatus.dev", 276 + port: 443, 277 + }, 278 + }), 279 + }); 280 + 281 + const result = MonitorSchema.safeParse(await res.json()); 282 + expect(res.status).toBe(200); 283 + expect(result.success).toBe(true); 284 + }); 285 + 286 + test("create TCP monitor with invalid host should return 400", async () => { 287 + const res = await app.request("/v1/monitor/tcp", { 288 + method: "POST", 289 + headers: { 290 + "x-openstatus-key": "1", 291 + "content-type": "application/json", 292 + }, 293 + body: JSON.stringify({ 294 + frequency: "10m", 295 + name: "Invalid TCP Monitor", 296 + regions: ["ams"], 297 + request: { 298 + host: "", 299 + port: 443, 300 + }, 301 + }), 302 + }); 303 + 304 + expect(res.status).toBe(400); 305 + }); 306 + 307 + test("create TCP monitor with invalid port should return 400", async () => { 308 + const res = await app.request("/v1/monitor/tcp", { 309 + method: "POST", 310 + headers: { 311 + "x-openstatus-key": "1", 312 + "content-type": "application/json", 313 + }, 314 + body: JSON.stringify({ 315 + frequency: "10m", 316 + name: "Invalid Port Monitor", 317 + regions: ["ams"], 318 + request: { 319 + host: "openstatus.dev", 320 + port: "not-a-number", 321 + }, 322 + }), 323 + }); 324 + 325 + expect(res.status).toBe(400); 326 + }); 327 + 328 + test("create TCP monitor with deprecated regions should return 400", async () => { 329 + const res = await app.request("/v1/monitor/tcp", { 330 + method: "POST", 331 + headers: { 332 + "x-openstatus-key": "1", 333 + "content-type": "application/json", 334 + }, 335 + body: JSON.stringify({ 336 + frequency: "10m", 337 + name: "Deprecated Regions TCP", 338 + regions: ["ams", "hkg", "waw"], 339 + request: { 340 + host: "openstatus.dev", 341 + port: 443, 342 + }, 343 + }), 344 + }); 345 + 346 + expect(res.status).toBe(400); 347 + });
+89
apps/server/src/routes/v1/monitors/results/get.test.ts
··· 1 + import { expect, test } from "bun:test"; 2 + 3 + import { app } from "@/index"; 4 + import { ResultRun } from "../schema"; 5 + 6 + test.todo("get monitor result with valid id should return 200", async () => { 7 + const res = await app.request("/v1/monitor/1/result/1", { 8 + method: "GET", 9 + headers: { 10 + "x-openstatus-key": "1", 11 + }, 12 + }); 13 + 14 + expect(res.status).toBe(200); 15 + 16 + const json = await res.json(); 17 + const result = ResultRun.array().safeParse(json); 18 + expect(result.success).toBe(true); 19 + }); 20 + 21 + test("get monitor result with invalid monitor id should return 404", async () => { 22 + const res = await app.request("/v1/monitor/999999/result/1", { 23 + method: "GET", 24 + headers: { 25 + "x-openstatus-key": "1", 26 + }, 27 + }); 28 + 29 + expect(res.status).toBe(404); 30 + }); 31 + 32 + test("get monitor result with invalid result id should return 404", async () => { 33 + const res = await app.request("/v1/monitor/1/result/999999", { 34 + method: "GET", 35 + headers: { 36 + "x-openstatus-key": "1", 37 + }, 38 + }); 39 + 40 + expect(res.status).toBe(404); 41 + }); 42 + 43 + test("get monitor result without auth key should return 401", async () => { 44 + const res = await app.request("/v1/monitor/1/result/1", { 45 + method: "GET", 46 + }); 47 + 48 + expect(res.status).toBe(401); 49 + }); 50 + 51 + test("get monitor result from different workspace should return 404", async () => { 52 + const res = await app.request("/v1/monitor/2/result/1", { 53 + method: "GET", 54 + headers: { 55 + "x-openstatus-key": "1", 56 + }, 57 + }); 58 + 59 + expect(res.status).toBe(404); 60 + }); 61 + 62 + test.todo( 63 + "get monitor result with valid TCP monitor should return 200", 64 + async () => { 65 + const res = await app.request("/v1/monitor/4/result/2", { 66 + method: "GET", 67 + headers: { 68 + "x-openstatus-key": "1", 69 + }, 70 + }); 71 + 72 + expect(res.status).toBe(200); 73 + 74 + const json = await res.json(); 75 + const result = ResultRun.array().safeParse(json); 76 + expect(result.success).toBe(true); 77 + }, 78 + ); 79 + 80 + test("get monitor result with non-matching result id should return 404", async () => { 81 + const res = await app.request("/v1/monitor/1/result/2", { 82 + method: "GET", 83 + headers: { 84 + "x-openstatus-key": "1", 85 + }, 86 + }); 87 + 88 + expect(res.status).toBe(404); 89 + });
+189
apps/server/src/routes/v1/monitors/run/post.test.ts
··· 1 + import { expect, test } from "bun:test"; 2 + 3 + import { afterEach, mock } from "bun:test"; 4 + import { app } from "@/index"; 5 + import { TriggerResult } from "../schema"; 6 + 7 + const mockFetch = mock(); 8 + 9 + global.fetch = mockFetch as unknown as typeof fetch; 10 + mock.module("node-fetch", () => mockFetch); 11 + 12 + afterEach(() => { 13 + mockFetch.mockReset(); 14 + }); 15 + 16 + test("run monitor with valid id should return 200", async () => { 17 + mockFetch.mockReturnValue( 18 + Promise.resolve( 19 + new Response( 20 + JSON.stringify({ 21 + jobType: "http", 22 + status: 200, 23 + latency: 100, 24 + region: "ams", 25 + timestamp: 1234567890, 26 + timing: { 27 + dnsStart: 1, 28 + dnsDone: 2, 29 + connectStart: 3, 30 + connectDone: 4, 31 + tlsHandshakeStart: 5, 32 + tlsHandshakeDone: 6, 33 + firstByteStart: 7, 34 + firstByteDone: 8, 35 + transferStart: 9, 36 + transferDone: 10, 37 + }, 38 + }), 39 + { status: 200, headers: { "content-type": "application/json" } }, 40 + ), 41 + ), 42 + ); 43 + 44 + const res = await app.request("/v1/monitor/1/run", { 45 + method: "POST", 46 + headers: { 47 + "x-openstatus-key": "1", 48 + "content-type": "application/json", 49 + }, 50 + }); 51 + 52 + expect(res.status).toBe(200); 53 + 54 + const json = await res.json(); 55 + const result = TriggerResult.array().safeParse(json); 56 + expect(result.success).toBe(true); 57 + }); 58 + 59 + test("run monitor with no-wait parameter should return empty array", async () => { 60 + const res = await app.request("/v1/monitor/1/run?no-wait=true", { 61 + method: "POST", 62 + headers: { 63 + "x-openstatus-key": "1", 64 + "content-type": "application/json", 65 + }, 66 + }); 67 + 68 + expect(res.status).toBe(200); 69 + 70 + const json = await res.json(); 71 + expect(json).toEqual([]); 72 + }); 73 + 74 + test("run monitor with invalid id should return 404", async () => { 75 + const res = await app.request("/v1/monitor/999999/run", { 76 + method: "POST", 77 + headers: { 78 + "x-openstatus-key": "1", 79 + "content-type": "application/json", 80 + }, 81 + }); 82 + 83 + expect(res.status).toBe(404); 84 + }); 85 + 86 + test("run monitor without auth key should return 401", async () => { 87 + const res = await app.request("/v1/monitor/1/run", { 88 + method: "POST", 89 + headers: { 90 + "content-type": "application/json", 91 + }, 92 + }); 93 + 94 + expect(res.status).toBe(401); 95 + }); 96 + 97 + test("run monitor from different workspace should return 404", async () => { 98 + const res = await app.request("/v1/monitor/55555/run", { 99 + method: "POST", 100 + headers: { 101 + "x-openstatus-key": "1", 102 + "content-type": "application/json", 103 + }, 104 + }); 105 + 106 + expect(res.status).toBe(404); 107 + }); 108 + 109 + test("run TCP monitor with valid id should return 200", async () => { 110 + mockFetch.mockReturnValue( 111 + Promise.resolve( 112 + new Response( 113 + JSON.stringify({ 114 + jobType: "tcp", 115 + latency: 50, 116 + region: "ams", 117 + timestamp: 1234567890, 118 + timing: { 119 + tcpStart: 1, 120 + tcpDone: 2, 121 + }, 122 + }), 123 + { status: 200, headers: { "content-type": "application/json" } }, 124 + ), 125 + ), 126 + ); 127 + 128 + const res = await app.request("/v1/monitor/4/run", { 129 + method: "POST", 130 + headers: { 131 + "x-openstatus-key": "1", 132 + "content-type": "application/json", 133 + }, 134 + }); 135 + 136 + expect(res.status).toBe(200); 137 + 138 + const json = await res.json(); 139 + const result = TriggerResult.array().safeParse(json); 140 + expect(result.success).toBe(true); 141 + if (result.success && result.data[0]) { 142 + expect(result.data[0].jobType).toBe("tcp"); 143 + } 144 + }); 145 + 146 + test.todo( 147 + "run monitor with multiple regions should return array of results", 148 + async () => { 149 + mockFetch.mockReturnValue( 150 + Promise.resolve( 151 + new Response( 152 + JSON.stringify({ 153 + jobType: "http", 154 + status: 200, 155 + latency: 100, 156 + region: "ams", 157 + timestamp: 1234567890, 158 + timing: { 159 + dnsStart: 1, 160 + dnsDone: 2, 161 + connectStart: 3, 162 + connectDone: 4, 163 + tlsHandshakeStart: 5, 164 + tlsHandshakeDone: 6, 165 + firstByteStart: 7, 166 + firstByteDone: 8, 167 + transferStart: 9, 168 + transferDone: 10, 169 + }, 170 + }), 171 + { status: 200, headers: { "content-type": "application/json" } }, 172 + ), 173 + ), 174 + ); 175 + 176 + const res = await app.request("/v1/monitor/5/run", { 177 + method: "POST", 178 + headers: { 179 + "x-openstatus-key": "1", 180 + "content-type": "application/json", 181 + }, 182 + }); 183 + 184 + expect(res.status).toBe(200); 185 + 186 + const json = await res.json(); 187 + expect(Array.isArray(json)).toBe(true); 188 + }, 189 + );
+28 -29
apps/server/src/routes/v1/monitors/schema.ts
··· 375 375 regions: z 376 376 .preprocess( 377 377 (val) => { 378 - let regions: Array<unknown> = []; 379 - if (!val) return regions; 380 - 378 + let parsedRegions: Array<unknown> = []; 379 + if (!val) return parsedRegions; 381 380 if (Array.isArray(val)) { 382 - regions = val; 381 + parsedRegions = val; 383 382 } 384 383 if (String(val).length > 0) { 385 - regions = String(val).split(","); 386 - } 387 - 388 - const deprecatedRegions = regions.filter((r) => { 389 - return !AVAILABLE_REGIONS.includes( 390 - r as (typeof AVAILABLE_REGIONS)[number], 391 - ); 392 - }); 393 - 394 - if (deprecatedRegions.length > 0) { 395 - throw new ZodError([ 396 - { 397 - code: "custom", 398 - path: ["regions"], 399 - message: `Deprecated regions are not allowed: ${deprecatedRegions.join( 400 - ", ", 401 - )}`, 402 - }, 403 - ]); 384 + parsedRegions = String(val).split(","); 404 385 } 405 - 406 - return regions; 386 + return parsedRegions; 407 387 }, 408 388 z.array(z.enum(monitorRegions)), 409 389 ) 390 + .superRefine((regions, ctx) => { 391 + const deprecatedRegions = regions.filter((r) => { 392 + return !AVAILABLE_REGIONS.includes( 393 + r as (typeof AVAILABLE_REGIONS)[number], 394 + ); 395 + }); 396 + if (deprecatedRegions.length > 0) { 397 + ctx.addIssue({ 398 + code: "custom", 399 + path: ["regions"], 400 + message: `Deprecated regions are not allowed: ${deprecatedRegions.join( 401 + ", ", 402 + )}`, 403 + }); 404 + } 405 + }) 410 406 .prefault([]) 411 407 .openapi({ 412 408 example: ["ams"], ··· 455 451 }); 456 452 457 453 const tcpRequestSchema = z.object({ 458 - host: z.string().openapi({ 459 - examples: ["example.com", "localhost"], 460 - description: "Host to connect to", 461 - }), 454 + host: z 455 + .string() 456 + .min(1) 457 + .openapi({ 458 + examples: ["example.com", "localhost"], 459 + description: "Host to connect to", 460 + }), 462 461 port: z.number().openapi({ 463 462 description: "Port to connect to", 464 463 examples: [80, 443, 1337],
+185
apps/server/src/routes/v1/monitors/trigger/post.test.ts
··· 1 + import { expect, test } from "bun:test"; 2 + 3 + import { afterEach, mock } from "bun:test"; 4 + import { app } from "@/index"; 5 + import { TriggerSchema } from "./schema"; 6 + 7 + const mockFetch = mock(); 8 + 9 + global.fetch = mockFetch as unknown as typeof fetch; 10 + mock.module("node-fetch", () => mockFetch); 11 + 12 + afterEach(() => { 13 + mockFetch.mockReset(); 14 + }); 15 + 16 + test("trigger monitor with valid id should return 200", async () => { 17 + mockFetch.mockReturnValue( 18 + Promise.resolve( 19 + new Response( 20 + JSON.stringify({ 21 + jobType: "http", 22 + status: 200, 23 + latency: 100, 24 + region: "ams", 25 + timestamp: 1234567890, 26 + timing: { 27 + dnsStart: 1, 28 + dnsDone: 2, 29 + connectStart: 3, 30 + connectDone: 4, 31 + tlsHandshakeStart: 5, 32 + tlsHandshakeDone: 6, 33 + firstByteStart: 7, 34 + firstByteDone: 8, 35 + transferStart: 9, 36 + transferDone: 10, 37 + }, 38 + }), 39 + { status: 200, headers: { "content-type": "application/json" } }, 40 + ), 41 + ), 42 + ); 43 + 44 + const res = await app.request("/v1/monitor/1/trigger", { 45 + method: "POST", 46 + headers: { 47 + "x-openstatus-key": "1", 48 + "content-type": "application/json", 49 + }, 50 + }); 51 + 52 + expect(res.status).toBe(200); 53 + 54 + const json = await res.json(); 55 + const result = TriggerSchema.safeParse(json); 56 + expect(result.success).toBe(true); 57 + expect(json.resultId).toBeDefined(); 58 + expect(typeof json.resultId).toBe("number"); 59 + }); 60 + 61 + test("trigger monitor with invalid id should return 404", async () => { 62 + const res = await app.request("/v1/monitor/999999/trigger", { 63 + method: "POST", 64 + headers: { 65 + "x-openstatus-key": "1", 66 + "content-type": "application/json", 67 + }, 68 + }); 69 + 70 + expect(res.status).toBe(404); 71 + }); 72 + 73 + test("trigger monitor without auth key should return 401", async () => { 74 + const res = await app.request("/v1/monitor/1/trigger", { 75 + method: "POST", 76 + headers: { 77 + "content-type": "application/json", 78 + }, 79 + }); 80 + 81 + expect(res.status).toBe(401); 82 + }); 83 + 84 + test("trigger monitor from different workspace should return 404", async () => { 85 + const res = await app.request("/v1/monitor/99/trigger", { 86 + method: "POST", 87 + headers: { 88 + "x-openstatus-key": "1", 89 + "content-type": "application/json", 90 + }, 91 + }); 92 + expect(res.status).toBe(404); 93 + }); 94 + 95 + test("trigger deleted monitor should return 404", async () => { 96 + const res = await app.request("/v1/monitor/3/trigger", { 97 + method: "POST", 98 + headers: { 99 + "x-openstatus-key": "1", 100 + "content-type": "application/json", 101 + }, 102 + }); 103 + 104 + expect(res.status).toBe(404); 105 + }); 106 + 107 + test("trigger TCP monitor with valid id should return 200", async () => { 108 + mockFetch.mockReturnValue( 109 + Promise.resolve( 110 + new Response( 111 + JSON.stringify({ 112 + jobType: "tcp", 113 + latency: 50, 114 + region: "ams", 115 + timestamp: 1234567890, 116 + timing: { 117 + tcpStart: 1, 118 + tcpDone: 2, 119 + }, 120 + }), 121 + { status: 200, headers: { "content-type": "application/json" } }, 122 + ), 123 + ), 124 + ); 125 + 126 + const res = await app.request("/v1/monitor/4/trigger", { 127 + method: "POST", 128 + headers: { 129 + "x-openstatus-key": "1", 130 + "content-type": "application/json", 131 + }, 132 + }); 133 + 134 + expect(res.status).toBe(200); 135 + 136 + const json = await res.json(); 137 + const result = TriggerSchema.safeParse(json); 138 + expect(result.success).toBe(true); 139 + expect(json.resultId).toBeDefined(); 140 + expect(typeof json.resultId).toBe("number"); 141 + }); 142 + 143 + test("trigger monitor with multiple regions should return result id", async () => { 144 + mockFetch.mockReturnValue( 145 + Promise.resolve( 146 + new Response( 147 + JSON.stringify({ 148 + jobType: "http", 149 + status: 200, 150 + latency: 100, 151 + region: "ams", 152 + timestamp: 1234567890, 153 + timing: { 154 + dnsStart: 1, 155 + dnsDone: 2, 156 + connectStart: 3, 157 + connectDone: 4, 158 + tlsHandshakeStart: 5, 159 + tlsHandshakeDone: 6, 160 + firstByteStart: 7, 161 + firstByteDone: 8, 162 + transferStart: 9, 163 + transferDone: 10, 164 + }, 165 + }), 166 + { status: 200, headers: { "content-type": "application/json" } }, 167 + ), 168 + ), 169 + ); 170 + 171 + const res = await app.request("/v1/monitor/1/trigger", { 172 + method: "POST", 173 + headers: { 174 + "x-openstatus-key": "1", 175 + "content-type": "application/json", 176 + }, 177 + }); 178 + 179 + expect(res.status).toBe(200); 180 + 181 + const json = await res.json(); 182 + const result = TriggerSchema.safeParse(json); 183 + expect(result.success).toBe(true); 184 + expect(json.resultId).toBeDefined(); 185 + });
+215
apps/server/src/routes/v1/statusReportUpdates/post.test.ts
··· 50 50 51 51 expect(res.status).toBe(401); 52 52 }); 53 + 54 + test("create status report update with identified status should return 200", async () => { 55 + const res = await app.request("/v1/status_report_update", { 56 + method: "POST", 57 + headers: { 58 + "x-openstatus-key": "1", 59 + "content-type": "application/json", 60 + }, 61 + body: JSON.stringify({ 62 + status: "identified", 63 + date: new Date().toISOString(), 64 + message: "We have identified the root cause", 65 + statusReportId: 1, 66 + }), 67 + }); 68 + 69 + const result = StatusReportUpdateSchema.safeParse(await res.json()); 70 + expect(res.status).toBe(200); 71 + expect(result.success).toBe(true); 72 + }); 73 + 74 + test("create status report update with monitoring status should return 200", async () => { 75 + const res = await app.request("/v1/status_report_update", { 76 + method: "POST", 77 + headers: { 78 + "x-openstatus-key": "1", 79 + "content-type": "application/json", 80 + }, 81 + body: JSON.stringify({ 82 + status: "monitoring", 83 + date: new Date().toISOString(), 84 + message: "The fix has been deployed and we are monitoring", 85 + statusReportId: 1, 86 + }), 87 + }); 88 + 89 + const result = StatusReportUpdateSchema.safeParse(await res.json()); 90 + expect(res.status).toBe(200); 91 + expect(result.success).toBe(true); 92 + }); 93 + 94 + test("create status report update with resolved status should return 200", async () => { 95 + const res = await app.request("/v1/status_report_update", { 96 + method: "POST", 97 + headers: { 98 + "x-openstatus-key": "1", 99 + "content-type": "application/json", 100 + }, 101 + body: JSON.stringify({ 102 + status: "resolved", 103 + date: new Date().toISOString(), 104 + message: "Issue has been fully resolved", 105 + statusReportId: 1, 106 + }), 107 + }); 108 + 109 + const result = StatusReportUpdateSchema.safeParse(await res.json()); 110 + expect(res.status).toBe(200); 111 + expect(result.success).toBe(true); 112 + }); 113 + 114 + test("create status report update without date should use default", async () => { 115 + const res = await app.request("/v1/status_report_update", { 116 + method: "POST", 117 + headers: { 118 + "x-openstatus-key": "1", 119 + "content-type": "application/json", 120 + }, 121 + body: JSON.stringify({ 122 + status: "investigating", 123 + message: "Update without explicit date", 124 + statusReportId: 1, 125 + }), 126 + }); 127 + 128 + const result = StatusReportUpdateSchema.safeParse(await res.json()); 129 + expect(res.status).toBe(200); 130 + expect(result.success).toBe(true); 131 + }); 132 + 133 + test("create status report update with past date should return 200", async () => { 134 + const pastDate = new Date(); 135 + pastDate.setTime(pastDate.getTime() - 24 * 60 * 60 * 1000); 136 + 137 + const res = await app.request("/v1/status_report_update", { 138 + method: "POST", 139 + headers: { 140 + "x-openstatus-key": "1", 141 + "content-type": "application/json", 142 + }, 143 + body: JSON.stringify({ 144 + status: "investigating", 145 + date: pastDate.toISOString(), 146 + message: "Update with past date", 147 + statusReportId: 1, 148 + }), 149 + }); 150 + 151 + const result = StatusReportUpdateSchema.safeParse(await res.json()); 152 + expect(res.status).toBe(200); 153 + expect(result.success).toBe(true); 154 + }); 155 + 156 + test("create status report update with long message should return 200", async () => { 157 + const longMessage = 158 + "This is a very detailed status update message that provides comprehensive information about the incident, including what happened, what is being done to resolve it, and what measures are being taken to prevent similar issues in the future. We apologize for any inconvenience this may have caused."; 159 + 160 + const res = await app.request("/v1/status_report_update", { 161 + method: "POST", 162 + headers: { 163 + "x-openstatus-key": "1", 164 + "content-type": "application/json", 165 + }, 166 + body: JSON.stringify({ 167 + status: "monitoring", 168 + date: new Date().toISOString(), 169 + message: longMessage, 170 + statusReportId: 1, 171 + }), 172 + }); 173 + 174 + const result = StatusReportUpdateSchema.safeParse(await res.json()); 175 + expect(res.status).toBe(200); 176 + expect(result.success).toBe(true); 177 + }); 178 + 179 + test("create status report update with different status report ID should return 200", async () => { 180 + const res = await app.request("/v1/status_report_update", { 181 + method: "POST", 182 + headers: { 183 + "x-openstatus-key": "1", 184 + "content-type": "application/json", 185 + }, 186 + body: JSON.stringify({ 187 + status: "investigating", 188 + date: new Date().toISOString(), 189 + message: "Update for different report", 190 + statusReportId: 2, 191 + }), 192 + }); 193 + 194 + const result = StatusReportUpdateSchema.safeParse(await res.json()); 195 + expect(res.status).toBe(200); 196 + expect(result.success).toBe(true); 197 + }); 198 + 199 + test("create status report update with invalid status should return 400", async () => { 200 + const res = await app.request("/v1/status_report_update", { 201 + method: "POST", 202 + headers: { 203 + "x-openstatus-key": "1", 204 + "content-type": "application/json", 205 + }, 206 + body: JSON.stringify({ 207 + status: "invalid_status", 208 + date: new Date().toISOString(), 209 + message: "Test message", 210 + statusReportId: 1, 211 + }), 212 + }); 213 + 214 + expect(res.status).toBe(400); 215 + }); 216 + 217 + test("create status report update without message should return 400", async () => { 218 + const res = await app.request("/v1/status_report_update", { 219 + method: "POST", 220 + headers: { 221 + "x-openstatus-key": "1", 222 + "content-type": "application/json", 223 + }, 224 + body: JSON.stringify({ 225 + status: "investigating", 226 + date: new Date().toISOString(), 227 + statusReportId: 1, 228 + }), 229 + }); 230 + 231 + expect(res.status).toBe(400); 232 + }); 233 + 234 + test("create status report update without statusReportId should return 400", async () => { 235 + const res = await app.request("/v1/status_report_update", { 236 + method: "POST", 237 + headers: { 238 + "x-openstatus-key": "1", 239 + "content-type": "application/json", 240 + }, 241 + body: JSON.stringify({ 242 + status: "investigating", 243 + date: new Date().toISOString(), 244 + message: "Test message", 245 + }), 246 + }); 247 + 248 + expect(res.status).toBe(400); 249 + }); 250 + 251 + test("create status report update with empty message should return 400", async () => { 252 + const res = await app.request("/v1/status_report_update", { 253 + method: "POST", 254 + headers: { 255 + "x-openstatus-key": "1", 256 + "content-type": "application/json", 257 + }, 258 + body: JSON.stringify({ 259 + status: "investigating", 260 + date: new Date().toISOString(), 261 + message: "", 262 + statusReportId: 1, 263 + }), 264 + }); 265 + 266 + expect(res.status).toBe(400); 267 + });
+1 -1
apps/server/src/routes/v1/statusReportUpdates/schema.ts
··· 25 25 date: z.coerce.date().prefault(new Date()).openapi({ 26 26 description: "The date of the update in ISO8601 format", 27 27 }), 28 - message: z.string().openapi({ 28 + message: z.string().min(1).openapi({ 29 29 description: "The message of the update", 30 30 }), 31 31 statusReportId: z.number().openapi({
+194
apps/server/src/routes/v1/statusReports/update/post.test.ts
··· 1 + import { expect, test } from "bun:test"; 2 + 3 + import { app } from "@/index"; 4 + import { StatusReportSchema } from "../schema"; 5 + 6 + test("create status report update with valid data should return 200", async () => { 7 + const date = new Date(); 8 + date.setMilliseconds(0); 9 + 10 + const res = await app.request("/v1/status_report/1/update", { 11 + method: "POST", 12 + headers: { 13 + "x-openstatus-key": "1", 14 + "content-type": "application/json", 15 + }, 16 + body: JSON.stringify({ 17 + status: "monitoring", 18 + message: "The issue has been resolved and we are monitoring", 19 + date: date.toISOString(), 20 + }), 21 + }); 22 + 23 + expect(res.status).toBe(200); 24 + 25 + const json = await res.json(); 26 + const result = StatusReportSchema.safeParse(json); 27 + expect(result.success).toBe(true); 28 + }); 29 + 30 + test("create status report update with different status should return 200", async () => { 31 + const date = new Date(); 32 + date.setMilliseconds(0); 33 + 34 + const res = await app.request("/v1/status_report/1/update", { 35 + method: "POST", 36 + headers: { 37 + "x-openstatus-key": "1", 38 + "content-type": "application/json", 39 + }, 40 + body: JSON.stringify({ 41 + status: "identified", 42 + message: "We have identified the issue", 43 + date: date.toISOString(), 44 + }), 45 + }); 46 + 47 + expect(res.status).toBe(200); 48 + 49 + const json = await res.json(); 50 + const result = StatusReportSchema.safeParse(json); 51 + expect(result.success).toBe(true); 52 + }); 53 + 54 + test("create status report update with invalid status report id should return 404", async () => { 55 + const date = new Date(); 56 + date.setMilliseconds(0); 57 + 58 + const res = await app.request("/v1/status_report/999999/update", { 59 + method: "POST", 60 + headers: { 61 + "x-openstatus-key": "1", 62 + "content-type": "application/json", 63 + }, 64 + body: JSON.stringify({ 65 + status: "monitoring", 66 + message: "The issue has been resolved and we are monitoring", 67 + date: date.toISOString(), 68 + }), 69 + }); 70 + 71 + expect(res.status).toBe(404); 72 + }); 73 + 74 + test("create status report update with invalid status should return 400", async () => { 75 + const date = new Date(); 76 + date.setMilliseconds(0); 77 + 78 + const res = await app.request("/v1/status_report/1/update", { 79 + method: "POST", 80 + headers: { 81 + "x-openstatus-key": "1", 82 + "content-type": "application/json", 83 + }, 84 + body: JSON.stringify({ 85 + status: "invalid_status", 86 + message: "Test message", 87 + date: date.toISOString(), 88 + }), 89 + }); 90 + 91 + expect(res.status).toBe(400); 92 + }); 93 + 94 + test("create status report update without auth key should return 401", async () => { 95 + const date = new Date(); 96 + date.setMilliseconds(0); 97 + 98 + const res = await app.request("/v1/status_report/1/update", { 99 + method: "POST", 100 + headers: { 101 + "content-type": "application/json", 102 + }, 103 + body: JSON.stringify({ 104 + status: "monitoring", 105 + message: "Test message", 106 + date: date.toISOString(), 107 + }), 108 + }); 109 + 110 + expect(res.status).toBe(401); 111 + }); 112 + 113 + test("create status report update from different workspace should return 404", async () => { 114 + const date = new Date(); 115 + date.setMilliseconds(0); 116 + 117 + const res = await app.request("/v1/status_report/1/update", { 118 + method: "POST", 119 + headers: { 120 + "x-openstatus-key": "2", 121 + "content-type": "application/json", 122 + }, 123 + body: JSON.stringify({ 124 + status: "monitoring", 125 + message: "Test message", 126 + date: date.toISOString(), 127 + }), 128 + }); 129 + 130 + expect(res.status).toBe(404); 131 + }); 132 + 133 + test("create status report update without message should return 400", async () => { 134 + const date = new Date(); 135 + date.setMilliseconds(0); 136 + 137 + const res = await app.request("/v1/status_report/1/update", { 138 + method: "POST", 139 + headers: { 140 + "x-openstatus-key": "1", 141 + "content-type": "application/json", 142 + }, 143 + body: JSON.stringify({ 144 + status: "monitoring", 145 + date: date.toISOString(), 146 + }), 147 + }); 148 + 149 + expect(res.status).toBe(400); 150 + }); 151 + 152 + test("create status report update with resolved status should return 200", async () => { 153 + const date = new Date(); 154 + date.setMilliseconds(0); 155 + 156 + const res = await app.request("/v1/status_report/1/update", { 157 + method: "POST", 158 + headers: { 159 + "x-openstatus-key": "1", 160 + "content-type": "application/json", 161 + }, 162 + body: JSON.stringify({ 163 + status: "resolved", 164 + message: "Issue has been fully resolved", 165 + date: date.toISOString(), 166 + }), 167 + }); 168 + 169 + expect(res.status).toBe(200); 170 + 171 + const json = await res.json(); 172 + const result = StatusReportSchema.safeParse(json); 173 + expect(result.success).toBe(true); 174 + }); 175 + 176 + test("create status report update without date should use default", async () => { 177 + const res = await app.request("/v1/status_report/1/update", { 178 + method: "POST", 179 + headers: { 180 + "x-openstatus-key": "1", 181 + "content-type": "application/json", 182 + }, 183 + body: JSON.stringify({ 184 + status: "monitoring", 185 + message: "Test message without explicit date", 186 + }), 187 + }); 188 + 189 + expect(res.status).toBe(200); 190 + 191 + const json = await res.json(); 192 + const result = StatusReportSchema.safeParse(json); 193 + expect(result.success).toBe(true); 194 + });
+30 -11
apps/server/src/routes/v1/statusReports/update/post.ts
··· 28 28 description: "the status report update", 29 29 content: { 30 30 "application/json": { 31 - schema: StatusReportUpdateSchema.omit({ id: true }), 31 + schema: StatusReportUpdateSchema.omit({ 32 + id: true, 33 + statusReportId: true, 34 + }), 32 35 }, 33 36 }, 34 37 }, ··· 55 58 56 59 const _statusReport = await db 57 60 .update(statusReport) 58 - .set({ status: input.status }) 61 + .set({ status: input.status, updatedAt: new Date() }) 59 62 .where( 60 63 and( 61 64 eq(statusReport.id, Number(id)), ··· 83 86 .returning() 84 87 .get(); 85 88 86 - await db 87 - .update(statusReport) 88 - .set({ 89 - status: input.status, 90 - updatedAt: new Date(), 91 - }) 92 - .where(eq(statusReport.id, _statusReport.id)); 93 - 94 89 if (limits.notifications && _statusReport.pageId) { 95 90 const _statusReportWithRelations = await db.query.statusReport.findFirst({ 96 91 where: eq(statusReport.id, Number(id)), ··· 130 125 } 131 126 } 132 127 133 - const data = StatusReportSchema.parse(_statusReportUpdate); 128 + // Query the full status report with all its relationships 129 + const fullStatusReport = await db.query.statusReport.findFirst({ 130 + where: eq(statusReport.id, Number(id)), 131 + with: { 132 + statusReportUpdates: true, 133 + monitorsToStatusReports: true, 134 + }, 135 + }); 136 + 137 + if (!fullStatusReport) { 138 + throw new OpenStatusApiError({ 139 + code: "NOT_FOUND", 140 + message: `Status Report ${id} not found`, 141 + }); 142 + } 143 + 144 + const data = StatusReportSchema.parse({ 145 + ...fullStatusReport, 146 + statusReportUpdateIds: fullStatusReport.statusReportUpdates.map( 147 + (u) => u.id, 148 + ), 149 + monitorIds: fullStatusReport.monitorsToStatusReports.map( 150 + (m) => m.monitorId, 151 + ), 152 + }); 134 153 135 154 return c.json(data, 200); 136 155 });
+6 -1
apps/server/src/routes/v1/utils.ts
··· 9 9 return new Date().toISOString(); 10 10 } catch (_e) { 11 11 throw new ZodError([ 12 - { code: "invalid_date", message: "Invalid date", path: [] }, 12 + { 13 + code: "invalid_type", 14 + message: "Invalid date", 15 + expected: "string", 16 + path: [], 17 + }, 13 18 ]); 14 19 } 15 20 }, z.string());
+25 -14
pnpm-lock.yaml
··· 448 448 dependencies: 449 449 '@hono/sentry': 450 450 specifier: 1.2.2 451 - version: 1.2.2(hono@4.5.3) 451 + version: 1.2.2(hono@4.11.3) 452 452 '@hono/zod-openapi': 453 453 specifier: 1.1.5 454 - version: 1.1.5(hono@4.5.3)(zod@4.1.13) 454 + version: 1.1.5(hono@4.11.3)(zod@4.1.13) 455 455 '@hono/zod-validator': 456 - specifier: 0.2.2 457 - version: 0.2.2(hono@4.5.3)(zod@4.1.13) 456 + specifier: 0.7.6 457 + version: 0.7.6(hono@4.11.3)(zod@4.1.13) 458 458 '@logtape/logtape': 459 459 specifier: 1.1.2 460 460 version: 1.1.2 ··· 493 493 version: link:../../packages/utils 494 494 '@scalar/hono-api-reference': 495 495 specifier: 0.8.5 496 - version: 0.8.5(hono@4.5.3) 496 + version: 0.8.5(hono@4.11.3) 497 497 '@t3-oss/env-core': 498 498 specifier: 0.13.10 499 499 version: 0.13.10(typescript@5.9.3)(zod@4.1.13) ··· 504 504 specifier: 2.6.2 505 505 version: 2.6.2 506 506 hono: 507 - specifier: 4.5.3 508 - version: 4.5.3 507 + specifier: 4.11.3 508 + version: 4.11.3 509 509 nanoid: 510 510 specifier: 5.0.7 511 511 version: 5.0.7 ··· 8205 8205 hoist-non-react-statics@3.3.2: 8206 8206 resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} 8207 8207 8208 + hono@4.11.3: 8209 + resolution: {integrity: sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==} 8210 + engines: {node: '>=16.9.0'} 8211 + 8208 8212 hono@4.5.3: 8209 8213 resolution: {integrity: sha512-r26WwwbKD3BAYdfB294knNnegNda7VfV1tVn66D9Kvl9WQTdrR+5eKdoeaQNHQcC3Gr0KBikzAtjd6VsRGVSaw==} 8210 8214 engines: {node: '>=16.0.0'} ··· 12798 12802 protobufjs: 7.5.4 12799 12803 yargs: 17.7.2 12800 12804 12805 + '@hono/sentry@1.2.2(hono@4.11.3)': 12806 + dependencies: 12807 + hono: 4.11.3 12808 + toucan-js: 4.1.1 12809 + 12801 12810 '@hono/sentry@1.2.2(hono@4.5.3)': 12802 12811 dependencies: 12803 12812 hono: 4.5.3 12804 12813 toucan-js: 4.1.1 12805 12814 12806 - '@hono/zod-openapi@1.1.5(hono@4.5.3)(zod@4.1.13)': 12815 + '@hono/zod-openapi@1.1.5(hono@4.11.3)(zod@4.1.13)': 12807 12816 dependencies: 12808 12817 '@asteasolutions/zod-to-openapi': 8.2.0(zod@4.1.13) 12809 - '@hono/zod-validator': 0.7.6(hono@4.5.3)(zod@4.1.13) 12810 - hono: 4.5.3 12818 + '@hono/zod-validator': 0.7.6(hono@4.11.3)(zod@4.1.13) 12819 + hono: 4.11.3 12811 12820 openapi3-ts: 4.5.0 12812 12821 zod: 4.1.13 12813 12822 ··· 12816 12825 hono: 4.5.3 12817 12826 zod: 4.1.13 12818 12827 12819 - '@hono/zod-validator@0.7.6(hono@4.5.3)(zod@4.1.13)': 12828 + '@hono/zod-validator@0.7.6(hono@4.11.3)(zod@4.1.13)': 12820 12829 dependencies: 12821 - hono: 4.5.3 12830 + hono: 4.11.3 12822 12831 zod: 4.1.13 12823 12832 12824 12833 '@hookform/devtools@4.4.0(@types/react@19.2.2)(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': ··· 15142 15151 dependencies: 15143 15152 '@scalar/types': 0.1.11 15144 15153 15145 - '@scalar/hono-api-reference@0.8.5(hono@4.5.3)': 15154 + '@scalar/hono-api-reference@0.8.5(hono@4.11.3)': 15146 15155 dependencies: 15147 15156 '@scalar/core': 0.2.11 15148 - hono: 4.5.3 15157 + hono: 4.11.3 15149 15158 15150 15159 '@scalar/openapi-types@0.2.1': 15151 15160 dependencies: ··· 18262 18271 hoist-non-react-statics@3.3.2: 18263 18272 dependencies: 18264 18273 react-is: 16.13.1 18274 + 18275 + hono@4.11.3: {} 18265 18276 18266 18277 hono@4.5.3: {} 18267 18278