fix(dns-manager): throw proper DnsManagerException when hostname not found in Cloudflare

https://sonarly.com/issue/33567?type=bug

When a user checks DNS records for their custom domain, and the hostname is not registered in Cloudflare's custom hostnames despite being set in the workspace database, `refreshHostname()` crashes with a generic "Value not defined" error instead of a proper domain-specific exception.
This commit is contained in:
Sonarly Claude Code
2026-05-02 19:11:16 +00:00
parent 4f439bbe43
commit 36952cf81c
5 changed files with 82 additions and 5 deletions
@@ -6,19 +6,26 @@ import {
DnsManagerException,
DnsManagerExceptionCode,
} from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception';
import {
BaseGraphQLError,
ConflictError,
NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@Catch(DnsManagerException)
export class DnsManagerExceptionFilter implements ExceptionFilter {
catch(exception: DnsManagerException) {
switch (exception.code) {
case DnsManagerExceptionCode.INTERNAL_SERVER_ERROR:
case DnsManagerExceptionCode.HOSTNAME_ALREADY_REGISTERED:
case DnsManagerExceptionCode.HOSTNAME_NOT_REGISTERED:
case DnsManagerExceptionCode.MISSING_PUBLIC_DOMAIN_URL:
throw new NotFoundError(exception);
case DnsManagerExceptionCode.HOSTNAME_ALREADY_REGISTERED:
throw new ConflictError(exception);
case DnsManagerExceptionCode.INVALID_INPUT_DATA:
case DnsManagerExceptionCode.CLOUDFLARE_CLIENT_NOT_INITIALIZED:
case DnsManagerExceptionCode.MULTIPLE_HOSTNAMES_FOUND:
case DnsManagerExceptionCode.MISSING_PUBLIC_DOMAIN_URL:
throw exception;
case DnsManagerExceptionCode.INTERNAL_SERVER_ERROR:
throw new BaseGraphQLError(exception);
default: {
assertUnreachable(exception.code);
}
@@ -288,6 +288,63 @@ describe('DnsManagerService', () => {
});
});
describe('refreshHostname', () => {
it('should throw DnsManagerException when hostname is not found in Cloudflare', async () => {
const hostname = 'example.com';
const cloudflareMock = {
customHostnames: {
list: jest.fn().mockResolvedValueOnce({ result: [] }),
},
};
jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id');
(dnsManagerService as any).cloudflareClient = cloudflareMock;
await expect(
dnsManagerService.refreshHostname(hostname),
).rejects.toThrow(DnsManagerException);
});
it('should refresh and return hostname records when hostname exists', async () => {
const hostname = 'example.com';
const mockResult = {
id: 'custom-id',
hostname,
verification_errors: [],
ssl: {
dcv_delegation_records: [],
},
};
const cloudflareMock = {
customHostnames: {
list: jest.fn().mockResolvedValueOnce({ result: [mockResult] }),
edit: jest.fn().mockResolvedValueOnce({}),
},
};
jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id');
jest
.spyOn(domainServerConfigService, 'getBaseUrl')
.mockReturnValue(new URL('https://front.domain'));
(dnsManagerService as any).cloudflareClient = cloudflareMock;
const result = await dnsManagerService.refreshHostname(hostname);
expect(result).toEqual({
id: 'custom-id',
domain: hostname,
records: expect.any(Array),
});
expect(cloudflareMock.customHostnames.edit).toHaveBeenCalledWith(
'custom-id',
{
zone_id: 'test-zone-id',
ssl: expect.any(Object),
},
);
});
});
describe('updateHostname', () => {
it('should update a custom domain and register a new one', async () => {
const fromHostname = 'old.com';
@@ -138,7 +138,16 @@ export class DnsManagerService {
options,
);
assertIsDefinedOrThrow(publicDomainWithRecords);
assertIsDefinedOrThrow(
publicDomainWithRecords,
new DnsManagerException(
'Hostname not found in Cloudflare',
DnsManagerExceptionCode.HOSTNAME_NOT_REGISTERED,
{
userFriendlyMessage: msg`Domain is not registered in Cloudflare`,
},
),
);
await this.cloudflareClient.customHostnames.edit(
publicDomainWithRecords.id,
@@ -8,6 +8,7 @@ import { PermissionFlagType } from 'twenty-shared/constants';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
import { DnsManagerExceptionFilter } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -31,6 +32,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
)
@UsePipes(ResolverValidationPipe)
@UseFilters(
DnsManagerExceptionFilter,
PublicDomainExceptionFilter,
PreventNestToAutoLogGraphqlErrorsFilter,
)
@@ -23,6 +23,7 @@ import { BillingEntitlementDTO } from 'src/engine/core-modules/billing/dtos/bill
import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
import { DnsManagerExceptionFilter } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
@@ -82,6 +83,7 @@ const OriginHeader = createParamDecorator(
@MetadataResolver(() => WorkspaceEntity)
@UsePipes(ResolverValidationPipe)
@UseFilters(
DnsManagerExceptionFilter,
PreventNestToAutoLogGraphqlErrorsFilter,
PermissionsGraphqlApiExceptionFilter,
)