Skip to content

Commit cae44d7

Browse files
authored
RI-7778 Add proper error messages for invalid cloud database endpoints (#5266)
* fix(api): add proper error messages for invalid cloud database endpoints References: #RI-7778
1 parent 371f9c4 commit cae44d7

File tree

12 files changed

+161
-1
lines changed

12 files changed

+161
-1
lines changed

.ai/rules/backend.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,43 @@ Use appropriate exception types:
141141
- `ConflictException` - 409
142142
- `InternalServerErrorException` - 500
143143

144+
### Custom Exceptions
145+
146+
**Prefer custom exceptions over generic ones** when you need:
147+
148+
- Consistent error codes for frontend handling
149+
- Specific error messages from constants
150+
- Consistent error structure across the codebase
151+
152+
Create custom exceptions following existing patterns:
153+
154+
```typescript
155+
// src/modules/feature/exceptions/feature-invalid.exception.ts
156+
import {
157+
HttpException,
158+
HttpExceptionOptions,
159+
HttpStatus,
160+
} from '@nestjs/common';
161+
import ERROR_MESSAGES from 'src/constants/error-messages';
162+
import { CustomErrorCodes } from 'src/constants';
163+
164+
export class FeatureInvalidException extends HttpException {
165+
constructor(
166+
message = ERROR_MESSAGES.FEATURE_INVALID,
167+
options?: HttpExceptionOptions,
168+
) {
169+
const response = {
170+
message,
171+
statusCode: HttpStatus.BAD_REQUEST,
172+
error: 'FeatureInvalid',
173+
errorCode: CustomErrorCodes.FeatureInvalid,
174+
};
175+
176+
super(response, response.statusCode, options);
177+
}
178+
}
179+
```
180+
144181
### Error Logging
145182

146183
```typescript

.ai/rules/commits.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ chore: upgrade React to version 18.2
6262
- Atomic changes (one logical change per commit)
6363
- Reference issue/ticket in body
6464
- Explain **why**, not just **what**
65+
- **Keep it concise** - Don't list every file change in the body
6566

6667
```bash
6768
feat(ui): add user profile editing

.ai/rules/testing.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,30 @@ jest.mock('uiSrc/services/api', () => ({
422422
}));
423423
```
424424

425+
### Parameterized Tests with `it.each`
426+
427+
**Use `it.each` for multiple tests with the same body but different inputs:**
428+
429+
```typescript
430+
// ✅ GOOD: Parameterized tests
431+
it.each([
432+
{ description: 'null', value: null },
433+
{ description: 'undefined', value: undefined },
434+
{ description: 'empty string', value: '' },
435+
{ description: 'whitespace only', value: ' ' },
436+
])('should return error when input is $description', async ({ value }) => {
437+
const result = await service.processInput(value);
438+
expect(result.status).toBe('error');
439+
});
440+
```
441+
442+
**Benefits:**
443+
444+
- DRY: Single test body shared across all cases
445+
- Maintainability: Changes to test logic only need to be made once
446+
- Readability: Test cases are clearly defined in a table
447+
- Easier to extend: Adding new test cases is just adding a new row
448+
425449
### Test Edge Cases
426450

427451
Always test:

redisinsight/api/src/constants/custom-error-codes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export enum CustomErrorCodes {
4747
CloudJobNotFound = 11_113,
4848
CloudSubscriptionAlreadyExistsFree = 11_114,
4949
CloudDatabaseImportForbidden = 11_115,
50+
CloudDatabaseEndpointInvalid = 11_116,
5051

5152
// General database errors [11200, 11299]
5253
DatabaseAlreadyExists = 11_200,

redisinsight/api/src/constants/error-messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ export default {
144144
CLOUD_DATABASE_ALREADY_EXISTS_FREE: 'Free database already exists',
145145
CLOUD_DATABASE_IMPORT_FORBIDDEN:
146146
'Adding your Redis Cloud database to Redis Insight is disabled due to a setting restricting database connection management.',
147+
CLOUD_DATABASE_ENDPOINT_INVALID:
148+
'Database endpoint is unavailable. It may still be provisioning or has been disabled.',
147149
CLOUD_PLAN_NOT_FOUND_FREE: 'Unable to find free cloud plan',
148150
CLOUD_SUBSCRIPTION_ALREADY_EXISTS_FREE: 'Free subscription already exists',
149151
COMMON_DEFAULT_IMPORT_ERROR: 'Unable to import default data',

redisinsight/api/src/modules/cloud/autodiscovery/cloud-autodiscovery.service.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptio
2929
import { CloudUserCapiService } from 'src/modules/cloud/user/cloud-user.capi.service';
3030
import { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';
3131
import { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';
32+
import ERROR_MESSAGES from 'src/constants/error-messages';
33+
import { CustomErrorCodes } from 'src/constants';
3234

3335
describe('CloudAutodiscoveryService', () => {
3436
let service: CloudAutodiscoveryService;
@@ -364,5 +366,50 @@ describe('CloudAutodiscoveryService', () => {
364366
mockImportCloudDatabaseResponseFixed,
365367
]);
366368
});
369+
it.each([
370+
{
371+
description: 'null',
372+
publicEndpoint: null,
373+
},
374+
{
375+
description: 'undefined',
376+
publicEndpoint: undefined,
377+
},
378+
])(
379+
'should return error when publicEndpoint is $description',
380+
async ({ publicEndpoint }) => {
381+
cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce({
382+
...mockCloudDatabase,
383+
publicEndpoint,
384+
status: CloudDatabaseStatus.Active,
385+
});
386+
387+
const result = await service.addRedisCloudDatabases(
388+
mockSessionMetadata,
389+
mockCloudCapiAuthDto,
390+
[mockImportCloudDatabaseDto],
391+
);
392+
393+
expect(result).toEqual([
394+
{
395+
...mockImportCloudDatabaseResponse,
396+
status: ActionStatus.Fail,
397+
message: ERROR_MESSAGES.CLOUD_DATABASE_ENDPOINT_INVALID,
398+
error: {
399+
message: ERROR_MESSAGES.CLOUD_DATABASE_ENDPOINT_INVALID,
400+
statusCode: 400,
401+
error: 'CloudDatabaseEndpointInvalid',
402+
errorCode: CustomErrorCodes.CloudDatabaseEndpointInvalid,
403+
},
404+
databaseDetails: {
405+
...mockCloudDatabase,
406+
publicEndpoint,
407+
status: CloudDatabaseStatus.Active,
408+
},
409+
},
410+
]);
411+
expect(databaseService.create).not.toHaveBeenCalled();
412+
},
413+
);
367414
});
368415
});

redisinsight/api/src/modules/cloud/autodiscovery/cloud-autodiscovery.service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Logger,
44
ServiceUnavailableException,
55
} from '@nestjs/common';
6+
import { CloudDatabaseEndpointInvalidException } from 'src/modules/cloud/job/exceptions';
67
import { uniqBy } from 'lodash';
78
import ERROR_MESSAGES from 'src/constants/error-messages';
89
import {
@@ -222,6 +223,16 @@ export class CloudAutodiscoveryService {
222223
databaseDetails: database,
223224
};
224225
}
226+
if (!publicEndpoint) {
227+
const exception = new CloudDatabaseEndpointInvalidException();
228+
return {
229+
...dto,
230+
status: ActionStatus.Fail,
231+
message: exception.message,
232+
error: exception?.getResponse(),
233+
databaseDetails: database,
234+
};
235+
}
225236
const [host, port] = publicEndpoint.split(':');
226237

227238
await this.databaseService.create(sessionMetadata, {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
HttpException,
3+
HttpExceptionOptions,
4+
HttpStatus,
5+
} from '@nestjs/common';
6+
import ERROR_MESSAGES from 'src/constants/error-messages';
7+
import { CustomErrorCodes } from 'src/constants';
8+
9+
export class CloudDatabaseEndpointInvalidException extends HttpException {
10+
constructor(
11+
message = ERROR_MESSAGES.CLOUD_DATABASE_ENDPOINT_INVALID,
12+
options?: HttpExceptionOptions,
13+
) {
14+
const response = {
15+
message,
16+
statusCode: HttpStatus.BAD_REQUEST,
17+
error: 'CloudDatabaseEndpointInvalid',
18+
errorCode: CustomErrorCodes.CloudDatabaseEndpointInvalid,
19+
};
20+
21+
super(response, response.statusCode, options);
22+
}
23+
}

redisinsight/api/src/modules/cloud/job/exceptions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './cloud-database-already-exists-free.exception';
2+
export * from './cloud-database-endpoint-invalid.exception';
23
export * from './cloud-database-import-forbidden.exception';
34
export * from './cloud-database-in-failed-state.exception';
45
export * from './cloud-database-in-unexpected-state.exception';

redisinsight/api/src/modules/cloud/job/jobs/create-free-database.cloud-job.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { WaitForActiveDatabaseCloudJob } from 'src/modules/cloud/job/jobs/wait-f
1515
import { CloudJobName } from 'src/modules/cloud/job/constants';
1616
import { CloudJobStatus, CloudJobStep } from 'src/modules/cloud/job/models';
1717
import {
18+
CloudDatabaseEndpointInvalidException,
1819
CloudDatabaseImportForbiddenException,
1920
CloudJobUnexpectedErrorException,
2021
CloudTaskNoResourceIdException,
@@ -138,6 +139,10 @@ export class CreateFreeDatabaseCloudJob extends CloudJob {
138139

139140
const { publicEndpoint, name, password } = cloudDatabase;
140141

142+
if (!publicEndpoint) {
143+
throw new CloudDatabaseEndpointInvalidException();
144+
}
145+
141146
const [host, port] = publicEndpoint.split(':');
142147

143148
const database = await this.dependencies.databaseService.create(

0 commit comments

Comments
 (0)