Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d0848c0
feat: add Postgres proxy and mock API for E2E testing
Artuomka Apr 3, 2026
a257c97
Merge branch 'main' into backend_pg_proxy
Artuomka Apr 3, 2026
9530d8a
Merge branch 'main' into backend_pg_proxy
Artuomka Apr 6, 2026
35b9c3b
Merge branch 'main' into backend_pg_proxy
Artuomka Apr 7, 2026
709b710
Update subscription level in TEST_CONNECTION to TEAM_PLAN
Artuomka Apr 7, 2026
cb79f15
Add hostedDatabaseId to connection creation and deletion tests
Artuomka Apr 7, 2026
b4ec8e3
Merge branch 'main' into backend_pg_proxy
Artuomka Apr 8, 2026
ba2b842
Refactor connection handling in proxy mock API and update Postgres co…
Artuomka Apr 9, 2026
e0a3bb4
Add mock API endpoints for subscription level management and usage re…
Artuomka Apr 14, 2026
450f0dc
Merge branch 'main' into backend_pg_proxy
Artuomka Apr 15, 2026
0d1a680
Enhance proxy connection handling and usage reporting
Artuomka Apr 15, 2026
dc12ea4
Add tests for query-time budget exhaustion and rejection handling
Artuomka Apr 15, 2026
86bc7e7
Add GetHostedConnectionCredentials feature with DTO and use case impl…
Artuomka Apr 16, 2026
385da27
Update rocketadmin-private-microservice configuration for testing env…
Artuomka Apr 17, 2026
13ba6c7
Refactor GetHostedConnectionCredentials implementation to use findOne…
Artuomka Apr 17, 2026
bfa5e85
Merge branch 'main' into backend_pg_proxy
Artuomka Apr 17, 2026
d921aa1
Refactor docker-compose configuration: streamline healthcheck command…
Artuomka Apr 20, 2026
e0e5a0c
Merge branch 'main' into backend_pg_proxy
Artuomka Apr 21, 2026
e464e21
Merge branch 'main' into backend_pg_proxy
Artuomka Apr 23, 2026
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
1 change: 1 addition & 0 deletions backend/src/common/data-injection.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export enum UseCaseType {
SAAS_DELETE_CONNECTION_FOR_HOSTED_DB = 'SAAS_DELETE_CONNECTION_FOR_HOSTED_DB',
SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD = 'SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD',
SAAS_GET_CONNECTIONS_INFO_BY_IDS = 'SAAS_GET_CONNECTIONS_INFO_BY_IDS',
SAAS_GET_HOSTED_CONNECTION_CREDENTIALS = 'SAAS_GET_HOSTED_CONNECTION_CREDENTIALS',

INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',
VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class GetHostedConnectionCredentialsDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This DTO only accepts hostedDatabaseId. Since the SaaS auth middleware validates only that a JWT is signed (no tenant scoping), an internal caller could fetch credentials for any connection ID. Consider requiring companyId (or other tenant identifier) and validating the connection belongs to that tenant before returning credentials.

Suggested change
@IsNotEmpty()
@IsNotEmpty()
companyId: string;
@ApiProperty()
@IsString()
@IsNotEmpty()

Copilot uses AI. Check for mistakes.
hostedDatabaseId: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';

export class HostedConnectionCredentialsRO {
@ApiProperty()
connectionId: string;

@ApiProperty()
host: string;

@ApiProperty()
port: number;

@ApiProperty()
database: string;

@ApiProperty()
username: string;

@ApiProperty()
password: string;
Comment on lines +19 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for logging interceptors / middlewares that might serialize responses or DTOs.
rg -nP --type=ts -C3 '\b(LoggerInterceptor|intercept\s*\(|logResponse|res\.body|response\.body)\b'
# Confirm no toJSON/serializer strips sensitive fields yet.
rg -nP --type=ts -C2 '\bHostedConnectionCredentialsRO\b'

Repository: rocket-admin/rocketadmin

Length of output: 28104


🏁 Script executed:

cat -n backend/src/microservices/saas-microservice/data-structures/hosted-connection-credentials.ro.ts

Repository: rocket-admin/rocketadmin

Length of output: 579


🏁 Script executed:

# Check for serialization/redaction interceptors or utilities
rg -nP --type=ts -C3 '(ClassSerializerInterceptor|Exclude|Transform|toJSON|serializer)' backend/src/interceptors/

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Check SentryInterceptor for password/sensitive field handling
cat -n backend/src/interceptors/sentry.interceptor.ts

Repository: rocket-admin/rocketadmin

Length of output: 1424


🏁 Script executed:

# Look for password/sensitive field redaction lists or patterns
rg -nP --type=ts 'password|redact|sensitive|mask' backend/src/interceptors/

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Check the use case implementation
cat -n backend/src/microservices/saas-microservice/use-cases/get-hosted-connection-credentials.use.case.ts

Repository: rocket-admin/rocketadmin

Length of output: 1958


Add @Exclude() decorator to redact the password field from serialization, or document the security handling.

The password field is returned plaintext from this endpoint without any automatic redaction. While this may be required for the SaaS-to-proxy flow, there are no serialization interceptors (e.g., ClassSerializerInterceptor with @Exclude()) or middleware configured to strip this field from logs, error tracking, or cached responses. Add either:

  • @Exclude() decorator on the password property if it should never be serialized, or
  • A code comment explaining why the field is exposed and confirming that callers and middleware handle it securely (no caching, request/response logging redaction, etc.).

Without explicit redaction or documentation, future changes risk accidentally leaking credentials.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/microservices/saas-microservice/data-structures/hosted-connection-credentials.ro.ts`
around lines 19 - 20, The password property on HostedConnectionCredentials (the
password field currently annotated only with `@ApiProperty`()) must be either
redacted from serialization or explicitly documented: add the `@Exclude`()
decorator to the password property and ensure the class is serialized via
ClassSerializerInterceptor, or add a clear code comment on the
HostedConnectionCredentials class/property explaining why plaintext password is
exposed and list required safeguards (no caching, response/log redaction,
error-tracking filtering). Update related serialization configuration to
guarantee `@Exclude`() is honored if you choose that approach.


@ApiProperty()
is_frozen: boolean;
}
18 changes: 18 additions & 0 deletions backend/src/microservices/saas-microservice/saas.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { CreateConnectionForHostedDbDto } from './data-structures/create-connect
import { DeleteConnectionForHostedDbDto } from './data-structures/delete-connection-for-hosted-db.dto.js';
import { FoundConnectionInfoRO } from './data-structures/found-connection-info.ro.js';
import { GetConnectionsInfoByIdsDS } from './data-structures/get-connections-info-by-ids.ds.js';
import { GetHostedConnectionCredentialsDto } from './data-structures/get-hosted-connection-credentials.dto.js';
import { HostedConnectionCredentialsRO } from './data-structures/hosted-connection-credentials.ro.js';
import { RegisterCompanyWebhookDS } from './data-structures/register-company.ds.js';
import { RegisteredCompanyDS } from './data-structures/registered-company.ds.js';
import { SaasRegisterUserWithGithub } from './data-structures/saas-register-user-with-github.js';
Expand All @@ -41,6 +43,7 @@ import {
IDeleteConnectionForHostedDb,
IFreezeConnectionsInCompany,
IGetConnectionsInfoByIds,
IGetHostedConnectionCredentials,
IGetUserInfo,
ILoginUserWithGitHub,
ILoginUserWithGoogle,
Expand Down Expand Up @@ -100,6 +103,8 @@ export class SaasController {
private readonly updateHostedConnectionPasswordUseCase: IUpdateHostedConnectionPassword,
@Inject(UseCaseType.SAAS_GET_CONNECTIONS_INFO_BY_IDS)
private readonly getConnectionsInfoByIdsUseCase: IGetConnectionsInfoByIds,
@Inject(UseCaseType.SAAS_GET_HOSTED_CONNECTION_CREDENTIALS)
private readonly getHostedConnectionCredentialsUseCase: IGetHostedConnectionCredentials,
) {}

@ApiOperation({ summary: 'Company registered webhook' })
Expand Down Expand Up @@ -344,4 +349,17 @@ export class SaasController {
): Promise<Array<FoundConnectionInfoRO>> {
return await this.getConnectionsInfoByIdsUseCase.execute(connectionsData);
}

@ApiOperation({ summary: 'Get decrypted credentials for a hosted connection' })
@ApiBody({ type: GetHostedConnectionCredentialsDto })
@ApiResponse({
status: 200,
type: HostedConnectionCredentialsRO,
})
@Post('/connection/hosted/credentials')
async getHostedConnectionCredentials(
@Body() data: GetHostedConnectionCredentialsDto,
): Promise<HostedConnectionCredentialsRO> {
return await this.getHostedConnectionCredentialsUseCase.execute(data);
}
}
6 changes: 6 additions & 0 deletions backend/src/microservices/saas-microservice/saas.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DeleteConnectionForHostedDbUseCase } from './use-cases/delete-connectio
import { FreezeConnectionsInCompanyUseCase } from './use-cases/freeze-connections-in-company.use.case.js';
import { GetConnectionsInfoByIdsUseCase } from './use-cases/get-connections-info-by-ids.use.case.js';
import { GetFullCompanyInfoByUserIdUseCase } from './use-cases/get-full-company-info-by-user-id.use.case.js';
import { GetHostedConnectionCredentialsUseCase } from './use-cases/get-hosted-connection-credentials.use.case.js';
import { GetUserInfoUseCase } from './use-cases/get-user-info.use.case.js';
import { GetUsersCountInCompanyByIdUseCase } from './use-cases/get-users-count-in-company.use.case.js';
import { GetUsersInfosByEmailUseCase } from './use-cases/get-users-infos-by-email.use.case.js';
Expand Down Expand Up @@ -105,6 +106,10 @@ import { UpdateHostedConnectionPasswordUseCase } from './use-cases/update-hosted
provide: UseCaseType.SAAS_GET_CONNECTIONS_INFO_BY_IDS,
useClass: GetConnectionsInfoByIdsUseCase,
},
{
provide: UseCaseType.SAAS_GET_HOSTED_CONNECTION_CREDENTIALS,
useClass: GetHostedConnectionCredentialsUseCase,
},
SignInAuditService,
],
controllers: [SaasController],
Expand All @@ -131,6 +136,7 @@ export class SaasModule {
{ path: 'saas/connection/hosted', method: RequestMethod.POST },
{ path: 'saas/connection/hosted/delete', method: RequestMethod.POST },
{ path: 'saas/connection/hosted/password', method: RequestMethod.POST },
{ path: 'saas/connection/hosted/credentials', method: RequestMethod.POST },
{ path: 'saas/connections/info', method: RequestMethod.POST },
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
import AbstractUseCase from '../../../common/abstract-use.case.js';
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
import { BaseType } from '../../../common/data-injection.tokens.js';
import { Messages } from '../../../exceptions/text/messages.js';
import { GetHostedConnectionCredentialsDto } from '../data-structures/get-hosted-connection-credentials.dto.js';
import { HostedConnectionCredentialsRO } from '../data-structures/hosted-connection-credentials.ro.js';
import { IGetHostedConnectionCredentials } from './saas-use-cases.interface.js';

@Injectable({ scope: Scope.REQUEST })
export class GetHostedConnectionCredentialsUseCase
extends AbstractUseCase<GetHostedConnectionCredentialsDto, HostedConnectionCredentialsRO>
implements IGetHostedConnectionCredentials
{
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
) {
super();
}

protected async implementation(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO> {
const connection = await this._dbContext.connectionRepository.findOne({
where: { id: inputData.hostedDatabaseId },
});
Comment on lines +22 to +25
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connectionRepository.findOne() returns the ConnectionEntity with encrypted credentials (host/username/password/database) because no decrypt helper is called. As a result this endpoint will return encrypted values instead of the advertised decrypted credentials. Use a repository method that decrypts (e.g., connectionRepository.findOneById() / findOneConnection() / findAndDecryptConnection() as appropriate) before building the response, and consider how to handle masterEncryption connections.

Suggested change
protected async implementation(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO> {
const connection = await this._dbContext.connectionRepository.findOne({
where: { id: inputData.hostedDatabaseId },
});
private async findHostedConnectionWithDecryptedCredentials(hostedDatabaseId: string) {
const connectionRepository = this._dbContext.connectionRepository as typeof this._dbContext.connectionRepository & {
findOneById?: (id: string) => Promise<any>;
findOneConnection?: (id: string) => Promise<any>;
findAndDecryptConnection?: (id: string) => Promise<any>;
};
if (typeof connectionRepository.findAndDecryptConnection === 'function') {
return connectionRepository.findAndDecryptConnection(hostedDatabaseId);
}
if (typeof connectionRepository.findOneConnection === 'function') {
return connectionRepository.findOneConnection(hostedDatabaseId);
}
if (typeof connectionRepository.findOneById === 'function') {
return connectionRepository.findOneById(hostedDatabaseId);
}
throw new Error('Connection repository does not expose a decryption-aware lookup method.');
}
protected async implementation(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO> {
const connection = await this.findHostedConnectionWithDecryptedCredentials(inputData.hostedDatabaseId);

Copilot uses AI. Check for mistakes.
if (!connection) {
throw new NotFoundException(Messages.CONNECTION_NOT_FOUND);
}
Comment on lines +22 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

No tenant/company scoping on lookup.

connectionRepository.findOne({ where: { id: inputData.hostedDatabaseId } }) returns any connection by id, regardless of which company the caller belongs to. The DTO only carries hostedDatabaseId. Even though this route is behind the SaaS-to-SaaS JWT, a compromised or misbehaving caller (or a future non-SaaS caller) could enumerate UUIDs to exfiltrate credentials for any hosted DB in the system.

Recommend either: (a) require and validate a companyId in the DTO and filter where: { id, company: { id: companyId } }, or (b) explicitly filter to connections that are actually hosted (hosted: true / equivalent flag) so this path can't return credentials for customer-owned BYOD connections.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/microservices/saas-microservice/use-cases/get-hosted-connection-credentials.use.case.ts`
around lines 22 - 28, The lookup in implementation
(get-hosted-connection-credentials.use.case.ts -> protected async
implementation) uses connectionRepository.findOne({ where: { id:
inputData.hostedDatabaseId } }) without scoping to tenant/company or hosted-only
connections; update the code and DTO to prevent cross-tenant exposure by either
(A) adding companyId to GetHostedConnectionCredentialsDto and changing the query
to connectionRepository.findOne({ where: { id: inputData.hostedDatabaseId,
company: { id: inputData.companyId } } }) with validation that the caller's
company matches, or (B) at minimum filter for hosted connections only by using
connectionRepository.findOne({ where: { id: inputData.hostedDatabaseId, hosted:
true } }) so only system-hosted DBs can be returned; apply the chosen change in
the implementation, adjust any DTO/validator usages, and keep error handling
(NotFoundException) the same if no record matches.


return {
connectionId: connection.id,
host: connection.host,
port: connection.port,
database: connection.database,
username: connection.username,
password: connection.password,
is_frozen: connection.is_frozen,
};
}
Comment on lines +22 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) See how hosted-db connection creation stores the password (encryption?).
fd -t f 'create-connection-for-hosted-db.use.case.ts' | xargs -I{} sed -n '1,220p' {}

# 2) Look for decryption helpers used elsewhere when returning connection credentials.
rg -nP -C2 '\b(decrypt|encrypt)\w*\s*\(' --type=ts -g 'backend/src/**'

# 3) See whether master/azure/connection encryption is applied on hosted connection writes.
rg -nP -C3 '(masterEncryption|azure_encryption|encryptData|decryptData)' --type=ts -g 'backend/src/**'

Repository: rocket-admin/rocketadmin

Length of output: 50381


🏁 Script executed:

# Check ConnectionEntity definition for BeforeInsert/AfterLoad hooks
fd -t f 'connection.entity.ts' | xargs -I{} cat -n {} | head -150

# Check if there's connection-level encryption happening at entity level
rg -B5 -A5 'BeforeInsert|BeforeUpdate|AfterLoad' --type=ts -g 'backend/src/**/connection.entity.ts'

# Verify if masterEncryption should be true for hosted connections
rg -C5 'hosted.*masterEncryption|masterEncryption.*hosted' --type=ts -g 'backend/src/**'

Repository: rocket-admin/rocketadmin

Length of output: 8372


🏁 Script executed:

# Get the complete BeforeInsert hook implementation
rg -A30 'encryptCredentialsAndGenerateNanoid' --type=ts -g 'backend/src/**/connection.entity.ts'

# Also check how decryptConnectionsCredentialsAsync works
rg -B5 -A15 'decryptConnectionsCredentialsAsync' --type=ts -g 'backend/src/**' | head -100

Repository: rocket-admin/rocketadmin

Length of output: 16265


🏁 Script executed:

# Check the actual current implementation of get-hosted-connection-credentials.use.case.ts
cat -n backend/src/microservices/saas-microservice/use-cases/get-hosted-connection-credentials.use.case.ts

Repository: rocket-admin/rocketadmin

Length of output: 1958


Add missing decryption step before returning credentials.

The connection password is encrypted by ConnectionEntity's @BeforeInsert hook (via Encryptor.encryptData()), but this use case returns the encrypted value directly without decrypting. The API advertises "decrypted credentials" while returning ciphertext, causing authentication to fail silently.

Follow the pattern in UpdateHostedConnectionPasswordUseCase by calling decryptConnectionsCredentialsAsync() on the fetched connection before returning:

import { decryptConnectionCredentialsAsync } from '../../../entities/connection/utils/decrypt-connection-credentials-async.js';

protected async implementation(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO> {
    const connection = await this._dbContext.connectionRepository.findOne({
        where: { id: inputData.hostedDatabaseId },
    });
    if (!connection) {
        throw new NotFoundException(Messages.CONNECTION_NOT_FOUND);
    }

    await decryptConnectionCredentialsAsync(connection);

    return { ... };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/microservices/saas-microservice/use-cases/get-hosted-connection-credentials.use.case.ts`
around lines 22 - 39, The implementation of
GetHostedConnectionCredentialsUseCase returns the encrypted password; fetch the
connection via _dbContext.connectionRepository.findOne (as already done), then
call decryptConnectionCredentialsAsync(connection) (the same helper used by
UpdateHostedConnectionPasswordUseCase) before constructing and returning the
HostedConnectionCredentialsRO so the password is decrypted; add the appropriate
import for decryptConnectionCredentialsAsync from
entities/connection/utils/decrypt-connection-credentials-async.js and keep the
existing NotFoundException(Messages.CONNECTION_NOT_FOUND) check.

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { DeleteConnectionForHostedDbDto } from '../data-structures/delete-connec
import { FoundConnectionInfoRO } from '../data-structures/found-connection-info.ro.js';
import { FreezeConnectionsInCompanyDS } from '../data-structures/freeze-connections-in-company.ds.js';
import { GetConnectionsInfoByIdsDS } from '../data-structures/get-connections-info-by-ids.ds.js';
import { GetHostedConnectionCredentialsDto } from '../data-structures/get-hosted-connection-credentials.dto.js';
import { HostedConnectionCredentialsRO } from '../data-structures/hosted-connection-credentials.ro.js';
import { GetUserInfoByIdDS } from '../data-structures/get-user-info.ds.js';
import { GetUsersInfosByEmailDS } from '../data-structures/get-users-infos-by-email.ds.js';
import { RegisterCompanyWebhookDS } from '../data-structures/register-company.ds.js';
Expand Down Expand Up @@ -88,3 +90,7 @@ export interface IUpdateHostedConnectionPassword {
export interface IGetConnectionsInfoByIds {
execute(inputData: GetConnectionsInfoByIdsDS): Promise<Array<FoundConnectionInfoRO>>;
}

export interface IGetHostedConnectionCredentials {
execute(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO>;
}
32 changes: 15 additions & 17 deletions backend/test/ava-tests/saas-tests/hosted-connection-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi
.send({
companyId: companyId,
userId: userId,
hostedDatabaseId: faker.string.uuid(),
databaseName: 'postgres',
hostname: 'testPg-e2e-testing',
port: 5432,
Expand All @@ -89,24 +90,14 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(createHostedConnectionResult.status, 201);

const createdConnection = JSON.parse(createHostedConnectionResult.text);
const connectionId = createdConnection.id;
console.log('🚀 ~ createdConnection:', createdConnection);

t.is(createHostedConnectionResult.status, 201);
const connectionId = createdConnection.connectionId;

// 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())
Expand All @@ -119,6 +110,7 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi
const groups = JSON.parse(groupsResponse.text);
t.is(groups.length, 1);
t.is(groups[0].accessLevel, AccessLevelEnum.edit);
const adminGroup = groups[0];

// Verify tables endpoint works with this connection
const findTablesResponse = await request(app.getHttpServer())
Expand All @@ -132,8 +124,9 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi
t.true(Array.isArray(tables));

// Verify user permissions - user should have full access
const groupId = adminGroup.group.id;
const permissionsResponse = await request(app.getHttpServer())
.get(`/connection/permissions?connectionId=${connectionId}&groupId=${adminGroup.id}`)
.get(`/connection/permissions?connectionId=${connectionId}&groupId=${groupId}`)
.set('Content-Type', 'application/json')
.set('Cookie', token)
.set('Accept', 'application/json');
Expand Down Expand Up @@ -177,6 +170,7 @@ test.serial(`${currentTest} should return error when userId does not exist`, asy
.send({
companyId: faker.string.uuid(),
userId: faker.string.uuid(),
hostedDatabaseId: faker.string.uuid(),
databaseName: 'postgres',
hostname: 'testPg-e2e-testing',
port: 5432,
Expand All @@ -187,6 +181,8 @@ test.serial(`${currentTest} should return error when userId does not exist`, asy
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

const responseBody = JSON.parse(result.text);
console.log('🚀 ~ responseBody:', responseBody);
t.is(result.status, 500);
Comment on lines +184 to 186
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Asserting HTTP 500 codifies a server-error response for an invalid user id.

A missing/unknown userId is a caller/input problem and should surface as a 4xx (e.g. 404 Not Found or 400 Bad Request), not 500 Internal Server Error. Pinning the test at 500 freezes the buggy behavior in place and will mask a future fix in the use case.

Consider either fixing the upstream use case to throw NotFoundException (consistent with how get-hosted-connection-credentials.use.case.ts handles missing connections) and asserting 404 here, or at minimum leaving a TODO so this doesn't ossify.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/test/ava-tests/saas-tests/hosted-connection-e2e.test.ts` around lines
184 - 186, The test in hosted-connection-e2e.test.ts is asserting a 500 for an
unknown/missing userId (result.status === 500), which should be a 4xx; update
the test to expect the correct client error (preferably 404 to match how
get-hosted-connection-credentials.use.case.ts throws NotFoundException) by
changing the assertion to expect 404 (or, if you cannot update upstream yet,
replace the hard 500 assertion with a TODO comment and a looser check that the
response indicates a client error range) so the test no longer locks in a
server-error behavior.

} catch (e) {
console.error('Test error:', e);
Expand Down Expand Up @@ -219,6 +215,7 @@ test.serial(`${currentTest} should delete a hosted connection`, async (t) => {
.send({
companyId: companyId,
userId: userId,
hostedDatabaseId: faker.string.uuid(),
databaseName: 'postgres',
hostname: 'testPg-e2e-testing',
port: 5432,
Expand All @@ -229,9 +226,10 @@ test.serial(`${currentTest} should delete a hosted connection`, async (t) => {
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(createResult.status, 201);
const createdConnection = JSON.parse(createResult.text);
const connectionId = createdConnection.id;
console.log('🚀 ~ createdConnection:', createdConnection);
t.is(createResult.status, 201);
const connectionId = createdConnection.connectionId;

// Verify connection exists
const connectionsBeforeDelete = await request(app.getHttpServer())
Expand Down
Loading
Loading