Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,14 @@ export class DeleteConnectionForHostedDbDto {

@ApiProperty({
description: 'Hosted db entity ID',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsNotEmpty()
@IsString()
@IsUUID()
hostedDatabaseId: string;

@ApiProperty({
description: 'Database name',
example: 'my_database',
example: 'my_database',
})
databaseName: string;
Comment on lines 21 to 25
}
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ export class SaasController {
status: 201,
type: CreatedConnectionDTO,
})
@Post('connection/hosted/delete')
@Post('/connection/hosted/delete')
async deleteConnectionForHostedDb(
@Body() deleteConnectionData: DeleteConnectionForHostedDbDto,
): Promise<CreatedConnectionDTO> {
Expand Down
354 changes: 354 additions & 0 deletions backend/test/ava-tests/saas-tests/hosted-connection-e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { faker } from '@faker-js/faker';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import test from 'ava';
import { ValidationError } from 'class-validator';
import cookieParser from 'cookie-parser';
import jwt from 'jsonwebtoken';
import request from 'supertest';
import { ApplicationModule } from '../../../src/app.module.js';
import { AccessLevelEnum } from '../../../src/enums/index.js';
import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js';
import { Cacher } from '../../../src/helpers/cache/cacher.js';
import { DatabaseModule } from '../../../src/shared/database/database.module.js';
import { DatabaseService } from '../../../src/shared/database/database.service.js';
import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-return-user-info.js';
import { TestUtils } from '../../utils/test.utils.js';

let app: INestApplication;
let _testUtils: TestUtils;
let currentTest: string;

test.before(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [ApplicationModule, DatabaseModule],
providers: [DatabaseService, TestUtils],
}).compile();
_testUtils = moduleFixture.get<TestUtils>(TestUtils);
Comment on lines +1 to +28

app = moduleFixture.createNestApplication() as any;
app.use(cookieParser());
app.useGlobalPipes(
new ValidationPipe({
exceptionFactory(validationErrors: ValidationError[] = []) {
return new ValidationException(validationErrors);
},
}),
);
await app.init();
app.getHttpServer().listen(0);
});

test.after(async () => {
try {
await Cacher.clearAllCache();
await app.close();
} catch (e) {
console.error('After hosted connection test error: ' + e);
}
});

function generateSaasToken(): string {
const jwtSecret = process.env.MICROSERVICE_JWT_SECRET;
return jwt.sign({ request_id: faker.string.uuid() }, jwtSecret, { expiresIn: '1h' });
}
Comment on lines +52 to +55

currentTest = 'POST /saas/connection/hosted';

test.serial(`${currentTest} should create a hosted postgres connection with admin group and permissions`, async (t) => {
try {
const { token } = await registerUserAndReturnUserInfo(app);

// Get user info to obtain userId and companyId
const getUserResult = await request(app.getHttpServer())
.get('/user')
.set('Content-Type', 'application/json')
.set('Cookie', token)
.set('Accept', 'application/json');
t.is(getUserResult.status, 200);
const userInfo = JSON.parse(getUserResult.text);
const userId = userInfo.id;
const companyId = userInfo.company.id;

const saasToken = generateSaasToken();

// Create hosted connection via SaaS endpoint
const createHostedConnectionResult = await request(app.getHttpServer())
.post('/saas/connection/hosted')
.send({
companyId: companyId,
userId: userId,
databaseName: 'postgres',
hostname: 'testPg-e2e-testing',
port: 5432,
username: 'postgres',
password: '123',
})
.set('Authorization', `Bearer ${saasToken}`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(createHostedConnectionResult.status, 201);

const createdConnection = JSON.parse(createHostedConnectionResult.text);
const connectionId = createdConnection.id;

// Verify connection was created
t.truthy(connectionId);
t.is(createdConnection.type, 'postgres');
t.is(createdConnection.database, 'postgres');
t.is(createdConnection.host, 'testPg-e2e-testing');
t.is(createdConnection.port, 5432);

// Verify admin group was created
t.truthy(createdConnection.groups);
t.is(createdConnection.groups.length, 1);
const adminGroup = createdConnection.groups[0];
t.truthy(adminGroup.id);
t.is(adminGroup.isMain, true);

// Verify connection is accessible via connection groups endpoint
const groupsResponse = await request(app.getHttpServer())
.get(`/connection/groups/${connectionId}`)
.set('Content-Type', 'application/json')
.set('Cookie', token)
.set('Accept', 'application/json');

t.is(groupsResponse.status, 200);
const groups = JSON.parse(groupsResponse.text);
t.is(groups.length, 1);
t.is(groups[0].accessLevel, AccessLevelEnum.edit);

// Verify tables endpoint works with this connection
const findTablesResponse = await request(app.getHttpServer())
.get(`/connection/tables/${connectionId}`)
.set('Content-Type', 'application/json')
.set('Cookie', token)
.set('Accept', 'application/json');

t.is(findTablesResponse.status, 200);
const tables = JSON.parse(findTablesResponse.text);
t.true(Array.isArray(tables));

// Verify user permissions - user should have full access
const permissionsResponse = await request(app.getHttpServer())
.get(`/connection/permissions?connectionId=${connectionId}&groupId=${adminGroup.id}`)
.set('Content-Type', 'application/json')
.set('Cookie', token)
.set('Accept', 'application/json');

t.is(permissionsResponse.status, 200);
const permissions = JSON.parse(permissionsResponse.text);
t.truthy(permissions);
} catch (e) {
console.error('Test error:', e);
throw e;
}
});

test.serial(`${currentTest} should return validation error when required fields are missing`, async (t) => {
try {
const saasToken = generateSaasToken();

const result = await request(app.getHttpServer())
.post('/saas/connection/hosted')
.send({
companyId: faker.string.uuid(),
// missing userId, databaseName, hostname, port, username, password
})
.set('Authorization', `Bearer ${saasToken}`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 400);
} catch (e) {
console.error('Test error:', e);
throw e;
}
});

test.serial(`${currentTest} should return error when userId does not exist`, async (t) => {
try {
const saasToken = generateSaasToken();

const result = await request(app.getHttpServer())
.post('/saas/connection/hosted')
.send({
companyId: faker.string.uuid(),
userId: faker.string.uuid(),
databaseName: 'postgres',
hostname: 'testPg-e2e-testing',
port: 5432,
username: 'postgres',
password: '123',
})
.set('Authorization', `Bearer ${saasToken}`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 500);
} catch (e) {
console.error('Test error:', e);
throw e;
}
});

currentTest = 'POST /saas/connection/hosted/delete';

test.serial(`${currentTest} should delete a hosted connection`, async (t) => {
try {
const { token } = await registerUserAndReturnUserInfo(app);

// Get user info
const getUserResult = await request(app.getHttpServer())
.get('/user')
.set('Content-Type', 'application/json')
.set('Cookie', token)
.set('Accept', 'application/json');
t.is(getUserResult.status, 200);
const userInfo = JSON.parse(getUserResult.text);
const userId = userInfo.id;
const companyId = userInfo.company.id;

const saasToken = generateSaasToken();

// Create a hosted connection first
const createResult = await request(app.getHttpServer())
.post('/saas/connection/hosted')
.send({
companyId: companyId,
userId: userId,
databaseName: 'postgres',
hostname: 'testPg-e2e-testing',
port: 5432,
username: 'postgres',
password: '123',
})
.set('Authorization', `Bearer ${saasToken}`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(createResult.status, 201);
const createdConnection = JSON.parse(createResult.text);
const connectionId = createdConnection.id;

// Verify connection exists
const connectionsBeforeDelete = await request(app.getHttpServer())
.get('/connections')
.set('Cookie', token)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');
t.is(connectionsBeforeDelete.status, 200);
const connectionsBefore = JSON.parse(connectionsBeforeDelete.text);
const foundBefore = connectionsBefore.connections.find((c: any) => c.connection.id === connectionId);
t.truthy(foundBefore);

// Delete the hosted connection
const deleteResult = await request(app.getHttpServer())
.post('/saas/connection/hosted/delete')
.send({
companyId: companyId,
hostedDatabaseId: connectionId,
databaseName: 'postgres',
})
.set('Authorization', `Bearer ${saasToken}`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(deleteResult.status, 201);

// Verify connection was deleted
const connectionsAfterDelete = await request(app.getHttpServer())
.get('/connections')
.set('Cookie', token)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');
t.is(connectionsAfterDelete.status, 200);
const connectionsAfter = JSON.parse(connectionsAfterDelete.text);
const foundAfter = connectionsAfter.connections.find((c: any) => c.connection.id === connectionId);
t.falsy(foundAfter);
} catch (e) {
console.error('Test error:', e);
throw e;
}
});

test.serial(`${currentTest} should return error when connection does not exist`, async (t) => {
try {
const { token } = await registerUserAndReturnUserInfo(app);

const getUserResult = await request(app.getHttpServer())
.get('/user')
.set('Content-Type', 'application/json')
.set('Cookie', token)
.set('Accept', 'application/json');
const userInfo = JSON.parse(getUserResult.text);
const companyId = userInfo.company.id;

const saasToken = generateSaasToken();

const deleteResult = await request(app.getHttpServer())
.post('/saas/connection/hosted/delete')
.send({
companyId: companyId,
hostedDatabaseId: faker.string.uuid(),
databaseName: 'postgres',
})
.set('Authorization', `Bearer ${saasToken}`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(deleteResult.status, 404);
} catch (e) {
console.error('Test error:', e);
throw e;
}
});

test.serial('SaaS auth middleware should reject requests without valid token', async (t) => {
try {
const result = await request(app.getHttpServer())
.post('/saas/connection/hosted')
.send({
companyId: faker.string.uuid(),
userId: faker.string.uuid(),
databaseName: 'postgres',
hostname: 'testPg-e2e-testing',
port: 5432,
username: 'postgres',
password: '123',
})
.set('Authorization', 'Bearer invalid-token')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 401);
} catch (e) {
console.error('Test error:', e);
throw e;
}
});

test.serial('SaaS auth middleware should reject requests without token', async (t) => {
try {
const result = await request(app.getHttpServer())
.post('/saas/connection/hosted')
.send({
companyId: faker.string.uuid(),
userId: faker.string.uuid(),
databaseName: 'postgres',
hostname: 'testPg-e2e-testing',
port: 5432,
username: 'postgres',
password: '123',
})
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 401);
} catch (e) {
console.error('Test error:', e);
throw e;
}
});
Loading