@@ -22,7 +22,6 @@ import { KubeService } from './KubeService';
2222const mockCoreV1Api = {
2323 createNamespacedSecret : jest . fn ( ) ,
2424 readNamespacedSecret : jest . fn ( ) ,
25- replaceNamespacedSecret : jest . fn ( ) ,
2625 deleteNamespacedSecret : jest . fn ( ) ,
2726 listNamespacedPod : jest . fn ( ) ,
2827 readNamespacedPodLog : jest . fn ( ) ,
@@ -252,19 +251,9 @@ describe('KubeService', () => {
252251 beforeEach ( ( ) => {
253252 // Mock createProjectSecret and createJobSecret to succeed
254253 mockCoreV1Api . createNamespacedSecret . mockResolvedValue ( { } ) ;
255- // Mock readNamespacedSecret and replaceNamespacedSecret for ownerReference patching
256- mockCoreV1Api . readNamespacedSecret . mockResolvedValue ( {
257- apiVersion : 'v1' ,
258- kind : 'Secret' ,
259- metadata : {
260- name : 'x2a-job-secret-init-job-123' ,
261- namespace : 'test-namespace' ,
262- } ,
263- } ) ;
264- mockCoreV1Api . replaceNamespacedSecret . mockResolvedValue ( { } ) ;
265254 } ) ;
266255
267- it ( 'should create both project and job secrets before creating job' , async ( ) => {
256+ it ( 'should create project secret before job, and job secret after job with ownerReference ' , async ( ) => {
268257 mockBatchV1Api . createNamespacedJob . mockResolvedValue ( {
269258 metadata : { name : 'job-x2a-init-abc123' , uid : 'uid-123' } ,
270259 } ) ;
@@ -283,13 +272,22 @@ describe('KubeService', () => {
283272 } ) ,
284273 ) ;
285274
286- // Should create job secret (Git credentials)
275+ // Should create job secret (Git credentials) with ownerReference to the job
287276 expect ( mockCoreV1Api . createNamespacedSecret ) . toHaveBeenCalledWith (
288277 expect . objectContaining ( {
289278 namespace : 'test-namespace' ,
290279 body : expect . objectContaining ( {
291280 metadata : expect . objectContaining ( {
292281 name : 'x2a-job-secret-init-job-123' ,
282+ ownerReferences : [
283+ expect . objectContaining ( {
284+ apiVersion : 'batch/v1' ,
285+ kind : 'Job' ,
286+ name : expect . stringMatching ( / ^ j o b - x 2 a - i n i t - / ) ,
287+ uid : 'uid-123' ,
288+ blockOwnerDeletion : true ,
289+ } ) ,
290+ ] ,
293291 } ) ,
294292 stringData : expect . objectContaining ( {
295293 SOURCE_REPO_URL : 'https://github.com/org/source' ,
@@ -342,51 +340,65 @@ describe('KubeService', () => {
342340 expect ( result . k8sJobName ) . toBeDefined ( ) ;
343341 } ) ;
344342
345- it ( 'should set ownerReference on job secret after job creation ' , async ( ) => {
343+ it ( 'should create job secret with ownerReference containing job uid ' , async ( ) => {
346344 mockBatchV1Api . createNamespacedJob . mockResolvedValue ( {
347345 metadata : { name : 'job-x2a-init-abc123' , uid : 'uid-456' } ,
348346 } ) ;
349347
350348 await kubeService . createJob ( params ) ;
351349
352- // Should read the job secret
353- expect ( mockCoreV1Api . readNamespacedSecret ) . toHaveBeenCalledWith ( {
354- name : 'x2a-job-secret-init-job-123' ,
355- namespace : 'test-namespace' ,
356- } ) ;
350+ // Job secret should be the second createNamespacedSecret call (after project secret)
351+ const calls = mockCoreV1Api . createNamespacedSecret . mock . calls ;
352+ const jobSecretCall = calls . find (
353+ ( c : any ) => c [ 0 ] . body . metadata ?. name === 'x2a-job-secret-init-job-123' ,
354+ ) ;
355+ expect ( jobSecretCall ) . toBeDefined ( ) ;
357356
358- // Should replace the secret with ownerReference set
359- expect ( mockCoreV1Api . replaceNamespacedSecret ) . toHaveBeenCalled ( ) ;
360- const replaceCall =
361- mockCoreV1Api . replaceNamespacedSecret . mock . calls [ 0 ] [ 0 ] ;
362- expect ( replaceCall . name ) . toBe ( 'x2a-job-secret-init-job-123' ) ;
363- expect ( replaceCall . namespace ) . toBe ( 'test-namespace' ) ;
364- const ownerRefs = replaceCall . body . metadata . ownerReferences ;
357+ const ownerRefs = jobSecretCall ! [ 0 ] . body . metadata . ownerReferences ;
365358 expect ( ownerRefs ) . toHaveLength ( 1 ) ;
366- expect ( ownerRefs [ 0 ] ) . toEqual (
359+ expect ( ownerRefs [ 0 ] ) . toEqual ( {
360+ apiVersion : 'batch/v1' ,
361+ kind : 'Job' ,
362+ name : expect . stringMatching ( / ^ j o b - x 2 a - i n i t - / ) ,
363+ uid : 'uid-456' ,
364+ blockOwnerDeletion : true ,
365+ } ) ;
366+ } ) ;
367+
368+ it ( 'should delete job and throw if job secret creation fails' , async ( ) => {
369+ mockBatchV1Api . createNamespacedJob . mockResolvedValue ( {
370+ metadata : { name : 'job-x2a-init-abc123' , uid : 'uid-456' } ,
371+ } ) ;
372+ mockBatchV1Api . deleteNamespacedJob . mockResolvedValue ( { } ) ;
373+ // First call succeeds (project secret), second fails (job secret)
374+ mockCoreV1Api . createNamespacedSecret
375+ . mockResolvedValueOnce ( { } )
376+ . mockRejectedValueOnce ( new Error ( 'Secret creation failed' ) ) ;
377+
378+ await expect ( kubeService . createJob ( params ) ) . rejects . toThrow (
379+ 'Secret creation failed' ,
380+ ) ;
381+
382+ // Should clean up the orphaned job
383+ expect ( mockBatchV1Api . deleteNamespacedJob ) . toHaveBeenCalledWith (
367384 expect . objectContaining ( {
368- apiVersion : 'batch/v1' ,
369- kind : 'Job' ,
370- uid : 'uid-456' ,
371- blockOwnerDeletion : true ,
385+ namespace : 'test-namespace' ,
372386 } ) ,
373387 ) ;
374- expect ( ownerRefs [ 0 ] . name ) . toMatch ( / ^ j o b - x 2 a - i n i t - / ) ;
375388 } ) ;
376389
377- it ( 'should succeed even if ownerReference patching fails ' , async ( ) => {
390+ it ( 'should delete job and throw if created job has no UID ' , async ( ) => {
378391 mockBatchV1Api . createNamespacedJob . mockResolvedValue ( {
379- metadata : { name : 'job-x2a-init-abc123' , uid : 'uid-456' } ,
392+ metadata : { name : 'job-x2a-init-abc123' } ,
380393 } ) ;
381- mockCoreV1Api . readNamespacedSecret . mockRejectedValue (
382- new Error ( 'Secret read failed' ) ,
383- ) ;
394+ mockBatchV1Api . deleteNamespacedJob . mockResolvedValue ( { } ) ;
384395
385- const result = await kubeService . createJob ( params ) ;
396+ await expect ( kubeService . createJob ( params ) ) . rejects . toThrow (
397+ / c r e a t e d w i t h o u t U I D / ,
398+ ) ;
386399
387- // Job creation should still succeed
388- expect ( result . k8sJobName ) . toBeDefined ( ) ;
389- expect ( mockBatchV1Api . createNamespacedJob ) . toHaveBeenCalled ( ) ;
400+ // Should clean up the orphaned job
401+ expect ( mockBatchV1Api . deleteNamespacedJob ) . toHaveBeenCalled ( ) ;
390402 } ) ;
391403 } ) ;
392404
0 commit comments