@lti-tool/postgresql 1.0.0

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.
@@ -0,0 +1,207 @@
1
+ // oxlint-disable max-lines-per-function typescript/no-explicit-any
2
+ import type { LTIClient, LTIDeployment, LTISession } from '@lti-tool/core';
3
+ import 'dotenv/config';
4
+ import { drizzle } from 'drizzle-orm/postgres-js';
5
+ import postgres from 'postgres';
6
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
7
+
8
+ import * as schema from '../src/db/schema/index.js';
9
+ import { PostgresStorage } from '../src/index.js';
10
+
11
+ let storage: PostgresStorage;
12
+ let sql: postgres.Sql;
13
+ let db: any;
14
+
15
+ const testClient: Omit<LTIClient, 'id'> = {
16
+ name: 'Test Platform',
17
+ iss: 'https://platform.example.com',
18
+ clientId: 'test-client-123',
19
+ authUrl: 'https://platform.example.com/auth',
20
+ tokenUrl: 'https://platform.example.com/token',
21
+ jwksUrl: 'https://platform.example.com/.well-known/jwks',
22
+ deployments: [],
23
+ };
24
+
25
+ const testDeployment: Omit<LTIDeployment, 'id'> = {
26
+ deploymentId: 'deployment-456',
27
+ name: 'Test Deployment',
28
+ description: 'A test deployment',
29
+ };
30
+
31
+ const testSession: LTISession = {
32
+ id: crypto.randomUUID(),
33
+ jwtPayload: { iss: 'https://platform.example.com' },
34
+ user: { id: 'user123', roles: ['Learner'] },
35
+ context: { id: 'context123', label: 'TEST101', title: 'Test Course' },
36
+ platform: {
37
+ issuer: 'https://platform.example.com',
38
+ clientId: 'test-client-123',
39
+ deploymentId: 'deployment-456',
40
+ name: 'Test Platform',
41
+ },
42
+ launch: { target: 'https://tool.example.com/launch' },
43
+ customParameters: {},
44
+ isAdmin: false,
45
+ isInstructor: false,
46
+ isStudent: true,
47
+ isAssignmentAndGradesAvailable: false,
48
+ isDeepLinkingAvailable: false,
49
+ isNameAndRolesAvailable: false,
50
+ };
51
+
52
+ beforeAll(() => {
53
+ // env var or local podman / docker container credentials
54
+ const connectionUrl =
55
+ process.env.DATABASE_URL ||
56
+ 'postgresql://lti_user:lti_password@localhost:5432/lti_test';
57
+ sql = postgres(connectionUrl);
58
+ db = drizzle(sql, { schema });
59
+
60
+ storage = new PostgresStorage({ connectionUrl });
61
+ });
62
+
63
+ afterAll(async () => {
64
+ // close the drizzle connection
65
+ await storage.close();
66
+
67
+ // close the vitest connection
68
+ await sql.end();
69
+ });
70
+
71
+ beforeEach(async () => {
72
+ // Clean all tables between tests
73
+ await db.delete(schema.deploymentsTable);
74
+ await db.delete(schema.clientsTable);
75
+ await db.delete(schema.sessionsTable);
76
+ await db.delete(schema.noncesTable);
77
+ await db.delete(schema.registrationSessionsTable);
78
+ });
79
+
80
+ describe('PostgresStorage - Client Operations', () => {
81
+ it('should add and retrieve a client', async () => {
82
+ const clientId = await storage.addClient(testClient);
83
+ expect(clientId).toBeTruthy();
84
+
85
+ const retrieved = await storage.getClientById(clientId);
86
+ expect(retrieved).toBeDefined();
87
+ expect(retrieved?.name).toBe(testClient.name);
88
+ expect(retrieved?.iss).toBe(testClient.iss);
89
+ expect(retrieved?.deployments).toEqual([]);
90
+ });
91
+
92
+ it('should list all clients', async () => {
93
+ await storage.addClient(testClient);
94
+ await storage.addClient({ ...testClient, clientId: 'another-client' });
95
+
96
+ const clients = await storage.listClients();
97
+ expect(clients.length).toBeGreaterThanOrEqual(2);
98
+ });
99
+
100
+ it('should delete a client and its deployments', async () => {
101
+ const clientId = await storage.addClient(testClient);
102
+ await storage.addDeployment(clientId, testDeployment);
103
+
104
+ await storage.deleteClient(clientId);
105
+
106
+ const retrieved = await storage.getClientById(clientId);
107
+ expect(retrieved).toBeUndefined();
108
+ });
109
+ });
110
+
111
+ describe('PostgresStorage - Deployment Operations', () => {
112
+ let clientId: string;
113
+
114
+ beforeEach(async () => {
115
+ clientId = await storage.addClient(testClient);
116
+ });
117
+
118
+ it('should add and retrieve a deployment', async () => {
119
+ const deploymentId = await storage.addDeployment(clientId, testDeployment);
120
+
121
+ const retrieved = await storage.getDeployment(clientId, deploymentId);
122
+ expect(retrieved?.deploymentId).toBe(testDeployment.deploymentId);
123
+ });
124
+
125
+ it('should list deployments', async () => {
126
+ await storage.addDeployment(clientId, testDeployment);
127
+ await storage.addDeployment(clientId, { ...testDeployment, deploymentId: 'dep-2' });
128
+
129
+ const deployments = await storage.listDeployments(clientId);
130
+ expect(deployments.length).toBe(2);
131
+ });
132
+ });
133
+
134
+ describe('PostgresStorage - Session Operations', () => {
135
+ it('should add and retrieve a session', async () => {
136
+ await storage.addSession(testSession);
137
+
138
+ const retrieved = await storage.getSession(testSession.id);
139
+ expect(retrieved?.user.id).toBe(testSession.user.id);
140
+ });
141
+
142
+ it('should not retrieve expired sessions', async () => {
143
+ const expiredSessionId = crypto.randomUUID();
144
+ await db.insert(schema.sessionsTable).values({
145
+ id: expiredSessionId,
146
+ data: testSession,
147
+ expiresAt: new Date(Date.now() - 1000),
148
+ });
149
+
150
+ const retrieved = await storage.getSession(expiredSessionId);
151
+ expect(retrieved).toBeUndefined();
152
+ });
153
+ });
154
+
155
+ describe('PostgresStorage - Nonce Validation', () => {
156
+ it('should validate a new nonce', async () => {
157
+ const result = await storage.validateNonce('unique-nonce');
158
+ expect(result).toBe(true);
159
+ });
160
+
161
+ it('should reject duplicate nonce', async () => {
162
+ await storage.validateNonce('dup-nonce');
163
+ const result = await storage.validateNonce('dup-nonce');
164
+ expect(result).toBe(false);
165
+ });
166
+ });
167
+
168
+ describe('PostgresStorage - Launch Config', () => {
169
+ it('should derive launch config from join', async () => {
170
+ const clientId = await storage.addClient(testClient);
171
+ await storage.addDeployment(clientId, testDeployment);
172
+
173
+ const config = await storage.getLaunchConfig(
174
+ testClient.iss,
175
+ testClient.clientId,
176
+ testDeployment.deploymentId,
177
+ );
178
+
179
+ expect(config?.iss).toBe(testClient.iss);
180
+ expect(config?.authUrl).toBe(testClient.authUrl);
181
+ });
182
+
183
+ it('should fallback to default deployment', async () => {
184
+ const clientId = await storage.addClient(testClient);
185
+ await storage.addDeployment(clientId, { deploymentId: 'default' });
186
+
187
+ const config = await storage.getLaunchConfig(
188
+ testClient.iss,
189
+ testClient.clientId,
190
+ 'nonexistent',
191
+ );
192
+
193
+ expect(config?.deploymentId).toBe('default');
194
+ });
195
+ });
196
+
197
+ describe('PostgresStorage - Cleanup', () => {
198
+ it('should delete expired items', async () => {
199
+ await db.insert(schema.noncesTable).values({
200
+ nonce: 'expired-nonce',
201
+ expiresAt: new Date(Date.now() - 1000),
202
+ });
203
+
204
+ const result = await storage.cleanup();
205
+ expect(result.noncesDeleted).toBe(1);
206
+ });
207
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }