Skip to content

Commit 09bd856

Browse files
committed
test: add schema and events API tests for coverage
- test/commands/schema.test.ts: 10 tests covering schemaCommand.func paths (--all, --search, resource arg, glob patterns, OutputError paths) - test/lib/api/events.test.ts: 6 tests for listIssueEvents covering basic fetch, query options, full flag, limit trimming, date ranges
1 parent 7519b7f commit 09bd856

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

test/commands/schema.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Tests for `sentry schema` command func body.
3+
*
4+
* The schema command browses the in-memory Sentry API schema.
5+
* All queries are synchronous (no network calls), making this easy to test.
6+
*/
7+
8+
import { describe, expect, mock, test } from "bun:test";
9+
import { schemaCommand } from "../../src/commands/schema.js";
10+
import { OutputError } from "../../src/lib/errors.js";
11+
12+
function createMockContext() {
13+
const output: unknown[] = [];
14+
return {
15+
context: {
16+
stdout: {
17+
write: mock((s: string) => {
18+
output.push(s);
19+
}),
20+
},
21+
stderr: {
22+
write: mock((_s: string) => {
23+
/* suppress */
24+
}),
25+
},
26+
cwd: "/tmp",
27+
},
28+
getOutput: () => output.join(""),
29+
output,
30+
};
31+
}
32+
33+
describe("schemaCommand.func", () => {
34+
test("no args shows resource summary", async () => {
35+
const { context, getOutput } = createMockContext();
36+
const func = await schemaCommand.loader();
37+
await func.call(context, { all: false, json: false });
38+
// Should render a resource list
39+
expect(getOutput()).not.toBe("");
40+
});
41+
42+
test("--all shows flat endpoint list", async () => {
43+
const { context, getOutput } = createMockContext();
44+
const func = await schemaCommand.loader();
45+
await func.call(context, { all: true, json: false });
46+
const output = getOutput();
47+
expect(output).not.toBe("");
48+
// All endpoints mode renders more content than resource summary
49+
expect(output.length).toBeGreaterThan(10);
50+
});
51+
52+
test("--search with matches returns endpoints", async () => {
53+
const { context, getOutput } = createMockContext();
54+
const func = await schemaCommand.loader();
55+
// "issues" is a common resource — should match many endpoints
56+
await func.call(context, { all: false, json: false, search: "issues" });
57+
const output = getOutput();
58+
expect(output).not.toBe("");
59+
});
60+
61+
test("--search with no matches throws OutputError", async () => {
62+
const { context } = createMockContext();
63+
const func = await schemaCommand.loader();
64+
const err = await func
65+
.call(context, { all: false, json: false, search: "nonexistent-xyz-abc-never" })
66+
.catch((e: Error) => e);
67+
expect(err).toBeInstanceOf(OutputError);
68+
});
69+
70+
test("resource arg shows endpoints for that resource", async () => {
71+
const { context, getOutput } = createMockContext();
72+
const func = await schemaCommand.loader();
73+
// Pass a known resource name
74+
await func.call(context, { all: false, json: false }, "issues");
75+
expect(getOutput()).not.toBe("");
76+
});
77+
78+
test("glob pattern resource matches resources", async () => {
79+
const { context, getOutput } = createMockContext();
80+
const func = await schemaCommand.loader();
81+
await func.call(context, { all: false, json: false }, "*issue*");
82+
expect(getOutput()).not.toBe("");
83+
});
84+
85+
test("glob pattern resource with no match throws OutputError", async () => {
86+
const { context } = createMockContext();
87+
const func = await schemaCommand.loader();
88+
const err = await func
89+
.call(context, { all: false, json: false }, "*zzz-neverexists-xyz*")
90+
.catch((e: Error) => e);
91+
expect(err).toBeInstanceOf(OutputError);
92+
});
93+
94+
test("resource + operation shows single endpoint", async () => {
95+
const { context, getOutput } = createMockContext();
96+
const func = await schemaCommand.loader();
97+
// Try a resource with a known operation format
98+
// If not found, it falls back to showing resource endpoints
99+
await func.call(context, { all: false, json: false }, "issues", "list");
100+
expect(getOutput()).not.toBe("");
101+
});
102+
103+
test("unknown resource shows resource summary", async () => {
104+
const { context } = createMockContext();
105+
const func = await schemaCommand.loader();
106+
const err = await func
107+
.call(context, { all: false, json: false }, "nonexistent-resource-xyz")
108+
.catch((e: Error) => e);
109+
// Falls through to show all resources via OutputError
110+
expect(err).toBeInstanceOf(OutputError);
111+
});
112+
113+
test("JSON output for --all renders machine-readable", async () => {
114+
const { context, getOutput } = createMockContext();
115+
const func = await schemaCommand.loader();
116+
await func.call(context, { all: true, json: true });
117+
const output = getOutput();
118+
// JSON mode — should be parseable
119+
expect(() => JSON.parse(output)).not.toThrow();
120+
});
121+
});

test/lib/api/events.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* Event API function tests
3+
*
4+
* Tests for listIssueEvents from src/lib/api/events.ts.
5+
* Uses mock fetch to verify API call shapes and pagination behavior.
6+
*/
7+
8+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9+
import { listIssueEvents } from "../../../src/lib/api/events.js";
10+
import { setAuthToken } from "../../../src/lib/db/auth.js";
11+
import { setOrgRegion } from "../../../src/lib/db/regions.js";
12+
import { mockFetch, useTestConfigDir } from "../../helpers.js";
13+
14+
useTestConfigDir("api-events-");
15+
16+
const SAMPLE_EVENT = {
17+
id: "abc123",
18+
eventID: "abc123def456abc123def456abc123de",
19+
title: "TypeError: undefined",
20+
type: "error",
21+
dateCreated: "2025-01-01T00:00:00Z",
22+
} as const;
23+
24+
let originalFetch: typeof globalThis.fetch;
25+
26+
beforeEach(async () => {
27+
originalFetch = globalThis.fetch;
28+
await setAuthToken("test-token");
29+
setOrgRegion("test-org", "https://us.sentry.io");
30+
});
31+
32+
afterEach(() => {
33+
globalThis.fetch = originalFetch;
34+
});
35+
36+
// ---------------------------------------------------------------------------
37+
// listIssueEvents
38+
// ---------------------------------------------------------------------------
39+
40+
describe("listIssueEvents", () => {
41+
test("fetches events for an issue", async () => {
42+
globalThis.fetch = mockFetch(async () =>
43+
new Response(JSON.stringify([SAMPLE_EVENT]), {
44+
status: 200,
45+
headers: { "Content-Type": "application/json" },
46+
})
47+
);
48+
49+
const result = await listIssueEvents("test-org", "123456");
50+
expect(result.data).toHaveLength(1);
51+
expect(result.data[0].eventID).toBe("abc123def456abc123def456abc123de");
52+
});
53+
54+
test("passes query options to API", async () => {
55+
let capturedUrl = "";
56+
globalThis.fetch = mockFetch(async (input, init) => {
57+
const req = new Request(input!, init);
58+
capturedUrl = req.url;
59+
return new Response(JSON.stringify([SAMPLE_EVENT]), {
60+
status: 200,
61+
headers: { "Content-Type": "application/json" },
62+
});
63+
});
64+
65+
await listIssueEvents("test-org", "123456", {
66+
query: "level:error",
67+
statsPeriod: "24h",
68+
});
69+
70+
expect(capturedUrl).toContain("query=level");
71+
expect(capturedUrl).toContain("statsPeriod=24h");
72+
});
73+
74+
test("passes full=true option", async () => {
75+
let capturedUrl = "";
76+
globalThis.fetch = mockFetch(async (input, init) => {
77+
const req = new Request(input!, init);
78+
capturedUrl = req.url;
79+
return new Response(JSON.stringify([SAMPLE_EVENT]), {
80+
status: 200,
81+
headers: { "Content-Type": "application/json" },
82+
});
83+
});
84+
85+
await listIssueEvents("test-org", "123456", { full: true });
86+
87+
expect(capturedUrl).toContain("full=true");
88+
});
89+
90+
test("trims results to limit", async () => {
91+
const manyEvents = Array.from({ length: 30 }, (_, i) => ({
92+
...SAMPLE_EVENT,
93+
id: `event${i}`,
94+
eventID: `${"a".repeat(31)}${i}`.slice(0, 32),
95+
}));
96+
97+
globalThis.fetch = mockFetch(async () =>
98+
new Response(JSON.stringify(manyEvents), {
99+
status: 200,
100+
headers: { "Content-Type": "application/json" },
101+
})
102+
);
103+
104+
const result = await listIssueEvents("test-org", "123456", { limit: 5 });
105+
expect(result.data).toHaveLength(5);
106+
});
107+
108+
test("returns empty array for empty response", async () => {
109+
globalThis.fetch = mockFetch(async () =>
110+
new Response(JSON.stringify([]), {
111+
status: 200,
112+
headers: { "Content-Type": "application/json" },
113+
})
114+
);
115+
116+
const result = await listIssueEvents("test-org", "123456");
117+
expect(result.data).toHaveLength(0);
118+
expect(result.nextCursor).toBeUndefined();
119+
});
120+
121+
test("passes absolute date range options", async () => {
122+
let capturedUrl = "";
123+
globalThis.fetch = mockFetch(async (input, init) => {
124+
const req = new Request(input!, init);
125+
capturedUrl = req.url;
126+
return new Response(JSON.stringify([]), {
127+
status: 200,
128+
headers: { "Content-Type": "application/json" },
129+
});
130+
});
131+
132+
await listIssueEvents("test-org", "123456", {
133+
start: "2025-01-01T00:00:00Z",
134+
end: "2025-01-31T00:00:00Z",
135+
});
136+
137+
expect(capturedUrl).toContain("start=");
138+
expect(capturedUrl).toContain("end=");
139+
});
140+
});

0 commit comments

Comments
 (0)