Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { randomUUID } from 'crypto';
2import { Kysely, type CompiledQuery, type QueryResult } from 'kysely';
3
4import { type Dependencies } from '../../iocContainer/index.js';
5import { type MockedFn } from '../../test/mockHelpers/jestMocks.js';
6import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js';
7import { safePick } from '../../utils/misc.js';
8import { makeFetchUserSubmissionStatistics } from './fetchUserSubmissionStatistics.js';
9
10describe('fetchUserSubmissionStatistics', () => {
11 let warehouseMock: MockedFn<
12 (it: CompiledQuery) => Promise<QueryResult<unknown>>
13 >;
14 let sut: ReturnType<typeof makeFetchUserSubmissionStatistics>;
15
16 beforeEach(() => {
17 // This mutation is safe (while we're not running tests concurrently) as
18 // it's local to the test suite. Consider using the `makeTestWithFixture`
19 // helper instead to make a local copy of this state for each test.
20
21 warehouseMock = jest.fn(async (_it) => Promise.resolve({ rows: [] }));
22
23 // This mutation is safe (while we're not running tests concurrently) as
24 // it's local to the test suite. Consider using the `makeTestWithFixture`
25 // helper instead to make a local copy of this state for each test.
26
27 const kysely = new Kysely({
28 dialect: makeMockWarehouseDialect(warehouseMock),
29 });
30 const dialectMock: Dependencies['DataWarehouseDialect'] = {
31 getKyselyInstance: () => kysely,
32 destroy: jest.fn(async () => {}),
33 };
34
35 sut = makeFetchUserSubmissionStatistics(dialectMock);
36 });
37
38 test('should generate proper query given org + user ids only', async () => {
39 await sut({ orgId: 'x', userItemIdentifiers: [{ id: '1', typeId: 'a' }] });
40 await sut({
41 orgId: 'x',
42 userItemIdentifiers: [
43 { id: '1', typeId: 'a' },
44 { id: '3', typeId: 'b' },
45 ],
46 });
47
48 expect(warehouseMock).toHaveBeenCalledTimes(2);
49
50 const queriesRan = warehouseMock.mock.calls.map((it) =>
51 safePick(it[0], ['parameters', 'sql']),
52 );
53
54 expect(queriesRan).toMatchInlineSnapshot(`
55 [
56 {
57 "parameters": [
58 "x",
59 "1",
60 "a",
61 ],
62 "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and ("USER_ID" = :2 and "USER_TYPE_ID" = :3) group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"",
63 },
64 {
65 "parameters": [
66 "x",
67 "1",
68 "a",
69 "3",
70 "b",
71 ],
72 "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and (("USER_ID" = :2 and "USER_TYPE_ID" = :3) or ("USER_ID" = :4 and "USER_TYPE_ID" = :5)) group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"",
73 },
74 ]
75 `);
76 });
77
78 test('should batch queries of more than 16,000 unique user ids', async () => {
79 const numUserIds = Math.floor(16_000 / Math.max(Math.random(), 0.05)); // some big int over 16,000
80 const largeUserIdList = Array.from({ length: numUserIds }, (_) => ({
81 id: randomUUID(),
82 typeId: randomUUID(),
83 }));
84
85 await sut({ orgId: 'x', userItemIdentifiers: largeUserIdList });
86 expect(warehouseMock.mock.calls.length).toBeGreaterThan(1);
87 });
88
89 test('should generate proper query given user/org ids + date filters', async () => {
90 await sut({
91 orgId: 'x',
92 userItemIdentifiers: [{ id: '1', typeId: 'a' }],
93 startTime: new Date('2020-01-01T00:00Z'),
94 });
95
96 await sut({
97 orgId: 'x',
98 userItemIdentifiers: [
99 { id: '1', typeId: 'a' },
100 { id: '3', typeId: 'b' },
101 ],
102 endTime: new Date('2020-01-01T00:00:00Z'),
103 });
104
105 await sut({
106 orgId: 'x',
107 userItemIdentifiers: [
108 { id: '1', typeId: 'a' },
109 { id: '3', typeId: 'b' },
110 ],
111 endTime: new Date('2020-01-01T00:00:00Z'),
112 startTime: new Date('2020-02-01T00:00:00Z'),
113 });
114
115 expect(warehouseMock).toHaveBeenCalledTimes(3);
116
117 const queriesRan = warehouseMock.mock.calls.map((it) =>
118 safePick(it[0], ['parameters', 'sql']),
119 );
120
121 expect(queriesRan).toMatchInlineSnapshot(`
122 [
123 {
124 "parameters": [
125 "x",
126 "1",
127 "a",
128 2020-01-01T00:00:00.000Z,
129 ],
130 "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and ("USER_ID" = :2 and "USER_TYPE_ID" = :3) and "TS_START_INCLUSIVE" >= :4 group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"",
131 },
132 {
133 "parameters": [
134 "x",
135 "1",
136 "a",
137 "3",
138 "b",
139 2020-01-01T00:00:00.000Z,
140 ],
141 "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and (("USER_ID" = :2 and "USER_TYPE_ID" = :3) or ("USER_ID" = :4 and "USER_TYPE_ID" = :5)) and "TS_END_EXCLUSIVE" <= :6 group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"",
142 },
143 {
144 "parameters": [
145 "x",
146 "1",
147 "a",
148 "3",
149 "b",
150 2020-02-01T00:00:00.000Z,
151 2020-01-01T00:00:00.000Z,
152 ],
153 "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and (("USER_ID" = :2 and "USER_TYPE_ID" = :3) or ("USER_ID" = :4 and "USER_TYPE_ID" = :5)) and "TS_START_INCLUSIVE" >= :6 and "TS_END_EXCLUSIVE" <= :7 group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"",
154 },
155 ]
156 `);
157 });
158});