Skip to content
Open
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
86 changes: 44 additions & 42 deletions app/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,52 +51,54 @@ import { EnvironmentParityModule } from "./environment-parity/environment-parity
import { IndexerLagModule } from "./indexer-lag";
import { SupportBundleModule } from "./support-bundle/support-bundle.module";
import { OperationsModule } from "./operations/operations.module";
import { DeploymentSyncModule } from "./deployment-sync/deployment-sync.module";

type AppImport =
| Type<unknown>
| DynamicModule
| Promise<DynamicModule>
| ForwardReference<unknown>;
| Type<unknown>
| DynamicModule
| Promise<DynamicModule>
| ForwardReference<unknown>;

@Module({
imports: ((): AppImport[] => {
const baseImports: AppImport[] = [
SentryModule,
AppConfigModule,
ScheduleModule.forRoot(),
EventEmitterModule.forRoot({
wildcard: true,
delimiter: ".",
}),
ThrottlerModule.forRoot(throttlerModuleProfiles),
SupabaseModule,
HealthModule,
AssetMetadataModule,
StellarModule,
UsernamesModule,
MetricsModule,
AnalyticsModule,
LinksModule,
ScamAlertsModule,
TransactionsModule,
PaymentsModule,
IngestionModule,
ApiKeysModule,
MarketplaceModule,
FiatRampsModule,
RefundsModule,
ExportsModule,
JobQueueModule,
AuditModule,
ContractsModule,
FeatureFlagsModule,
PrivacyModule,
SorobanToolingModule,
EnvironmentParityModule,
IndexerLagModule,
SupportBundleModule,
OperationsModule,
];
imports: ((): AppImport[] => {
const baseImports: AppImport[] = [
SentryModule,
AppConfigModule,
ScheduleModule.forRoot(),
EventEmitterModule.forRoot({
wildcard: true,
delimiter: ".",
}),
ThrottlerModule.forRoot(throttlerModuleProfiles),
SupabaseModule,
HealthModule,
AssetMetadataModule,
StellarModule,
UsernamesModule,
MetricsModule,
AnalyticsModule,
LinksModule,
ScamAlertsModule,
TransactionsModule,
PaymentsModule,
IngestionModule,
ApiKeysModule,
MarketplaceModule,
FiatRampsModule,
RefundsModule,
ExportsModule,
JobQueueModule,
AuditModule,
ContractsModule,
FeatureFlagsModule,
PrivacyModule,
SorobanToolingModule,
EnvironmentParityModule,
IndexerLagModule,
SupportBundleModule,
OperationsModule,
DeploymentSyncModule,
];

try {
const supabaseUrl = process.env.SUPABASE_URL ?? "";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { DeploymentSyncController } from '../deployment-sync.controller';
import { DeploymentSyncService } from '../deployment-sync.service';
import { DeploymentWebhookPayloadDto } from '../dto/webhook-payload.dto';
import { DeploymentResponseDto } from '../dto/deployment-response.dto';
import { ApiKeyGuard } from '../../auth/guards/api-key.guard';

describe('DeploymentSyncController', () => {
let controller: DeploymentSyncController;
let service: jest.Mocked<DeploymentSyncService>;

const mockResponse: DeploymentResponseDto = {
id: 'uuid-123',
branchName: 'feat/be-branch-metadata-sync',
prNumber: 42,
commitSha: 'a1b2c3d4e5f6g7h8i9j0a1b2c3d4e5f6g7h8i9j0',
previewUrl: 'https://quickex-preview-pr-42.vercel.app',
status: 'success',
eventTimestamp: '2026-06-27T08:00:00.000Z',
createdAt: '2026-06-27T08:00:05.000Z',
updatedAt: '2026-06-27T08:00:05.000Z',
};

beforeEach(async () => {
const mockSyncService = {
syncDeployment: jest.fn().mockResolvedValue(mockResponse),
getDeploymentByBranch: jest.fn().mockResolvedValue(mockResponse),
getDeploymentsByPr: jest.fn().mockResolvedValue([mockResponse]),
};

const module: TestingModule = await Test.createTestingModule({
controllers: [DeploymentSyncController],
providers: [
{
provide: DeploymentSyncService,
useValue: mockSyncService,
},
],
})
.overrideGuard(ApiKeyGuard)
.useValue({ canActivate: () => true })
.compile();

controller = module.get<DeploymentSyncController>(DeploymentSyncController);
service = module.get(DeploymentSyncService) as jest.Mocked<DeploymentSyncService>;
});

afterEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('syncDeployment', () => {
it('should call service.syncDeployment and return the result', async () => {
const payload: DeploymentWebhookPayloadDto = {
branchName: 'feat/be-branch-metadata-sync',
prNumber: 42,
commitSha: 'a1b2c3d4e5f6g7h8i9j0a1b2c3d4e5f6g7h8i9j0',
previewUrl: 'https://quickex-preview-pr-42.vercel.app',
status: 'success',
eventTimestamp: '2026-06-27T08:00:00.000Z',
};

const result = await controller.syncDeployment(payload);

expect(service.syncDeployment).toHaveBeenCalledWith(payload);
expect(result).toEqual(mockResponse);
});
});

describe('getDeploymentByBranch', () => {
it('should call service.getDeploymentByBranch and return the result', async () => {
const result = await controller.getDeploymentByBranch('feat/be-branch-metadata-sync');

expect(service.getDeploymentByBranch).toHaveBeenCalledWith('feat/be-branch-metadata-sync');
expect(result).toEqual(mockResponse);
});

it('should propagate service NotFoundException', async () => {
service.getDeploymentByBranch.mockRejectedValueOnce(new NotFoundException('Not found'));

await expect(controller.getDeploymentByBranch('non-existent')).rejects.toThrow(NotFoundException);
});
});

describe('getDeploymentsByPr', () => {
it('should call service.getDeploymentsByPr and return the result', async () => {
const result = await controller.getDeploymentsByPr(42);

expect(service.getDeploymentsByPr).toHaveBeenCalledWith(42);
expect(result).toEqual([mockResponse]);
});

it('should propagate service NotFoundException', async () => {
service.getDeploymentsByPr.mockRejectedValueOnce(new NotFoundException('Not found'));

await expect(controller.getDeploymentsByPr(999)).rejects.toThrow(NotFoundException);
});
});
});
Loading
Loading