1- import { CoreV1Api , V1Service } from '@kubernetes/client-node'
2- import { getCloudttyActiveTime , getLogTime , mergeCanaryServices , toK8sService } from './k8s-operations'
1+ import { CoreV1Api , CustomObjectsApi , V1Service } from '@kubernetes/client-node'
2+ import {
3+ getCloudttyActiveTime ,
4+ getLogTime ,
5+ getServiceStatus ,
6+ mergeCanaryServices ,
7+ parseHTTPRouteStatus ,
8+ toK8sService ,
9+ } from './k8s-operations'
10+ import { AplServiceResponse } from './otomi-models'
311
412// Mock the KubeConfig
513jest . mock ( '@kubernetes/client-node' , ( ) => {
@@ -12,6 +20,9 @@ jest.mock('@kubernetes/client-node', () => {
1220 if ( apiClientType === actual . CoreV1Api ) {
1321 return new actual . CoreV1Api ( )
1422 }
23+ if ( apiClientType === actual . CustomObjectsApi ) {
24+ return new actual . CustomObjectsApi ( )
25+ }
1526 return { }
1627 } ) ,
1728 } ) ) ,
@@ -160,3 +171,132 @@ describe('getCloudttyActiveTime', () => {
160171 expect ( result ) . toBeUndefined ( )
161172 } )
162173} )
174+
175+ // ---------------------------------------------------------------------------
176+ // Helpers
177+ // ---------------------------------------------------------------------------
178+
179+ const makeServiceResponse = ( name : string , teamName : string , overrides : Record < string , any > = { } ) : AplServiceResponse =>
180+ ( {
181+ kind : 'AplTeamService' ,
182+ metadata : { name, labels : { 'apl.io/teamId' : teamName } } ,
183+ spec : { } ,
184+ ...overrides ,
185+ } ) as unknown as AplServiceResponse
186+
187+ const makeHTTPRoute = ( conditions : { type : string ; status : string } [ ] ) => ( {
188+ status : {
189+ parents : [
190+ {
191+ conditions,
192+ controllerName : 'istio.io/gateway-controller' ,
193+ parentRef : { group : 'gateway.networking.k8s.io' , kind : 'Gateway' , name : 'knative-local-gateway' } ,
194+ } ,
195+ ] ,
196+ } ,
197+ } )
198+
199+ // ---------------------------------------------------------------------------
200+ // parseHTTPRouteStatus
201+ // ---------------------------------------------------------------------------
202+
203+ describe ( 'parseHTTPRouteStatus' , ( ) => {
204+ test ( 'returns true when Accepted and ResolvedRefs are both True' , ( ) => {
205+ const route = makeHTTPRoute ( [
206+ { type : 'Accepted' , status : 'True' } ,
207+ { type : 'ResolvedRefs' , status : 'True' } ,
208+ ] )
209+ expect ( parseHTTPRouteStatus ( route ) ) . toBe ( true )
210+ } )
211+
212+ test ( 'returns false when Accepted is False' , ( ) => {
213+ const route = makeHTTPRoute ( [
214+ { type : 'Accepted' , status : 'False' } ,
215+ { type : 'ResolvedRefs' , status : 'True' } ,
216+ ] )
217+ expect ( parseHTTPRouteStatus ( route ) ) . toBe ( false )
218+ } )
219+
220+ test ( 'returns false when ResolvedRefs is False' , ( ) => {
221+ const route = makeHTTPRoute ( [
222+ { type : 'Accepted' , status : 'True' } ,
223+ { type : 'ResolvedRefs' , status : 'False' } ,
224+ ] )
225+ expect ( parseHTTPRouteStatus ( route ) ) . toBe ( false )
226+ } )
227+
228+ test ( 'returns false when conditions are empty' , ( ) => {
229+ expect ( parseHTTPRouteStatus ( makeHTTPRoute ( [ ] ) ) ) . toBe ( false )
230+ } )
231+
232+ test ( 'returns false when parents array is missing' , ( ) => {
233+ expect ( parseHTTPRouteStatus ( { status : { } } ) ) . toBe ( false )
234+ } )
235+ } )
236+
237+ // ---------------------------------------------------------------------------
238+ // getServiceStatus
239+ // ---------------------------------------------------------------------------
240+
241+ describe ( 'getServiceStatus' , ( ) => {
242+ afterEach ( ( ) => {
243+ jest . clearAllMocks ( )
244+ } )
245+
246+ const mockList = ( items : any [ ] ) =>
247+ jest . spyOn ( CustomObjectsApi . prototype , 'listNamespacedCustomObject' ) . mockResolvedValue ( { items } as any )
248+
249+ test ( 'returns NotFound when no HTTPRoutes exist' , async ( ) => {
250+ mockList ( [ ] )
251+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
252+ expect ( result ) . toBe ( 'NotFound' )
253+ } )
254+
255+ test ( 'returns Unknown when multiple HTTPRoutes are found' , async ( ) => {
256+ mockList ( [ makeHTTPRoute ( [ ] ) , makeHTTPRoute ( [ ] ) ] )
257+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
258+ expect ( result ) . toBe ( 'Unknown' )
259+ } )
260+
261+ test ( 'returns Succeeded when single HTTPRoute is accepted' , async ( ) => {
262+ mockList ( [
263+ makeHTTPRoute ( [
264+ { type : 'Accepted' , status : 'True' } ,
265+ { type : 'ResolvedRefs' , status : 'True' } ,
266+ ] ) ,
267+ ] )
268+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
269+ expect ( result ) . toBe ( 'Succeeded' )
270+ } )
271+
272+ test ( 'returns Unknown when single HTTPRoute is not accepted' , async ( ) => {
273+ mockList ( [
274+ makeHTTPRoute ( [
275+ { type : 'Accepted' , status : 'False' } ,
276+ { type : 'ResolvedRefs' , status : 'True' } ,
277+ ] ) ,
278+ ] )
279+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
280+ expect ( result ) . toBe ( 'Unknown' )
281+ } )
282+
283+ test ( 'queries with the correct label selector and namespace' , async ( ) => {
284+ const spy = mockList ( [ ] )
285+ await getServiceStatus ( makeServiceResponse ( 'my-app' , 'dev' ) )
286+ expect ( spy ) . toHaveBeenCalledWith (
287+ expect . objectContaining ( {
288+ group : 'gateway.networking.k8s.io' ,
289+ version : 'v1' ,
290+ namespace : 'team-dev' ,
291+ plural : 'httproutes' ,
292+ labelSelector : 'otomi.io/app=my-app' ,
293+ } ) ,
294+ )
295+ } )
296+
297+ test ( 'returns NotFound when the k8s API call throws' , async ( ) => {
298+ jest . spyOn ( CustomObjectsApi . prototype , 'listNamespacedCustomObject' ) . mockRejectedValue ( new Error ( 'network error' ) )
299+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
300+ expect ( result ) . toBe ( 'NotFound' )
301+ } )
302+ } )
0 commit comments