diff --git a/src/modules/platform-api/platform-api.controller.spec.ts b/src/modules/platform-api/platform-api.controller.spec.ts new file mode 100644 index 0000000..c235dd3 --- /dev/null +++ b/src/modules/platform-api/platform-api.controller.spec.ts @@ -0,0 +1,107 @@ +// These service modules pull in gRPC client-config modules that read +// process.env at load time; mock them (hoisted before imports) so the spec +// needs no runtime env. Each mock severs an entire import subtree and still +// provides a class usable as a DI token. +jest.mock('../customers/customers.service', () => ({ CustomersService: class {} })); +jest.mock('../catalog/catalog.service', () => ({ CatalogService: class {} })); +jest.mock('../inputs/inputs.service', () => ({ InputsService: class {} })); + +import { Test, TestingModule } from '@nestjs/testing'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { PlatformApiController } from './platform-api.controller'; +import { PlatformApiService } from './platform-api.service'; +import { ElasticsearchService } from '../../services/elasticsearch'; +import { DynamoDBService } from '../../services/dynamodb'; +import { CustomersService } from '../customers/customers.service'; +import { CatalogService } from '../catalog/catalog.service'; +import { InputsService } from '../inputs/inputs.service'; + +const logger = { + info: (...args) => args, + error: (...args) => args, +}; + +const mockUser: any = { + customer_id: 'c1', + customer_name: 'cust', + user_id: 'u1', +}; + +describe('PlatformApiController - deleteTable', () => { + let controller: PlatformApiController; + let platformApiService: { proxy: jest.Mock }; + let inputsService: { markTableDeleted: jest.Mock; unmarkTableDeleted: jest.Mock }; + + beforeEach(async () => { + platformApiService = { proxy: jest.fn() }; + inputsService = { + markTableDeleted: jest.fn(), + unmarkTableDeleted: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [PlatformApiController], + providers: [ + { provide: PlatformApiService, useValue: platformApiService }, + { provide: ElasticsearchService, useValue: {} }, + { provide: DynamoDBService, useValue: {} }, + { provide: CustomersService, useValue: {} }, + { provide: CatalogService, useValue: {} }, + { provide: InputsService, useValue: inputsService }, + { provide: DadosferaLogger, useValue: { logger } }, + ], + }).compile(); + + controller = module.get(PlatformApiController); + }); + + it('CDC removal reconfigures the connector', async () => { + inputsService.markTableDeleted.mockResolvedValue({ is_deleted: true, deleted_at: 't' }); + platformApiService.proxy.mockImplementation((method: string, path: string) => { + if (method === 'GET') { + return Promise.resolve({ + jobs: [{ job_id: 'p_0', input: { connector: 'cdc', table_name: 'pedidos' } }], + }); + } + return Promise.resolve({}); + }); + + await controller.deleteTable('pid', 'iid', { table_name: 'pedidos' }, mockUser); + + expect(platformApiService.proxy).toHaveBeenCalledWith( + 'DELETE', + '/pipeline/pid/jobs', + mockUser, + { job_ids: ['p_0'], delete_snowflake_tables: false }, + ); + const deletedViaJobsRoute = platformApiService.proxy.mock.calls.some( + ([method, path]: any[]) => method === 'DELETE' && path === '/jobs/p_0', + ); + expect(deletedViaJobsRoute).toBe(false); + }); + + it('batch removal unchanged', async () => { + inputsService.markTableDeleted.mockResolvedValue({ is_deleted: true, deleted_at: 't' }); + platformApiService.proxy.mockImplementation((method: string, path: string) => { + if (method === 'GET') { + return Promise.resolve({ + jobs: [{ job_id: 'p_0', input: { connector: 'jdbc', table_name: 'pedidos' } }], + }); + } + return Promise.resolve({}); + }); + + await controller.deleteTable('pid', 'iid', { table_name: 'pedidos' }, mockUser); + + expect(platformApiService.proxy).toHaveBeenCalledWith( + 'DELETE', + '/jobs/p_0', + mockUser, + ); + const reconfiguredConnector = platformApiService.proxy.mock.calls.some( + ([method, path]: any[]) => method === 'DELETE' && path === '/pipeline/pid/jobs', + ); + expect(reconfiguredConnector).toBe(false); + }); +}); diff --git a/src/modules/platform-api/platform-api.controller.ts b/src/modules/platform-api/platform-api.controller.ts index 503d117..b10b09e 100644 --- a/src/modules/platform-api/platform-api.controller.ts +++ b/src/modules/platform-api/platform-api.controller.ts @@ -964,7 +964,18 @@ export class PlatformApiController { if (!job) throw new NotFoundException(`Job for table '${tableName}' not found in pipeline`); this.logger.info('deleteTable: deleting job from platform-api', { jobId: job.job_id }); - await this.platformApiService.proxy('DELETE', `/jobs/${job.job_id}`, user); + if (job.input?.connector === 'cdc') { + // CDC: reconfigure the Debezium/Kafka-Connect connector (stop + // replicating this table); keep the landed Snowflake data. + await this.platformApiService.proxy( + 'DELETE', + `/pipeline/${normalizedPipelineId}/jobs`, + user, + { job_ids: [job.job_id], delete_snowflake_tables: false }, + ); + } else { + await this.platformApiService.proxy('DELETE', `/jobs/${job.job_id}`, user); + } this.logger.info('deleteTable: job deleted', { jobId: job.job_id }); return { name: tableName, is_deleted: updatedInput.is_deleted ?? true, deleted_at: updatedInput.deleted_at };