Skip to content

Commit 86c2ab1

Browse files
committed
test: add unit and property tests for transaction aliases
Add comprehensive test coverage for the transaction alias system: - test/lib/transaction-alias.property.test.ts: Property-based tests using fast-check to verify extractTransactionSegment and buildTransactionAliases invariants - test/lib/db/transaction-aliases.test.ts: Unit tests for SQLite storage layer including fingerprint building, CRUD operations, and stale detection - test/lib/resolve-transaction.test.ts: Unit tests for transaction resolution including index/alias lookup, full name pass-through, and stale alias error handling
1 parent ce939e1 commit 86c2ab1

File tree

3 files changed

+1027
-0
lines changed

3 files changed

+1027
-0
lines changed
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/**
2+
* Transaction Aliases Database Layer Tests
3+
*
4+
* Tests for SQLite storage of transaction aliases from profile list commands.
5+
*/
6+
7+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
8+
import {
9+
buildTransactionFingerprint,
10+
clearTransactionAliases,
11+
getStaleFingerprint,
12+
getStaleIndexFingerprint,
13+
getTransactionAliases,
14+
getTransactionByAlias,
15+
getTransactionByIndex,
16+
setTransactionAliases,
17+
} from "../../../src/lib/db/transaction-aliases.js";
18+
import type { TransactionAliasEntry } from "../../../src/types/index.js";
19+
import { cleanupTestDir, createTestConfigDir } from "../../helpers.js";
20+
21+
let testConfigDir: string;
22+
23+
beforeEach(async () => {
24+
testConfigDir = await createTestConfigDir("test-transaction-aliases-");
25+
process.env.SENTRY_CLI_CONFIG_DIR = testConfigDir;
26+
});
27+
28+
afterEach(async () => {
29+
delete process.env.SENTRY_CLI_CONFIG_DIR;
30+
await cleanupTestDir(testConfigDir);
31+
});
32+
33+
// =============================================================================
34+
// buildTransactionFingerprint
35+
// =============================================================================
36+
37+
describe("buildTransactionFingerprint", () => {
38+
test("builds fingerprint with org, project, and period", () => {
39+
const fp = buildTransactionFingerprint("my-org", "my-project", "7d");
40+
expect(fp).toBe("my-org:my-project:7d");
41+
});
42+
43+
test("uses * for null project (multi-project)", () => {
44+
const fp = buildTransactionFingerprint("my-org", null, "24h");
45+
expect(fp).toBe("my-org:*:24h");
46+
});
47+
48+
test("handles various period formats", () => {
49+
expect(buildTransactionFingerprint("o", "p", "1h")).toBe("o:p:1h");
50+
expect(buildTransactionFingerprint("o", "p", "24h")).toBe("o:p:24h");
51+
expect(buildTransactionFingerprint("o", "p", "7d")).toBe("o:p:7d");
52+
expect(buildTransactionFingerprint("o", "p", "30d")).toBe("o:p:30d");
53+
});
54+
});
55+
56+
// =============================================================================
57+
// setTransactionAliases / getTransactionAliases
58+
// =============================================================================
59+
60+
describe("setTransactionAliases", () => {
61+
const fingerprint = "test-org:test-project:7d";
62+
63+
const createEntry = (idx: number, alias: string): TransactionAliasEntry => ({
64+
idx,
65+
alias,
66+
transaction: `/api/0/${alias}/`,
67+
orgSlug: "test-org",
68+
projectSlug: "test-project",
69+
});
70+
71+
test("stores and retrieves aliases", () => {
72+
const aliases: TransactionAliasEntry[] = [
73+
createEntry(1, "issues"),
74+
createEntry(2, "events"),
75+
createEntry(3, "releases"),
76+
];
77+
78+
setTransactionAliases(aliases, fingerprint);
79+
80+
const result = getTransactionAliases(fingerprint);
81+
expect(result).toHaveLength(3);
82+
expect(result[0]?.alias).toBe("issues");
83+
expect(result[1]?.alias).toBe("events");
84+
expect(result[2]?.alias).toBe("releases");
85+
});
86+
87+
test("replaces existing aliases with same fingerprint", () => {
88+
setTransactionAliases([createEntry(1, "old")], fingerprint);
89+
setTransactionAliases([createEntry(1, "new")], fingerprint);
90+
91+
const result = getTransactionAliases(fingerprint);
92+
expect(result).toHaveLength(1);
93+
expect(result[0]?.alias).toBe("new");
94+
});
95+
96+
test("keeps aliases with different fingerprints separate", () => {
97+
const fp1 = "org1:proj1:7d";
98+
const fp2 = "org2:proj2:7d";
99+
100+
setTransactionAliases([createEntry(1, "first")], fp1);
101+
setTransactionAliases([createEntry(1, "second")], fp2);
102+
103+
const result1 = getTransactionAliases(fp1);
104+
const result2 = getTransactionAliases(fp2);
105+
106+
expect(result1).toHaveLength(1);
107+
expect(result1[0]?.alias).toBe("first");
108+
expect(result2).toHaveLength(1);
109+
expect(result2[0]?.alias).toBe("second");
110+
});
111+
112+
test("stores empty array", () => {
113+
setTransactionAliases([], fingerprint);
114+
115+
const result = getTransactionAliases(fingerprint);
116+
expect(result).toHaveLength(0);
117+
});
118+
119+
test("normalizes aliases to lowercase", () => {
120+
const entry: TransactionAliasEntry = {
121+
idx: 1,
122+
alias: "UPPERCASE",
123+
transaction: "/api/test/",
124+
orgSlug: "org",
125+
projectSlug: "proj",
126+
};
127+
128+
setTransactionAliases([entry], fingerprint);
129+
130+
const result = getTransactionAliases(fingerprint);
131+
expect(result[0]?.alias).toBe("uppercase");
132+
});
133+
});
134+
135+
// =============================================================================
136+
// getTransactionByIndex
137+
// =============================================================================
138+
139+
describe("getTransactionByIndex", () => {
140+
const fingerprint = "test-org:test-project:7d";
141+
142+
beforeEach(() => {
143+
const aliases: TransactionAliasEntry[] = [
144+
{
145+
idx: 1,
146+
alias: "i",
147+
transaction: "/api/0/issues/",
148+
orgSlug: "test-org",
149+
projectSlug: "test-project",
150+
},
151+
{
152+
idx: 2,
153+
alias: "e",
154+
transaction: "/api/0/events/",
155+
orgSlug: "test-org",
156+
projectSlug: "test-project",
157+
},
158+
];
159+
setTransactionAliases(aliases, fingerprint);
160+
});
161+
162+
test("returns entry for valid index", () => {
163+
const result = getTransactionByIndex(1, fingerprint);
164+
expect(result).toBeDefined();
165+
expect(result?.transaction).toBe("/api/0/issues/");
166+
expect(result?.alias).toBe("i");
167+
});
168+
169+
test("returns null for non-existent index", () => {
170+
const result = getTransactionByIndex(99, fingerprint);
171+
expect(result).toBeNull();
172+
});
173+
174+
test("returns null for wrong fingerprint", () => {
175+
const result = getTransactionByIndex(1, "different:fingerprint:7d");
176+
expect(result).toBeNull();
177+
});
178+
179+
test("returns null for index 0", () => {
180+
const result = getTransactionByIndex(0, fingerprint);
181+
expect(result).toBeNull();
182+
});
183+
});
184+
185+
// =============================================================================
186+
// getTransactionByAlias
187+
// =============================================================================
188+
189+
describe("getTransactionByAlias", () => {
190+
const fingerprint = "test-org:test-project:7d";
191+
192+
beforeEach(() => {
193+
const aliases: TransactionAliasEntry[] = [
194+
{
195+
idx: 1,
196+
alias: "issues",
197+
transaction: "/api/0/organizations/{org}/issues/",
198+
orgSlug: "test-org",
199+
projectSlug: "test-project",
200+
},
201+
{
202+
idx: 2,
203+
alias: "events",
204+
transaction: "/api/0/projects/{org}/{proj}/events/",
205+
orgSlug: "test-org",
206+
projectSlug: "test-project",
207+
},
208+
];
209+
setTransactionAliases(aliases, fingerprint);
210+
});
211+
212+
test("returns entry for valid alias", () => {
213+
const result = getTransactionByAlias("issues", fingerprint);
214+
expect(result).toBeDefined();
215+
expect(result?.transaction).toBe("/api/0/organizations/{org}/issues/");
216+
expect(result?.idx).toBe(1);
217+
});
218+
219+
test("returns null for non-existent alias", () => {
220+
const result = getTransactionByAlias("unknown", fingerprint);
221+
expect(result).toBeNull();
222+
});
223+
224+
test("returns null for wrong fingerprint", () => {
225+
const result = getTransactionByAlias("issues", "different:fingerprint:7d");
226+
expect(result).toBeNull();
227+
});
228+
229+
test("alias lookup is case-insensitive", () => {
230+
const lower = getTransactionByAlias("issues", fingerprint);
231+
const upper = getTransactionByAlias("ISSUES", fingerprint);
232+
const mixed = getTransactionByAlias("Issues", fingerprint);
233+
234+
expect(lower?.transaction).toBe(upper?.transaction);
235+
expect(lower?.transaction).toBe(mixed?.transaction);
236+
});
237+
});
238+
239+
// =============================================================================
240+
// getStaleFingerprint / getStaleIndexFingerprint
241+
// =============================================================================
242+
243+
describe("stale detection", () => {
244+
test("getStaleFingerprint returns fingerprint when alias exists elsewhere", () => {
245+
const oldFp = "old-org:old-project:7d";
246+
setTransactionAliases(
247+
[
248+
{
249+
idx: 1,
250+
alias: "issues",
251+
transaction: "/api/issues/",
252+
orgSlug: "old-org",
253+
projectSlug: "old-project",
254+
},
255+
],
256+
oldFp
257+
);
258+
259+
const stale = getStaleFingerprint("issues");
260+
expect(stale).toBe(oldFp);
261+
});
262+
263+
test("getStaleFingerprint returns null when alias doesn't exist", () => {
264+
const stale = getStaleFingerprint("nonexistent");
265+
expect(stale).toBeNull();
266+
});
267+
268+
test("getStaleIndexFingerprint returns fingerprint when index exists elsewhere", () => {
269+
const oldFp = "old-org:old-project:7d";
270+
setTransactionAliases(
271+
[
272+
{
273+
idx: 5,
274+
alias: "test",
275+
transaction: "/api/test/",
276+
orgSlug: "old-org",
277+
projectSlug: "old-project",
278+
},
279+
],
280+
oldFp
281+
);
282+
283+
const stale = getStaleIndexFingerprint(5);
284+
expect(stale).toBe(oldFp);
285+
});
286+
287+
test("getStaleIndexFingerprint returns null when index doesn't exist", () => {
288+
const stale = getStaleIndexFingerprint(999);
289+
expect(stale).toBeNull();
290+
});
291+
});
292+
293+
// =============================================================================
294+
// clearTransactionAliases
295+
// =============================================================================
296+
297+
describe("clearTransactionAliases", () => {
298+
test("removes all transaction aliases", () => {
299+
const fp1 = "org1:proj1:7d";
300+
const fp2 = "org2:proj2:7d";
301+
302+
setTransactionAliases(
303+
[
304+
{
305+
idx: 1,
306+
alias: "a",
307+
transaction: "/a/",
308+
orgSlug: "org1",
309+
projectSlug: "proj1",
310+
},
311+
],
312+
fp1
313+
);
314+
setTransactionAliases(
315+
[
316+
{
317+
idx: 1,
318+
alias: "b",
319+
transaction: "/b/",
320+
orgSlug: "org2",
321+
projectSlug: "proj2",
322+
},
323+
],
324+
fp2
325+
);
326+
327+
clearTransactionAliases();
328+
329+
expect(getTransactionAliases(fp1)).toHaveLength(0);
330+
expect(getTransactionAliases(fp2)).toHaveLength(0);
331+
});
332+
333+
test("safe to call when no aliases exist", () => {
334+
// Should not throw
335+
clearTransactionAliases();
336+
expect(getTransactionAliases("any:fingerprint:7d")).toHaveLength(0);
337+
});
338+
});

0 commit comments

Comments
 (0)