Skip to content

Commit d3db0ad

Browse files
authored
Merge pull request #1672 from rocket-admin/backend_selfosted_dbs
feat: update delete connection endpoint and add integration tests
2 parents 37157c2 + b4e110a commit d3db0ad

3 files changed

Lines changed: 356 additions & 4 deletions

File tree

backend/src/microservices/saas-microservice/data-structures/delete-connection-for-hosted-db.dto.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,14 @@ export class DeleteConnectionForHostedDbDto {
1313

1414
@ApiProperty({
1515
description: 'Hosted db entity ID',
16-
example: '123e4567-e89b-12d3-a456-426614174000',
1716
})
1817
@IsNotEmpty()
1918
@IsString()
20-
@IsUUID()
2119
hostedDatabaseId: string;
2220

2321
@ApiProperty({
2422
description: 'Database name',
25-
example: 'my_database',
23+
example: 'my_database',
2624
})
2725
databaseName: string;
2826
}

backend/src/microservices/saas-microservice/saas.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export class SaasController {
303303
status: 201,
304304
type: CreatedConnectionDTO,
305305
})
306-
@Post('connection/hosted/delete')
306+
@Post('/connection/hosted/delete')
307307
async deleteConnectionForHostedDb(
308308
@Body() deleteConnectionData: DeleteConnectionForHostedDbDto,
309309
): Promise<CreatedConnectionDTO> {
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import { faker } from '@faker-js/faker';
3+
import { INestApplication, ValidationPipe } from '@nestjs/common';
4+
import { Test } from '@nestjs/testing';
5+
import test from 'ava';
6+
import { ValidationError } from 'class-validator';
7+
import cookieParser from 'cookie-parser';
8+
import jwt from 'jsonwebtoken';
9+
import request from 'supertest';
10+
import { ApplicationModule } from '../../../src/app.module.js';
11+
import { AccessLevelEnum } from '../../../src/enums/index.js';
12+
import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js';
13+
import { Cacher } from '../../../src/helpers/cache/cacher.js';
14+
import { DatabaseModule } from '../../../src/shared/database/database.module.js';
15+
import { DatabaseService } from '../../../src/shared/database/database.service.js';
16+
import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-return-user-info.js';
17+
import { TestUtils } from '../../utils/test.utils.js';
18+
19+
let app: INestApplication;
20+
let _testUtils: TestUtils;
21+
let currentTest: string;
22+
23+
test.before(async () => {
24+
const moduleFixture = await Test.createTestingModule({
25+
imports: [ApplicationModule, DatabaseModule],
26+
providers: [DatabaseService, TestUtils],
27+
}).compile();
28+
_testUtils = moduleFixture.get<TestUtils>(TestUtils);
29+
30+
app = moduleFixture.createNestApplication() as any;
31+
app.use(cookieParser());
32+
app.useGlobalPipes(
33+
new ValidationPipe({
34+
exceptionFactory(validationErrors: ValidationError[] = []) {
35+
return new ValidationException(validationErrors);
36+
},
37+
}),
38+
);
39+
await app.init();
40+
app.getHttpServer().listen(0);
41+
});
42+
43+
test.after(async () => {
44+
try {
45+
await Cacher.clearAllCache();
46+
await app.close();
47+
} catch (e) {
48+
console.error('After hosted connection test error: ' + e);
49+
}
50+
});
51+
52+
function generateSaasToken(): string {
53+
const jwtSecret = process.env.MICROSERVICE_JWT_SECRET;
54+
return jwt.sign({ request_id: faker.string.uuid() }, jwtSecret, { expiresIn: '1h' });
55+
}
56+
57+
currentTest = 'POST /saas/connection/hosted';
58+
59+
test.serial(`${currentTest} should create a hosted postgres connection with admin group and permissions`, async (t) => {
60+
try {
61+
const { token } = await registerUserAndReturnUserInfo(app);
62+
63+
// Get user info to obtain userId and companyId
64+
const getUserResult = await request(app.getHttpServer())
65+
.get('/user')
66+
.set('Content-Type', 'application/json')
67+
.set('Cookie', token)
68+
.set('Accept', 'application/json');
69+
t.is(getUserResult.status, 200);
70+
const userInfo = JSON.parse(getUserResult.text);
71+
const userId = userInfo.id;
72+
const companyId = userInfo.company.id;
73+
74+
const saasToken = generateSaasToken();
75+
76+
// Create hosted connection via SaaS endpoint
77+
const createHostedConnectionResult = await request(app.getHttpServer())
78+
.post('/saas/connection/hosted')
79+
.send({
80+
companyId: companyId,
81+
userId: userId,
82+
databaseName: 'postgres',
83+
hostname: 'testPg-e2e-testing',
84+
port: 5432,
85+
username: 'postgres',
86+
password: '123',
87+
})
88+
.set('Authorization', `Bearer ${saasToken}`)
89+
.set('Content-Type', 'application/json')
90+
.set('Accept', 'application/json');
91+
92+
t.is(createHostedConnectionResult.status, 201);
93+
94+
const createdConnection = JSON.parse(createHostedConnectionResult.text);
95+
const connectionId = createdConnection.id;
96+
97+
// Verify connection was created
98+
t.truthy(connectionId);
99+
t.is(createdConnection.type, 'postgres');
100+
t.is(createdConnection.database, 'postgres');
101+
t.is(createdConnection.host, 'testPg-e2e-testing');
102+
t.is(createdConnection.port, 5432);
103+
104+
// Verify admin group was created
105+
t.truthy(createdConnection.groups);
106+
t.is(createdConnection.groups.length, 1);
107+
const adminGroup = createdConnection.groups[0];
108+
t.truthy(adminGroup.id);
109+
t.is(adminGroup.isMain, true);
110+
111+
// Verify connection is accessible via connection groups endpoint
112+
const groupsResponse = await request(app.getHttpServer())
113+
.get(`/connection/groups/${connectionId}`)
114+
.set('Content-Type', 'application/json')
115+
.set('Cookie', token)
116+
.set('Accept', 'application/json');
117+
118+
t.is(groupsResponse.status, 200);
119+
const groups = JSON.parse(groupsResponse.text);
120+
t.is(groups.length, 1);
121+
t.is(groups[0].accessLevel, AccessLevelEnum.edit);
122+
123+
// Verify tables endpoint works with this connection
124+
const findTablesResponse = await request(app.getHttpServer())
125+
.get(`/connection/tables/${connectionId}`)
126+
.set('Content-Type', 'application/json')
127+
.set('Cookie', token)
128+
.set('Accept', 'application/json');
129+
130+
t.is(findTablesResponse.status, 200);
131+
const tables = JSON.parse(findTablesResponse.text);
132+
t.true(Array.isArray(tables));
133+
134+
// Verify user permissions - user should have full access
135+
const permissionsResponse = await request(app.getHttpServer())
136+
.get(`/connection/permissions?connectionId=${connectionId}&groupId=${adminGroup.id}`)
137+
.set('Content-Type', 'application/json')
138+
.set('Cookie', token)
139+
.set('Accept', 'application/json');
140+
141+
t.is(permissionsResponse.status, 200);
142+
const permissions = JSON.parse(permissionsResponse.text);
143+
t.truthy(permissions);
144+
} catch (e) {
145+
console.error('Test error:', e);
146+
throw e;
147+
}
148+
});
149+
150+
test.serial(`${currentTest} should return validation error when required fields are missing`, async (t) => {
151+
try {
152+
const saasToken = generateSaasToken();
153+
154+
const result = await request(app.getHttpServer())
155+
.post('/saas/connection/hosted')
156+
.send({
157+
companyId: faker.string.uuid(),
158+
// missing userId, databaseName, hostname, port, username, password
159+
})
160+
.set('Authorization', `Bearer ${saasToken}`)
161+
.set('Content-Type', 'application/json')
162+
.set('Accept', 'application/json');
163+
164+
t.is(result.status, 400);
165+
} catch (e) {
166+
console.error('Test error:', e);
167+
throw e;
168+
}
169+
});
170+
171+
test.serial(`${currentTest} should return error when userId does not exist`, async (t) => {
172+
try {
173+
const saasToken = generateSaasToken();
174+
175+
const result = await request(app.getHttpServer())
176+
.post('/saas/connection/hosted')
177+
.send({
178+
companyId: faker.string.uuid(),
179+
userId: faker.string.uuid(),
180+
databaseName: 'postgres',
181+
hostname: 'testPg-e2e-testing',
182+
port: 5432,
183+
username: 'postgres',
184+
password: '123',
185+
})
186+
.set('Authorization', `Bearer ${saasToken}`)
187+
.set('Content-Type', 'application/json')
188+
.set('Accept', 'application/json');
189+
190+
t.is(result.status, 500);
191+
} catch (e) {
192+
console.error('Test error:', e);
193+
throw e;
194+
}
195+
});
196+
197+
currentTest = 'POST /saas/connection/hosted/delete';
198+
199+
test.serial(`${currentTest} should delete a hosted connection`, async (t) => {
200+
try {
201+
const { token } = await registerUserAndReturnUserInfo(app);
202+
203+
// Get user info
204+
const getUserResult = await request(app.getHttpServer())
205+
.get('/user')
206+
.set('Content-Type', 'application/json')
207+
.set('Cookie', token)
208+
.set('Accept', 'application/json');
209+
t.is(getUserResult.status, 200);
210+
const userInfo = JSON.parse(getUserResult.text);
211+
const userId = userInfo.id;
212+
const companyId = userInfo.company.id;
213+
214+
const saasToken = generateSaasToken();
215+
216+
// Create a hosted connection first
217+
const createResult = await request(app.getHttpServer())
218+
.post('/saas/connection/hosted')
219+
.send({
220+
companyId: companyId,
221+
userId: userId,
222+
databaseName: 'postgres',
223+
hostname: 'testPg-e2e-testing',
224+
port: 5432,
225+
username: 'postgres',
226+
password: '123',
227+
})
228+
.set('Authorization', `Bearer ${saasToken}`)
229+
.set('Content-Type', 'application/json')
230+
.set('Accept', 'application/json');
231+
232+
t.is(createResult.status, 201);
233+
const createdConnection = JSON.parse(createResult.text);
234+
const connectionId = createdConnection.id;
235+
236+
// Verify connection exists
237+
const connectionsBeforeDelete = await request(app.getHttpServer())
238+
.get('/connections')
239+
.set('Cookie', token)
240+
.set('Content-Type', 'application/json')
241+
.set('Accept', 'application/json');
242+
t.is(connectionsBeforeDelete.status, 200);
243+
const connectionsBefore = JSON.parse(connectionsBeforeDelete.text);
244+
const foundBefore = connectionsBefore.connections.find((c: any) => c.connection.id === connectionId);
245+
t.truthy(foundBefore);
246+
247+
// Delete the hosted connection
248+
const deleteResult = await request(app.getHttpServer())
249+
.post('/saas/connection/hosted/delete')
250+
.send({
251+
companyId: companyId,
252+
hostedDatabaseId: connectionId,
253+
databaseName: 'postgres',
254+
})
255+
.set('Authorization', `Bearer ${saasToken}`)
256+
.set('Content-Type', 'application/json')
257+
.set('Accept', 'application/json');
258+
259+
t.is(deleteResult.status, 201);
260+
261+
// Verify connection was deleted
262+
const connectionsAfterDelete = await request(app.getHttpServer())
263+
.get('/connections')
264+
.set('Cookie', token)
265+
.set('Content-Type', 'application/json')
266+
.set('Accept', 'application/json');
267+
t.is(connectionsAfterDelete.status, 200);
268+
const connectionsAfter = JSON.parse(connectionsAfterDelete.text);
269+
const foundAfter = connectionsAfter.connections.find((c: any) => c.connection.id === connectionId);
270+
t.falsy(foundAfter);
271+
} catch (e) {
272+
console.error('Test error:', e);
273+
throw e;
274+
}
275+
});
276+
277+
test.serial(`${currentTest} should return error when connection does not exist`, async (t) => {
278+
try {
279+
const { token } = await registerUserAndReturnUserInfo(app);
280+
281+
const getUserResult = await request(app.getHttpServer())
282+
.get('/user')
283+
.set('Content-Type', 'application/json')
284+
.set('Cookie', token)
285+
.set('Accept', 'application/json');
286+
const userInfo = JSON.parse(getUserResult.text);
287+
const companyId = userInfo.company.id;
288+
289+
const saasToken = generateSaasToken();
290+
291+
const deleteResult = await request(app.getHttpServer())
292+
.post('/saas/connection/hosted/delete')
293+
.send({
294+
companyId: companyId,
295+
hostedDatabaseId: faker.string.uuid(),
296+
databaseName: 'postgres',
297+
})
298+
.set('Authorization', `Bearer ${saasToken}`)
299+
.set('Content-Type', 'application/json')
300+
.set('Accept', 'application/json');
301+
302+
t.is(deleteResult.status, 404);
303+
} catch (e) {
304+
console.error('Test error:', e);
305+
throw e;
306+
}
307+
});
308+
309+
test.serial('SaaS auth middleware should reject requests without valid token', async (t) => {
310+
try {
311+
const result = await request(app.getHttpServer())
312+
.post('/saas/connection/hosted')
313+
.send({
314+
companyId: faker.string.uuid(),
315+
userId: faker.string.uuid(),
316+
databaseName: 'postgres',
317+
hostname: 'testPg-e2e-testing',
318+
port: 5432,
319+
username: 'postgres',
320+
password: '123',
321+
})
322+
.set('Authorization', 'Bearer invalid-token')
323+
.set('Content-Type', 'application/json')
324+
.set('Accept', 'application/json');
325+
326+
t.is(result.status, 401);
327+
} catch (e) {
328+
console.error('Test error:', e);
329+
throw e;
330+
}
331+
});
332+
333+
test.serial('SaaS auth middleware should reject requests without token', async (t) => {
334+
try {
335+
const result = await request(app.getHttpServer())
336+
.post('/saas/connection/hosted')
337+
.send({
338+
companyId: faker.string.uuid(),
339+
userId: faker.string.uuid(),
340+
databaseName: 'postgres',
341+
hostname: 'testPg-e2e-testing',
342+
port: 5432,
343+
username: 'postgres',
344+
password: '123',
345+
})
346+
.set('Content-Type', 'application/json')
347+
.set('Accept', 'application/json');
348+
349+
t.is(result.status, 401);
350+
} catch (e) {
351+
console.error('Test error:', e);
352+
throw e;
353+
}
354+
});

0 commit comments

Comments
 (0)