@tstdl/base 0.93.145 → 0.93.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/authentication/tests/authentication.client-service.test.js +15 -19
- package/authentication/tests/authentication.service.test.js +92 -119
- package/notification/tests/notification-client.test.js +39 -50
- package/notification/tests/notification-flow.test.js +204 -238
- package/notification/tests/notification-sse.service.test.js +20 -27
- package/notification/tests/notification-type.service.test.js +17 -20
- package/orm/tests/query-complex.test.js +80 -111
- package/orm/tests/repository-advanced.test.js +100 -143
- package/orm/tests/repository-attributes.test.js +30 -39
- package/orm/tests/repository-compound-primary-key.test.js +67 -75
- package/orm/tests/repository-comprehensive.test.js +76 -101
- package/orm/tests/repository-coverage.test.d.ts +1 -0
- package/orm/tests/repository-coverage.test.js +88 -149
- package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
- package/orm/tests/repository-cti-extensive.test.js +118 -147
- package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
- package/orm/tests/repository-cti-mapping.test.js +29 -42
- package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
- package/orm/tests/repository-cti-soft-delete.test.js +25 -37
- package/orm/tests/repository-cti-transactions.test.js +19 -33
- package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
- package/orm/tests/repository-cti-upsert-many.test.js +38 -50
- package/orm/tests/repository-cti.test.d.ts +1 -0
- package/orm/tests/repository-cti.test.js +195 -247
- package/orm/tests/repository-expiration.test.d.ts +1 -0
- package/orm/tests/repository-expiration.test.js +46 -59
- package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
- package/orm/tests/repository-extra-coverage.test.js +195 -337
- package/orm/tests/repository-mapping.test.d.ts +1 -0
- package/orm/tests/repository-mapping.test.js +20 -20
- package/orm/tests/repository-regression.test.js +124 -163
- package/orm/tests/repository-search.test.js +30 -44
- package/orm/tests/repository-soft-delete.test.js +54 -79
- package/orm/tests/repository-types.test.js +77 -111
- package/package.json +1 -1
- package/task-queue/tests/worker.test.js +5 -5
- package/testing/README.md +38 -16
- package/testing/integration-setup.d.ts +11 -0
- package/testing/integration-setup.js +57 -30
|
@@ -7,17 +7,17 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
-
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
11
|
-
import { StringProperty } from '../../schema/index.js';
|
|
12
10
|
import { sql } from 'drizzle-orm';
|
|
13
11
|
import { beforeAll, describe, expect, test } from 'vitest';
|
|
12
|
+
import { StringProperty } from '../../schema/index.js';
|
|
13
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
14
14
|
import { Table } from '../decorators.js';
|
|
15
15
|
import { Entity } from '../entity.js';
|
|
16
|
-
import {
|
|
17
|
-
import { injectRepository } from '../server/repository.js';
|
|
16
|
+
import { getRepository } from '../server/index.js';
|
|
18
17
|
describe('ORM Repository Soft Delete (Integration)', () => {
|
|
19
18
|
let injector;
|
|
20
19
|
let db;
|
|
20
|
+
let repository;
|
|
21
21
|
const schema = 'test_orm_soft_delete';
|
|
22
22
|
let SoftDeleteEntity = class SoftDeleteEntity extends Entity {
|
|
23
23
|
name;
|
|
@@ -30,14 +30,10 @@ describe('ORM Repository Soft Delete (Integration)', () => {
|
|
|
30
30
|
Table('soft_delete_entities', { schema })
|
|
31
31
|
], SoftDeleteEntity);
|
|
32
32
|
beforeAll(async () => {
|
|
33
|
-
injector =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
db = injector.resolve(Database);
|
|
33
|
+
({ injector, database: db } = await setupIntegrationTest({
|
|
34
|
+
orm: { schema },
|
|
35
|
+
}));
|
|
36
|
+
repository = injector.resolve(getRepository(SoftDeleteEntity));
|
|
41
37
|
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
42
38
|
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
43
39
|
await db.execute(sql `
|
|
@@ -54,90 +50,69 @@ describe('ORM Repository Soft Delete (Integration)', () => {
|
|
|
54
50
|
});
|
|
55
51
|
test('should soft delete an entity by ID', async () => {
|
|
56
52
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
expect(allWithDeleted).toHaveLength(1);
|
|
68
|
-
expect(allWithDeleted[0].id).toBe(e1.id);
|
|
69
|
-
});
|
|
53
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
54
|
+
const deleted = await repository.delete(e1.id);
|
|
55
|
+
expect(deleted.id).toBe(e1.id);
|
|
56
|
+
expect(deleted.metadata.deleteTimestamp).toBeDefined();
|
|
57
|
+
expect(await repository.has(e1.id)).toBe(false);
|
|
58
|
+
expect(await repository.tryLoad(e1.id)).toBeUndefined();
|
|
59
|
+
expect(await repository.count()).toBe(0);
|
|
60
|
+
const allWithDeleted = await repository.loadAll({ withDeleted: true });
|
|
61
|
+
expect(allWithDeleted).toHaveLength(1);
|
|
62
|
+
expect(allWithDeleted[0].id).toBe(e1.id);
|
|
70
63
|
});
|
|
71
64
|
test('should soft delete multiple entities by IDs', async () => {
|
|
72
65
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(await repository.count()).toBe(0);
|
|
80
|
-
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(2);
|
|
81
|
-
});
|
|
66
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
67
|
+
const e2 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E2' }));
|
|
68
|
+
const deleted = await repository.deleteMany([e1.id, e2.id]);
|
|
69
|
+
expect(deleted).toHaveLength(2);
|
|
70
|
+
expect(await repository.count()).toBe(0);
|
|
71
|
+
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(2);
|
|
82
72
|
});
|
|
83
73
|
test('should soft delete entities by query', async () => {
|
|
84
74
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
85
|
-
await
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
expect(await repository.count()).toBe(1);
|
|
95
|
-
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(3);
|
|
96
|
-
});
|
|
75
|
+
await repository.insertMany([
|
|
76
|
+
Object.assign(new SoftDeleteEntity(), { name: 'A' }),
|
|
77
|
+
Object.assign(new SoftDeleteEntity(), { name: 'B' }),
|
|
78
|
+
Object.assign(new SoftDeleteEntity(), { name: 'A' }),
|
|
79
|
+
]);
|
|
80
|
+
const deleted = await repository.deleteManyByQuery({ name: 'A' });
|
|
81
|
+
expect(deleted).toHaveLength(2);
|
|
82
|
+
expect(await repository.count()).toBe(1);
|
|
83
|
+
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(3);
|
|
97
84
|
});
|
|
98
85
|
test('should not update soft-deleted entities by default', async () => {
|
|
99
86
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
100
|
-
await
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const loaded = await repository.tryLoad(e1.id, { withDeleted: true });
|
|
107
|
-
expect(loaded.name).toBe('E1');
|
|
108
|
-
});
|
|
87
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
88
|
+
await repository.delete(e1.id);
|
|
89
|
+
const updated = await repository.tryUpdate(e1.id, { name: 'Updated' });
|
|
90
|
+
expect(updated).toBeUndefined();
|
|
91
|
+
const loaded = await repository.tryLoad(e1.id, { withDeleted: true });
|
|
92
|
+
expect(loaded.name).toBe('E1');
|
|
109
93
|
});
|
|
110
94
|
test('should support hard delete on soft-deleted entities', async () => {
|
|
111
95
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
112
|
-
await
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expect(hardDeleted.id).toBe(e1.id);
|
|
118
|
-
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
|
|
119
|
-
});
|
|
96
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
97
|
+
await repository.delete(e1.id);
|
|
98
|
+
const hardDeleted = await repository.hardDelete(e1.id);
|
|
99
|
+
expect(hardDeleted.id).toBe(e1.id);
|
|
100
|
+
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
|
|
120
101
|
});
|
|
121
102
|
test('should ignore already soft-deleted entities when deleting again', async () => {
|
|
122
103
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
123
|
-
await
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const secondDelete = await repository.tryDelete(e1.id);
|
|
128
|
-
expect(secondDelete).toBeUndefined();
|
|
129
|
-
});
|
|
104
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
105
|
+
await repository.delete(e1.id);
|
|
106
|
+
const secondDelete = await repository.tryDelete(e1.id);
|
|
107
|
+
expect(secondDelete).toBeUndefined();
|
|
130
108
|
});
|
|
131
109
|
test('should hard delete multiple entities by query (including soft-deleted)', async () => {
|
|
132
110
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
|
|
133
|
-
await
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
expect(hardDeleted).toHaveLength(2); // Should hard delete both
|
|
140
|
-
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
|
|
141
|
-
});
|
|
111
|
+
const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
|
|
112
|
+
const e2 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E2' }));
|
|
113
|
+
await repository.delete(e1.id); // Soft delete E1
|
|
114
|
+
const hardDeleted = await repository.hardDeleteManyByQuery({});
|
|
115
|
+
expect(hardDeleted).toHaveLength(2); // Should hard delete both
|
|
116
|
+
expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
|
|
142
117
|
});
|
|
143
118
|
});
|
|
@@ -9,16 +9,16 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
import { sql } from 'drizzle-orm';
|
|
11
11
|
import { beforeAll, describe, expect, test } from 'vitest';
|
|
12
|
-
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
13
12
|
import { Array, BooleanProperty, Integer, NumberProperty, string, StringProperty } from '../../schema/index.js';
|
|
13
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
14
14
|
import { Table } from '../decorators.js';
|
|
15
15
|
import { Entity } from '../entity.js';
|
|
16
16
|
import { JsonProperty, NumericDateProperty, TimestampProperty } from '../schemas/index.js';
|
|
17
|
-
import {
|
|
18
|
-
import { injectRepository } from '../server/repository.js';
|
|
17
|
+
import { getRepository } from '../server/index.js';
|
|
19
18
|
describe('ORM Repository Types (Integration)', () => {
|
|
20
19
|
let injector;
|
|
21
20
|
let db;
|
|
21
|
+
let repo;
|
|
22
22
|
const schema = 'test_orm_types';
|
|
23
23
|
let TypeEntity = class TypeEntity extends Entity {
|
|
24
24
|
text;
|
|
@@ -66,14 +66,10 @@ describe('ORM Repository Types (Integration)', () => {
|
|
|
66
66
|
Table('type_entities', { schema })
|
|
67
67
|
], TypeEntity);
|
|
68
68
|
beforeAll(async () => {
|
|
69
|
-
injector =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
db = injector.resolve(Database);
|
|
69
|
+
({ injector, database: db } = await setupIntegrationTest({
|
|
70
|
+
orm: { schema },
|
|
71
|
+
}));
|
|
72
|
+
repo = injector.resolve(getRepository(TypeEntity));
|
|
77
73
|
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
78
74
|
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('type_entities')} CASCADE`);
|
|
79
75
|
await db.execute(sql `
|
|
@@ -96,123 +92,93 @@ describe('ORM Repository Types (Integration)', () => {
|
|
|
96
92
|
`);
|
|
97
93
|
});
|
|
98
94
|
test('should persist and retrieve boolean values (true)', async () => {
|
|
99
|
-
await
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const loaded = await repo.load(entity.id);
|
|
105
|
-
expect(loaded.bool).toBe(true);
|
|
106
|
-
});
|
|
95
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
96
|
+
text: 't', int: 1, float: 1.1, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
97
|
+
}));
|
|
98
|
+
const loaded = await repo.load(entity.id);
|
|
99
|
+
expect(loaded.bool).toBe(true);
|
|
107
100
|
});
|
|
108
101
|
test('should persist and retrieve boolean values (false)', async () => {
|
|
109
|
-
await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const loaded = await repo.load(entity.id);
|
|
115
|
-
expect(loaded.bool).toBe(false);
|
|
116
|
-
});
|
|
102
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
103
|
+
text: 'f', int: 1, float: 1.1, bool: false, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
104
|
+
}));
|
|
105
|
+
const loaded = await repo.load(entity.id);
|
|
106
|
+
expect(loaded.bool).toBe(false);
|
|
117
107
|
});
|
|
118
108
|
test('should persist and retrieve integer boundaries', async () => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const l2 = await repo.load(e2.id);
|
|
132
|
-
expect(l2.int).toBe(minInt);
|
|
133
|
-
});
|
|
109
|
+
const maxInt = 2147483647;
|
|
110
|
+
const minInt = -2147483648;
|
|
111
|
+
const e1 = await repo.insert(Object.assign(new TypeEntity(), {
|
|
112
|
+
text: 'max', int: maxInt, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
113
|
+
}));
|
|
114
|
+
const l1 = await repo.load(e1.id);
|
|
115
|
+
expect(l1.int).toBe(maxInt);
|
|
116
|
+
const e2 = await repo.insert(Object.assign(new TypeEntity(), {
|
|
117
|
+
text: 'min', int: minInt, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
118
|
+
}));
|
|
119
|
+
const l2 = await repo.load(e2.id);
|
|
120
|
+
expect(l2.int).toBe(minInt);
|
|
134
121
|
});
|
|
135
122
|
test('should persist and retrieve float precision', async () => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const loaded = await repo.load(entity.id);
|
|
143
|
-
expect(loaded.float).toBeCloseTo(pi);
|
|
144
|
-
});
|
|
123
|
+
const pi = 3.1415926535;
|
|
124
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
125
|
+
text: 'pi', int: 0, float: pi, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
126
|
+
}));
|
|
127
|
+
const loaded = await repo.load(entity.id);
|
|
128
|
+
expect(loaded.float).toBeCloseTo(pi);
|
|
145
129
|
});
|
|
146
130
|
test('should persist and retrieve numeric dates', async () => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const loaded = await repo.load(entity.id);
|
|
154
|
-
expect(loaded.numericDate).toBe(today);
|
|
155
|
-
});
|
|
131
|
+
const today = 19723; // 2024-01-01
|
|
132
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
133
|
+
text: 'date', int: 0, float: 0, bool: true, numericDate: today, timestamp: new Date(), json: {}, tags: [],
|
|
134
|
+
}));
|
|
135
|
+
const loaded = await repo.load(entity.id);
|
|
136
|
+
expect(loaded.numericDate).toBe(today);
|
|
156
137
|
});
|
|
157
138
|
test('should persist and retrieve timestamps', async () => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
expect(typeof loaded.timestamp).toBe('number');
|
|
169
|
-
expect(Math.abs(loaded.timestamp - now.getTime())).toBeLessThan(2);
|
|
170
|
-
});
|
|
139
|
+
const now = new Date();
|
|
140
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
141
|
+
text: 'ts', int: 0, float: 0, bool: true, numericDate: 0, timestamp: now, json: {}, tags: [],
|
|
142
|
+
}));
|
|
143
|
+
const loaded = await repo.load(entity.id);
|
|
144
|
+
// Postgres stores microsecond precision, JS Date has millisecond.
|
|
145
|
+
// Drizzle custom type returns number (milliseconds).
|
|
146
|
+
// We accept a small difference due to precision loss or round-trip.
|
|
147
|
+
expect(typeof loaded.timestamp).toBe('number');
|
|
148
|
+
expect(Math.abs(loaded.timestamp - now.getTime())).toBeLessThan(2);
|
|
171
149
|
});
|
|
172
150
|
test('should persist and retrieve complex JSON', async () => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const loaded = await repo.load(entity.id);
|
|
184
|
-
expect(loaded.json).toEqual(data);
|
|
185
|
-
});
|
|
151
|
+
const data = {
|
|
152
|
+
foo: 'bar',
|
|
153
|
+
nested: { a: 1, b: [true, false] },
|
|
154
|
+
nullVal: null,
|
|
155
|
+
};
|
|
156
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
157
|
+
text: 'json', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: data, tags: [],
|
|
158
|
+
}));
|
|
159
|
+
const loaded = await repo.load(entity.id);
|
|
160
|
+
expect(loaded.json).toEqual(data);
|
|
186
161
|
});
|
|
187
162
|
test('should persist and retrieve arrays', async () => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const loaded = await repo.load(entity.id);
|
|
195
|
-
expect(loaded.tags).toEqual(tags);
|
|
196
|
-
});
|
|
163
|
+
const tags = ['a', 'b', 'c'];
|
|
164
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
165
|
+
text: 'array', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags,
|
|
166
|
+
}));
|
|
167
|
+
const loaded = await repo.load(entity.id);
|
|
168
|
+
expect(loaded.tags).toEqual(tags);
|
|
197
169
|
});
|
|
198
170
|
test('should persist and retrieve empty arrays', async () => {
|
|
199
|
-
await
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const loaded = await repo.load(entity.id);
|
|
205
|
-
expect(loaded.tags).toEqual([]);
|
|
206
|
-
});
|
|
171
|
+
const entity = await repo.insert(Object.assign(new TypeEntity(), {
|
|
172
|
+
text: 'empty-array', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
173
|
+
}));
|
|
174
|
+
const loaded = await repo.load(entity.id);
|
|
175
|
+
expect(loaded.tags).toEqual([]);
|
|
207
176
|
});
|
|
208
177
|
test('should fail on integer overflow', async () => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
text: 'overflow', int: overflow, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
215
|
-
}))).rejects.toThrow(); // Postgres should reject
|
|
216
|
-
});
|
|
178
|
+
// Max int is 2147483647
|
|
179
|
+
const overflow = 3000000000;
|
|
180
|
+
await expect(repo.insert(Object.assign(new TypeEntity(), {
|
|
181
|
+
text: 'overflow', int: overflow, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
|
|
182
|
+
}))).rejects.toThrow(); // Postgres should reject
|
|
217
183
|
});
|
|
218
184
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
2
|
import { CancellationToken } from '../../cancellation/index.js';
|
|
3
|
-
import {
|
|
4
|
-
import { injectRepository } from '../../orm/server/index.js';
|
|
3
|
+
import { getRepository } from '../../orm/server/index.js';
|
|
5
4
|
import { TaskProcessResult, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
6
5
|
import { PostgresTask } from '../../task-queue/postgres/index.js';
|
|
7
6
|
import { setupIntegrationTest } from '../../testing/index.js';
|
|
@@ -11,8 +10,10 @@ describe('Worker & Base Class Tests', () => {
|
|
|
11
10
|
let queue;
|
|
12
11
|
let token;
|
|
13
12
|
let otherQueueName;
|
|
13
|
+
let taskRepository;
|
|
14
14
|
beforeAll(async () => {
|
|
15
15
|
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
16
|
+
taskRepository = injector.resolve(getRepository(PostgresTask));
|
|
16
17
|
otherQueueName = `other-queue-${crypto.randomUUID()}`;
|
|
17
18
|
});
|
|
18
19
|
beforeEach(() => {
|
|
@@ -25,11 +26,10 @@ describe('Worker & Base Class Tests', () => {
|
|
|
25
26
|
});
|
|
26
27
|
afterEach(async () => {
|
|
27
28
|
token.set();
|
|
28
|
-
const repository = runInInjectionContext(injector, () => injectRepository(PostgresTask));
|
|
29
29
|
const namespace = queue.getTransactionalContextData().namespace;
|
|
30
30
|
// Clear foreign keys
|
|
31
|
-
await
|
|
32
|
-
await
|
|
31
|
+
await taskRepository.updateManyByQuery({ namespace }, { parentId: null });
|
|
32
|
+
await taskRepository.updateManyByQuery({ namespace: otherQueueName }, { parentId: null });
|
|
33
33
|
await queue.clear();
|
|
34
34
|
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
35
35
|
await queueProvider.get(otherQueueName).clear();
|
package/testing/README.md
CHANGED
|
@@ -80,11 +80,16 @@ The `setupIntegrationTest` utility (found in `source/testing/integration-setup.t
|
|
|
80
80
|
4. **Schema Isolation**: Automatically creates and uses PostgreSQL schemas for isolation.
|
|
81
81
|
5. **Modules**: Optional configuration for `taskQueue`, `authentication`, `objectStorage`, `notification`, etc.
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
Integration tests typically require an injection context to resolve services and repositories. You can use the `testInInjector` (or `itInInjector`) helper to automatically wrap your test body.
|
|
84
|
+
|
|
85
|
+
> [!IMPORTANT]
|
|
86
|
+
> `testInInjector` is designed for test files where a **single injector** is shared across all tests (typically initialized in `beforeAll`). It accepts a `ValueOrProvider<Injector>`, allowing you to pass a getter function (e.g., `() => injector`) if the injector variable is initialized late.
|
|
87
|
+
>
|
|
88
|
+
> If your tests require a fresh injector per test case (e.g. `setupIntegrationTest` in `beforeEach`), you can still use `testInInjector` by passing a provider.
|
|
84
89
|
|
|
85
90
|
```typescript
|
|
86
|
-
import { beforeAll, describe, expect
|
|
87
|
-
import { setupIntegrationTest } from '#/testing/index.js';
|
|
91
|
+
import { beforeAll, describe, expect } from 'vitest';
|
|
92
|
+
import { setupIntegrationTest, testInInjector } from '#/testing/index.js';
|
|
88
93
|
import { MyService } from '../my-service.js';
|
|
89
94
|
|
|
90
95
|
describe('MyService Integration', () => {
|
|
@@ -98,19 +103,32 @@ describe('MyService Integration', () => {
|
|
|
98
103
|
}));
|
|
99
104
|
});
|
|
100
105
|
|
|
101
|
-
|
|
106
|
+
testInInjector('should process data through the database', injector, async () => {
|
|
102
107
|
const service = injector.resolve(MyService);
|
|
103
108
|
const result = await service.process();
|
|
104
109
|
expect(result.success).toBe(true);
|
|
105
110
|
});
|
|
111
|
+
|
|
112
|
+
// Using a provider function to defer resolution of the 'injector' variable when using `beforeEach` for injector setup
|
|
113
|
+
testInInjector('should process data through the database', () => injector, async () => { ... });
|
|
106
114
|
});
|
|
107
115
|
```
|
|
108
116
|
|
|
109
117
|
## Best Practices & Guidelines
|
|
110
118
|
|
|
111
|
-
### 1. Isolation
|
|
112
|
-
|
|
113
|
-
-
|
|
119
|
+
### 1. Isolation & Performance
|
|
120
|
+
|
|
121
|
+
- **`beforeAll` vs `beforeEach`**:
|
|
122
|
+
- **Use `beforeAll` (Recommended)**: For most integration tests. It's significantly faster as it creates the schema and initializes the injector once.
|
|
123
|
+
- **Use `beforeEach`**: Use this only when:
|
|
124
|
+
- You need a completely clean Injector state for every test (e.g. if singletons are being mutated and cannot be reset).
|
|
125
|
+
- You are testing the initialization or disposal logic of your services.
|
|
126
|
+
- Each test requires a different module configuration in `setupIntegrationTest`.
|
|
127
|
+
- **Data Reset**:
|
|
128
|
+
- **`clearTenantData` (Best for Parallelism)**: If your tests use random `tenantId`s, use this in `afterEach` or `beforeEach`. It allows multiple tests within the same schema to run without interfering with each other's data.
|
|
129
|
+
- **`truncateTables` (Use with Caution)**: Only suitable if you are certain that no other tests are concurrently using the same database tables (even with different tenant IDs), as `TRUNCATE` affects the entire table and can cause flakiness in parallel environments.
|
|
130
|
+
- **Unit vs. Integration**: Prefer integration tests over unit tests when reasonably possible to cover more code paths and reduce the need for extensive mocking.
|
|
131
|
+
- **Parallelism**: Vitest runs test files in parallel. Always use unique resource names (e.g., using `crypto.randomUUID()`) to avoid flaky tests and cross-file interference.
|
|
114
132
|
- **Resource Naming**: When testing shared infrastructure (Task Queues, S3 Buckets, Rate Limiters), append a random suffix or timestamp to the resource name (e.g., `const queueName = \`test-queue-${crypto.randomUUID()}\``).
|
|
115
133
|
- **Cleanup**: Use `truncateTables` or `dropTables` in `beforeAll` or `beforeEach` to ensure a clean state.
|
|
116
134
|
- **Tenant Data**: Use `clearTenantData` if your tests are multi-tenant and use random tenant IDs to avoid collisions when concurrently running tests.
|
|
@@ -124,24 +142,26 @@ describe('MyService Integration', () => {
|
|
|
124
142
|
const mailServiceMock = { send: vi.fn() };
|
|
125
143
|
injector.register(MailService, { useValue: mailServiceMock });
|
|
126
144
|
```
|
|
127
|
-
- **Injection Context**: Any code that uses repositories or services relying on `inject()` outside of a class constructor/initializer **must** be wrapped in
|
|
145
|
+
- **Injection Context**: Any code that uses repositories or services relying on `inject()` outside of a class constructor/initializer **must** be wrapped in an injection context.
|
|
146
|
+
- **Preferred**: Use `testInInjector` (or `itInInjector`) for the entire test body.
|
|
147
|
+
- **Alternative**: Use `runInInjectionContext(injector, async () => { ... })` for specific blocks within a test or hook.
|
|
128
148
|
- **Mock Reset**: Always call `vi.clearAllMocks()` in `beforeEach` if using spys or mocks to ensure test independence.
|
|
129
149
|
|
|
130
150
|
### 3. Integration Test Patterns
|
|
131
151
|
|
|
132
152
|
- **Schema Management**:
|
|
133
153
|
- **Preferred**: Use the `modules` option in `setupIntegrationTest`. This automatically runs the necessary migrations for standard modules (e.g., `taskQueue`, `authentication`, `notification`).
|
|
134
|
-
- **Fallback**: Use Manual DDL via `database.execute(sql\`...\`)
|
|
154
|
+
- **Fallback**: Use Manual DDL via `database.execute(sql\`...\`)` **only** for test-specific entities (like local `@Table` classes defined inside your test file) that are not part of a standard module. Always call `dropTables` before creating them to ensure a fresh start.
|
|
155
|
+
- **Repositories**: You can inject ad-hoc repositories inside an injection context:
|
|
156
|
+
```typescript
|
|
157
|
+
const repo = injectRepository(MyEntity);
|
|
158
|
+
```
|
|
135
159
|
- **Background Workers**: When testing workers or loops, use a `CancellationToken` to stop them in `afterEach` or `afterAll`.
|
|
160
|
+
- **Polling & Waiting**: When testing asynchronous background effects (like a task queue processing a task), avoid arbitrary `timeout()` calls if possible. Instead, use a polling loop or wait for a specific database state.
|
|
136
161
|
- **Timeouts**: Use the `timeout(ms)` helper from `#/utils/timing.js` for tests involving refills, retries, or background processing. Prefer `vi.useFakeTimers()` only when real-time passage is too slow for the test suite.
|
|
137
162
|
- **Logging**: Test should generally not output logs unless for active debugging. Use try-catch for expected errors and assert on the error message instead of relying on logs.
|
|
138
163
|
|
|
139
|
-
### 4.
|
|
140
|
-
|
|
141
|
-
- Use `beforeAll` for heavy setup (like schema and table creation) and `beforeEach` for lightweight data resetting (truncation).
|
|
142
|
-
- Prefer integration tests over unit tests when reasonably possible to cover more code paths and reduce the need for extensive mocking and stubbing.
|
|
143
|
-
|
|
144
|
-
### 5. Coding Style
|
|
164
|
+
### 4. Coding Style
|
|
145
165
|
|
|
146
166
|
- **Explicit Types**: Use explicit return types for methods and functions.
|
|
147
167
|
- **Async/Await**: Always use `async/await` for asynchronous code to improve readability and error handling.
|
|
@@ -149,9 +169,11 @@ describe('MyService Integration', () => {
|
|
|
149
169
|
- **Avoid `any`**: Strive to use specific types or `unknown` if required to maintain type safety.
|
|
150
170
|
- **Descriptive Names**: Use descriptive names for test files, cases and variables to improve readability.
|
|
151
171
|
|
|
152
|
-
###
|
|
172
|
+
### 5. Common Helpers (from `source/testing/`)
|
|
153
173
|
|
|
154
174
|
- `setupIntegrationTest(options)`: Main entry point for integration tests.
|
|
175
|
+
- `testInInjector(name, injector, fn, options)`: Runs a test within an injection context.
|
|
176
|
+
- `itInInjector`: Alias for `testInInjector`.
|
|
155
177
|
- `truncateTables(database, schema, tables)`: Clears data from specified tables.
|
|
156
178
|
- `dropTables(database, schema, tables)`: Drops specified tables.
|
|
157
179
|
- `clearTenantData(database, schema, tables, tenantId)`: Deletes data for a specific tenant.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/useExpect: helper file */
|
|
1
2
|
import type { PoolConfig } from 'pg';
|
|
3
|
+
import { type TestOptions } from 'vitest';
|
|
2
4
|
import { type AuthenticationAncillaryService } from '../authentication/server/index.js';
|
|
3
5
|
import { Injector } from '../injector/index.js';
|
|
4
6
|
import { LogLevel } from '../logger/index.js';
|
|
@@ -6,6 +8,7 @@ import { type S3ObjectStorageProviderConfig } from '../object-storage/s3/index.j
|
|
|
6
8
|
import type { EntityType } from '../orm/entity.js';
|
|
7
9
|
import { Database } from '../orm/server/index.js';
|
|
8
10
|
import type { Type } from '../types/index.js';
|
|
11
|
+
import { type ValueOrProvider } from '../utils/value-or-provider.js';
|
|
9
12
|
export type IntegrationTestOptions = {
|
|
10
13
|
dbConfig?: Partial<PoolConfig>;
|
|
11
14
|
orm?: {
|
|
@@ -43,6 +46,14 @@ export type TestContext = {
|
|
|
43
46
|
* Standard setup for integration tests.
|
|
44
47
|
*/
|
|
45
48
|
export declare function setupIntegrationTest(options?: IntegrationTestOptions): Promise<TestContext>;
|
|
49
|
+
/**
|
|
50
|
+
* A wrapper for vitest's `test` that automatically runs the test function in the provided injector's context.
|
|
51
|
+
* @param name The name of the test.
|
|
52
|
+
* @param injector The injector to use for the context.
|
|
53
|
+
* @param fn The test function.
|
|
54
|
+
* @param options Vitest test options.
|
|
55
|
+
*/
|
|
56
|
+
export declare function testInInjector(name: string, injector: ValueOrProvider<Injector>, fn: () => any, options?: number | TestOptions): void;
|
|
46
57
|
/**
|
|
47
58
|
* Helper to truncate specific tables in a schema.
|
|
48
59
|
* Useful in beforeEach() to reset state.
|