Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { randomUUID } from 'crypto';
2import {
3 Kysely,
4 type CompiledQuery,
5 type DatabaseConnection,
6 type QueryResult,
7} from 'kysely';
8
9import { type Dependencies } from '../../iocContainer/index.js';
10import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js';
11import { type MockedFn } from '../../test/mockHelpers/jestMocks.js';
12import { safePick } from '../../utils/misc.js';
13import { makeFetchUserActionStatistics } from './fetchUserActionStatistics.js';
14
15describe('fetchUserActionStatistics', () => {
16 let warehouseMock: MockedFn<
17 (it: CompiledQuery) => Promise<QueryResult<unknown>>
18 >;
19 let sut: ReturnType<typeof makeFetchUserActionStatistics>;
20
21 beforeEach(() => {
22 // For these tests, configure the mock to always resolve w/ an empty query
23 // result, since our code doesn't branch on the query result, and our tests
24 // are just asserting what queries were issued.
25 //
26 // This mutation is safe (while we're not running tests concurrently) as
27 // it's local to the test suite. Consider using the `makeTestWithFixture`
28 // helper instead to make a local copy of this state for each test.
29 // eslint-disable-next-line better-mutation/no-mutation
30 warehouseMock = jest
31 .fn<DatabaseConnection['executeQuery']>()
32 .mockResolvedValue({ rows: [] });
33
34 // This mutation is safe (while we're not running tests concurrently) as
35 // it's local to the test suite. Consider using the `makeTestWithFixture`
36 // helper instead to make a local copy of this state for each test.
37 // eslint-disable-next-line better-mutation/no-mutation
38 const kysely = new Kysely({
39 dialect: makeMockWarehouseDialect(warehouseMock),
40 });
41 const dialectMock: Dependencies['DataWarehouseDialect'] = {
42 getKyselyInstance: () => kysely,
43 destroy: jest.fn(async () => {}),
44 };
45
46 // eslint-disable-next-line better-mutation/no-mutation
47 sut = makeFetchUserActionStatistics(dialectMock);
48 });
49
50 test('should generate proper query given user item identifiers', async () => {
51 await sut({ orgId: 'x', userItemIdentifiers: [{ id: '1', typeId: 'a' }] });
52 await sut({
53 orgId: 'x',
54 userItemIdentifiers: [
55 { id: '1', typeId: 'a' },
56 { id: '3', typeId: 'b' },
57 ],
58 });
59 expect(warehouseMock).toHaveBeenCalledTimes(2);
60
61 const queriesRan = warehouseMock.mock.calls.map((it) =>
62 safePick(it[0], ['parameters', 'sql']),
63 );
64
65 expect(queriesRan[0]).toMatchInlineSnapshot(`
66 {
67 "parameters": [
68 "x",
69 "1",
70 "a",
71 ],
72 "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ACTION_ID" as "actionId", "POLICY_ID" as "policyId", "ITEM_SUBMISSION_IDS" as "itemSubmissionIds", "ACTOR_ID" as "actorId", "COUNT" as "count" from "USER_STATISTICS_SERVICE"."LIFETIME_ACTION_STATS" where "ORG_ID" = :1 and ("USER_ID" = :2 and "USER_TYPE_ID" = :3)",
73 }
74 `);
75 expect(queriesRan[1]).toMatchInlineSnapshot(`
76 {
77 "parameters": [
78 "x",
79 "1",
80 "a",
81 "3",
82 "b",
83 ],
84 "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ACTION_ID" as "actionId", "POLICY_ID" as "policyId", "ITEM_SUBMISSION_IDS" as "itemSubmissionIds", "ACTOR_ID" as "actorId", "COUNT" as "count" from "USER_STATISTICS_SERVICE"."LIFETIME_ACTION_STATS" where "ORG_ID" = :1 and (("USER_ID" = :2 and "USER_TYPE_ID" = :3) or ("USER_ID" = :4 and "USER_TYPE_ID" = :5))",
85 }
86 `);
87 });
88
89 test('should batch queries of more than 16,000 unique user ids', async () => {
90 const numUserIds = Math.floor(16_000 / Math.max(Math.random(), 0.05)); // some big int over 16,000
91 const largeUserIdList = Array.from({ length: numUserIds }, (_) => ({
92 id: randomUUID(),
93 typeId: randomUUID(),
94 }));
95
96 await sut({ orgId: 'x', userItemIdentifiers: largeUserIdList });
97 expect(warehouseMock.mock.calls.length).toBeGreaterThan(1);
98 });
99});