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,179 @@ 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 = (
188+ conditions : { type : string ; status : string } [ ] ,
189+ options : { parentGatewayName ?: string ; visibility ?: string } = { } ,
190+ ) => ( {
191+ metadata : {
192+ labels : {
193+ 'networking.knative.dev/visibility' : options . visibility ?? '' ,
194+ } ,
195+ } ,
196+ spec : {
197+ parentRefs : [ { name : options . parentGatewayName ?? 'platform' } ] ,
198+ } ,
199+ status : {
200+ parents : [
201+ {
202+ conditions,
203+ controllerName : 'istio.io/gateway-controller' ,
204+ parentRef : {
205+ group : 'gateway.networking.k8s.io' ,
206+ kind : 'Gateway' ,
207+ name : options . parentGatewayName ?? 'platform' ,
208+ } ,
209+ } ,
210+ ] ,
211+ } ,
212+ } )
213+
214+ // ---------------------------------------------------------------------------
215+ // parseHTTPRouteStatus
216+ // ---------------------------------------------------------------------------
217+
218+ describe ( 'parseHTTPRouteStatus' , ( ) => {
219+ test ( 'returns true when Accepted and ResolvedRefs are both True' , ( ) => {
220+ const route = makeHTTPRoute ( [
221+ { type : 'Accepted' , status : 'True' } ,
222+ { type : 'ResolvedRefs' , status : 'True' } ,
223+ ] )
224+ expect ( parseHTTPRouteStatus ( route ) ) . toBe ( true )
225+ } )
226+
227+ test ( 'returns false when Accepted is False' , ( ) => {
228+ const route = makeHTTPRoute ( [
229+ { type : 'Accepted' , status : 'False' } ,
230+ { type : 'ResolvedRefs' , status : 'True' } ,
231+ ] )
232+ expect ( parseHTTPRouteStatus ( route ) ) . toBe ( false )
233+ } )
234+
235+ test ( 'returns false when ResolvedRefs is False' , ( ) => {
236+ const route = makeHTTPRoute ( [
237+ { type : 'Accepted' , status : 'True' } ,
238+ { type : 'ResolvedRefs' , status : 'False' } ,
239+ ] )
240+ expect ( parseHTTPRouteStatus ( route ) ) . toBe ( false )
241+ } )
242+
243+ test ( 'returns false when conditions are empty' , ( ) => {
244+ expect ( parseHTTPRouteStatus ( makeHTTPRoute ( [ ] ) ) ) . toBe ( false )
245+ } )
246+
247+ test ( 'returns false when parents array is missing' , ( ) => {
248+ expect ( parseHTTPRouteStatus ( { status : { } } ) ) . toBe ( false )
249+ } )
250+ } )
251+
252+ // ---------------------------------------------------------------------------
253+ // getServiceStatus
254+ // ---------------------------------------------------------------------------
255+
256+ describe ( 'getServiceStatus' , ( ) => {
257+ afterEach ( ( ) => {
258+ jest . clearAllMocks ( )
259+ } )
260+
261+ const mockList = ( items : any [ ] ) =>
262+ jest . spyOn ( CustomObjectsApi . prototype , 'listNamespacedCustomObject' ) . mockResolvedValue ( { items } as any )
263+
264+ test ( 'returns NotFound when no HTTPRoutes exist' , async ( ) => {
265+ mockList ( [ ] )
266+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
267+ expect ( result ) . toBe ( 'NotFound' )
268+ } )
269+
270+ test ( 'returns NotFound when only local HTTPRoutes exist' , async ( ) => {
271+ mockList ( [
272+ makeHTTPRoute (
273+ [
274+ { type : 'Accepted' , status : 'True' } ,
275+ { type : 'ResolvedRefs' , status : 'True' } ,
276+ ] ,
277+ { parentGatewayName : 'knative-local-gateway' , visibility : 'cluster-local' } ,
278+ ) ,
279+ ] )
280+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
281+ expect ( result ) . toBe ( 'NotFound' )
282+ } )
283+
284+ test ( 'returns Unknown when multiple HTTPRoutes are found' , async ( ) => {
285+ mockList ( [ makeHTTPRoute ( [ ] ) , makeHTTPRoute ( [ ] ) ] )
286+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
287+ expect ( result ) . toBe ( 'Unknown' )
288+ } )
289+
290+ test ( 'returns Succeeded when single HTTPRoute is accepted' , async ( ) => {
291+ mockList ( [
292+ makeHTTPRoute ( [
293+ { type : 'Accepted' , status : 'True' } ,
294+ { type : 'ResolvedRefs' , status : 'True' } ,
295+ ] ) ,
296+ ] )
297+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
298+ expect ( result ) . toBe ( 'Succeeded' )
299+ } )
300+
301+ test ( 'returns Succeeded when local and public routes exist and public is accepted' , async ( ) => {
302+ mockList ( [
303+ makeHTTPRoute (
304+ [
305+ { type : 'Accepted' , status : 'True' } ,
306+ { type : 'ResolvedRefs' , status : 'True' } ,
307+ ] ,
308+ { parentGatewayName : 'knative-local-gateway' , visibility : 'cluster-local' } ,
309+ ) ,
310+ makeHTTPRoute ( [
311+ { type : 'Accepted' , status : 'True' } ,
312+ { type : 'ResolvedRefs' , status : 'True' } ,
313+ ] ) ,
314+ ] )
315+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
316+ expect ( result ) . toBe ( 'Succeeded' )
317+ } )
318+
319+ test ( 'returns Unknown when single HTTPRoute is not accepted' , async ( ) => {
320+ mockList ( [
321+ makeHTTPRoute ( [
322+ { type : 'Accepted' , status : 'False' } ,
323+ { type : 'ResolvedRefs' , status : 'True' } ,
324+ ] ) ,
325+ ] )
326+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
327+ expect ( result ) . toBe ( 'Unknown' )
328+ } )
329+
330+ test ( 'queries with the correct label selector and namespace' , async ( ) => {
331+ const spy = mockList ( [ ] )
332+ await getServiceStatus ( makeServiceResponse ( 'my-app' , 'dev' ) )
333+ expect ( spy ) . toHaveBeenCalledWith (
334+ expect . objectContaining ( {
335+ group : 'gateway.networking.k8s.io' ,
336+ version : 'v1' ,
337+ namespace : 'team-dev' ,
338+ plural : 'httproutes' ,
339+ labelSelector : 'otomi.io/app=my-app' ,
340+ } ) ,
341+ )
342+ } )
343+
344+ test ( 'returns NotFound when the k8s API call throws' , async ( ) => {
345+ jest . spyOn ( CustomObjectsApi . prototype , 'listNamespacedCustomObject' ) . mockRejectedValue ( new Error ( 'network error' ) )
346+ const result = await getServiceStatus ( makeServiceResponse ( 'web' , 'alpha' ) )
347+ expect ( result ) . toBe ( 'NotFound' )
348+ } )
349+ } )
0 commit comments