@@ -8,13 +8,16 @@ import {
88 setActivePinia ,
99 createPinia ,
1010} from 'pinia'
11+ import { diff as jsondiffpatchDiff } from 'jsondiffpatch'
1112
1213import { useAuthzStore } from '@/store/authz'
1314import { useConfigStore } from '@/store/config'
1415import { useCloudProfileStore } from '@/store/cloudProfile'
1516
1617import { firstItemMatchingVersionClassification } from '@/composables/helper'
1718
19+ import { getCloudProfileSpec } from '@/utils'
20+
1821describe ( 'stores' , ( ) => {
1922 describe ( 'cloudProfile' , ( ) => {
2023 const namespace = 'default'
@@ -49,6 +52,341 @@ describe('stores', () => {
4952 cloudProfileStore . setCloudProfiles ( [ ] )
5053 } )
5154
55+ describe ( 'cloudProfileByRef' , ( ) => {
56+ beforeEach ( ( ) => {
57+ cloudProfileStore . setCloudProfiles ( [
58+ {
59+ kind : 'CloudProfile' ,
60+ metadata : { name : 'aws' } ,
61+ spec : { type : 'aws' } ,
62+ } ,
63+ {
64+ kind : 'CloudProfile' ,
65+ metadata : { name : 'gcp' } ,
66+ spec : { type : 'gcp' } ,
67+ } ,
68+ ] )
69+ cloudProfileStore . setNamespacedCloudProfiles ( [
70+ {
71+ kind : 'NamespacedCloudProfile' ,
72+ metadata : { name : 'custom-aws' , namespace : 'garden-local' } ,
73+ spec : { parent : { kind : 'CloudProfile' , name : 'aws' } } ,
74+ status : { cloudProfileSpec : { type : 'aws' } } ,
75+ } ,
76+ ] , 'garden-local' )
77+ cloudProfileStore . setNamespacedCloudProfiles ( [
78+ {
79+ kind : 'NamespacedCloudProfile' ,
80+ metadata : { name : 'custom-gcp' , namespace : 'garden-dev' } ,
81+ spec : { parent : { kind : 'CloudProfile' , name : 'gcp' } } ,
82+ status : { cloudProfileSpec : { type : 'gcp' } } ,
83+ } ,
84+ ] , 'garden-dev' )
85+ } )
86+
87+ it ( 'should resolve a CloudProfile ref' , ( ) => {
88+ const result = cloudProfileStore . cloudProfileByRef ( { kind : 'CloudProfile' , name : 'aws' } )
89+ expect ( result ) . toBeDefined ( )
90+ expect ( result . metadata . name ) . toBe ( 'aws' )
91+ } )
92+
93+ it ( 'should resolve a NamespacedCloudProfile ref with namespace' , ( ) => {
94+ const result = cloudProfileStore . cloudProfileByRef ( {
95+ kind : 'NamespacedCloudProfile' ,
96+ name : 'custom-aws' ,
97+ namespace : 'garden-local' ,
98+ } )
99+ expect ( result ) . toBeDefined ( )
100+ expect ( result . metadata . name ) . toBe ( 'custom-aws' )
101+ expect ( result . metadata . namespace ) . toBe ( 'garden-local' )
102+ } )
103+
104+ it ( 'should resolve a NamespacedCloudProfile ref without namespace (fallback)' , ( ) => {
105+ const result = cloudProfileStore . cloudProfileByRef ( {
106+ kind : 'NamespacedCloudProfile' ,
107+ name : 'custom-gcp' ,
108+ } )
109+ expect ( result ) . toBeDefined ( )
110+ expect ( result . metadata . name ) . toBe ( 'custom-gcp' )
111+ } )
112+
113+ it ( 'should return null for ambiguous NamespacedCloudProfile ref without namespace' , ( ) => {
114+ cloudProfileStore . setNamespacedCloudProfiles ( [
115+ {
116+ kind : 'NamespacedCloudProfile' ,
117+ metadata : { name : 'custom-gcp' , namespace : 'garden-local' } ,
118+ spec : { parent : { kind : 'CloudProfile' , name : 'gcp' } } ,
119+ status : { cloudProfileSpec : { type : 'gcp' } } ,
120+ } ,
121+ ] , 'garden-local' )
122+ cloudProfileStore . setNamespacedCloudProfiles ( [
123+ {
124+ kind : 'NamespacedCloudProfile' ,
125+ metadata : { name : 'custom-gcp' , namespace : 'garden-dev' } ,
126+ spec : { parent : { kind : 'CloudProfile' , name : 'gcp' } } ,
127+ status : { cloudProfileSpec : { type : 'gcp' } } ,
128+ } ,
129+ ] , 'garden-dev' )
130+
131+ const result = cloudProfileStore . cloudProfileByRef ( {
132+ kind : 'NamespacedCloudProfile' ,
133+ name : 'custom-gcp' ,
134+ } )
135+ expect ( result ) . toBeNull ( )
136+ } )
137+
138+ it ( 'should return null for unknown ref' , ( ) => {
139+ expect ( cloudProfileStore . cloudProfileByRef ( { kind : 'CloudProfile' , name : 'unknown' } ) ) . toBeNull ( )
140+ expect ( cloudProfileStore . cloudProfileByRef ( { kind : 'NamespacedCloudProfile' , name : 'unknown' } ) ) . toBeNull ( )
141+ } )
142+
143+ it ( 'should return null for null or undefined ref' , ( ) => {
144+ expect ( cloudProfileStore . cloudProfileByRef ( null ) ) . toBeNull ( )
145+ expect ( cloudProfileStore . cloudProfileByRef ( undefined ) ) . toBeNull ( )
146+ } )
147+
148+ it ( 'should return null for unknown kind' , ( ) => {
149+ expect ( cloudProfileStore . cloudProfileByRef ( { kind : 'Unknown' , name : 'aws' } ) ) . toBeNull ( )
150+ } )
151+ } )
152+
153+ describe ( 'namespaced cloud profile cache' , ( ) => {
154+ beforeEach ( ( ) => {
155+ cloudProfileStore . setCloudProfiles ( [
156+ {
157+ kind : 'CloudProfile' ,
158+ metadata : { name : 'aws' } ,
159+ spec : { type : 'aws' } ,
160+ } ,
161+ {
162+ kind : 'CloudProfile' ,
163+ metadata : { name : 'gcp' } ,
164+ spec : { type : 'gcp' } ,
165+ } ,
166+ ] )
167+ } )
168+
169+ it ( 'should aggregate namespaced cloud profiles across multiple namespaces' , ( ) => {
170+ cloudProfileStore . setNamespacedCloudProfiles ( [
171+ {
172+ kind : 'NamespacedCloudProfile' ,
173+ metadata : { name : 'custom-aws' , namespace : 'garden-local' } ,
174+ spec : { parent : { kind : 'CloudProfile' , name : 'aws' } } ,
175+ status : { cloudProfileSpec : { type : 'aws' } } ,
176+ } ,
177+ ] , 'garden-local' )
178+
179+ cloudProfileStore . setNamespacedCloudProfiles ( [
180+ {
181+ kind : 'NamespacedCloudProfile' ,
182+ metadata : { name : 'custom-gcp' , namespace : 'garden-dev' } ,
183+ spec : { parent : { kind : 'CloudProfile' , name : 'gcp' } } ,
184+ status : { cloudProfileSpec : { type : 'gcp' } } ,
185+ } ,
186+ ] , 'garden-dev' )
187+
188+ expect ( cloudProfileStore . namespacedCloudProfileList ) . toHaveLength ( 2 )
189+ expect ( cloudProfileStore . hasNamespacedCloudProfilesForNamespace ( 'garden-local' ) ) . toBe ( true )
190+ expect ( cloudProfileStore . hasNamespacedCloudProfilesForNamespace ( 'garden-dev' ) ) . toBe ( true )
191+ } )
192+
193+ it ( 'should resolve namespaced cloud profile refs across multiple cached namespaces' , ( ) => {
194+ cloudProfileStore . setNamespacedCloudProfiles ( [
195+ {
196+ kind : 'NamespacedCloudProfile' ,
197+ metadata : { name : 'custom-aws' , namespace : 'garden-local' } ,
198+ spec : { parent : { kind : 'CloudProfile' , name : 'aws' } } ,
199+ status : { cloudProfileSpec : { type : 'aws' } } ,
200+ } ,
201+ ] , 'garden-local' )
202+ cloudProfileStore . setNamespacedCloudProfiles ( [
203+ {
204+ kind : 'NamespacedCloudProfile' ,
205+ metadata : { name : 'custom-gcp' , namespace : 'garden-dev' } ,
206+ spec : { parent : { kind : 'CloudProfile' , name : 'gcp' } } ,
207+ status : { cloudProfileSpec : { type : 'gcp' } } ,
208+ } ,
209+ ] , 'garden-dev' )
210+
211+ expect ( cloudProfileStore . cloudProfileByRef ( {
212+ kind : 'NamespacedCloudProfile' ,
213+ name : 'custom-gcp' ,
214+ namespace : 'garden-dev' ,
215+ } ) ?. metadata . namespace ) . toBe ( 'garden-dev' )
216+ } )
217+ } )
218+
219+ describe ( 'cloudProfilesByProviderType' , ( ) => {
220+ beforeEach ( ( ) => {
221+ cloudProfileStore . setCloudProfiles ( [
222+ {
223+ kind : 'CloudProfile' ,
224+ metadata : { name : 'aws' } ,
225+ spec : { type : 'aws' } ,
226+ } ,
227+ ] )
228+ cloudProfileStore . setNamespacedCloudProfiles ( [
229+ {
230+ kind : 'NamespacedCloudProfile' ,
231+ metadata : { name : 'custom-aws' , namespace : 'garden-local' } ,
232+ spec : { parent : { kind : 'CloudProfile' , name : 'aws' } } ,
233+ status : { cloudProfileSpec : { type : 'aws' } } ,
234+ } ,
235+ ] , 'garden-local' )
236+ cloudProfileStore . setNamespacedCloudProfiles ( [
237+ {
238+ kind : 'NamespacedCloudProfile' ,
239+ metadata : { name : 'custom-gcp' , namespace : 'garden-dev' } ,
240+ spec : { parent : { kind : 'CloudProfile' , name : 'gcp' } } ,
241+ status : { cloudProfileSpec : { type : 'gcp' } } ,
242+ } ,
243+ ] , 'garden-dev' )
244+ } )
245+
246+ it ( 'should return both regular and namespaced profiles for a provider type' , ( ) => {
247+ const profiles = cloudProfileStore . cloudProfilesByProviderType ( 'aws' )
248+ expect ( profiles ) . toHaveLength ( 2 )
249+ expect ( profiles [ 0 ] . metadata . name ) . toBe ( 'aws' )
250+ expect ( profiles [ 1 ] . metadata . name ) . toBe ( 'custom-aws' )
251+ } )
252+
253+ it ( 'should return only namespaced profiles when no regular profiles match' , ( ) => {
254+ const profiles = cloudProfileStore . cloudProfilesByProviderType ( 'gcp' )
255+ expect ( profiles ) . toHaveLength ( 1 )
256+ expect ( profiles [ 0 ] . metadata . name ) . toBe ( 'custom-gcp' )
257+ } )
258+
259+ it ( 'should return empty array for unknown provider type' , ( ) => {
260+ const profiles = cloudProfileStore . cloudProfilesByProviderType ( 'unknown' )
261+ expect ( profiles ) . toHaveLength ( 0 )
262+ } )
263+ } )
264+
265+ describe ( 'sortedInfraProviderTypeList' , ( ) => {
266+ it ( 'should include provider types from both regular and namespaced profiles' , ( ) => {
267+ cloudProfileStore . setCloudProfiles ( [
268+ {
269+ kind : 'CloudProfile' ,
270+ metadata : { name : 'aws' } ,
271+ spec : { type : 'aws' } ,
272+ } ,
273+ ] )
274+ cloudProfileStore . setNamespacedCloudProfiles ( [
275+ {
276+ kind : 'NamespacedCloudProfile' ,
277+ metadata : { name : 'custom-gcp' , namespace : 'garden-local' } ,
278+ spec : { parent : { kind : 'CloudProfile' , name : 'gcp' } } ,
279+ status : { cloudProfileSpec : { type : 'gcp' } } ,
280+ } ,
281+ ] , 'garden-local' )
282+ const providerTypes = cloudProfileStore . sortedInfraProviderTypeList
283+ expect ( providerTypes ) . toContain ( 'aws' )
284+ expect ( providerTypes ) . toContain ( 'gcp' )
285+ } )
286+ } )
287+
288+ describe ( 'setNamespacedCloudProfiles with rehydration' , ( ) => {
289+ it ( 'should pass through profiles that already have cloudProfileSpec' , ( ) => {
290+ cloudProfileStore . setCloudProfiles ( [
291+ {
292+ kind : 'CloudProfile' ,
293+ metadata : { name : 'aws' } ,
294+ spec : { type : 'aws' , kubernetes : { versions : [ { version : '1.30.0' } ] } } ,
295+ } ,
296+ ] )
297+ const namespacedProfile = {
298+ kind : 'NamespacedCloudProfile' ,
299+ metadata : { name : 'custom' , namespace : 'garden-local' } ,
300+ spec : { parent : { kind : 'CloudProfile' , name : 'aws' } } ,
301+ status : {
302+ cloudProfileSpec : { type : 'aws' , kubernetes : { versions : [ { version : '1.31.0' } ] } } ,
303+ } ,
304+ }
305+ cloudProfileStore . setNamespacedCloudProfiles ( [ namespacedProfile ] , 'garden-local' )
306+ const stored = cloudProfileStore . namespacedCloudProfileList [ 0 ]
307+ expect ( getCloudProfileSpec ( stored ) . kubernetes . versions [ 0 ] . version ) . toBe ( '1.31.0' )
308+ } )
309+
310+ it ( 'should rehydrate profiles with cloudProfileSpecDiff from parent' , ( ) => {
311+ const parentSpec = {
312+ type : 'aws' ,
313+ kubernetes : { versions : [ { version : '1.30.0' } ] } ,
314+ }
315+ cloudProfileStore . setCloudProfiles ( [
316+ {
317+ kind : 'CloudProfile' ,
318+ metadata : { name : 'aws' } ,
319+ spec : parentSpec ,
320+ } ,
321+ ] )
322+ // null diff means spec is identical to parent
323+ const namespacedProfile = {
324+ kind : 'NamespacedCloudProfile' ,
325+ metadata : { name : 'custom' , namespace : 'garden-local' } ,
326+ spec : { parent : { kind : 'CloudProfile' , name : 'aws' } } ,
327+ status : {
328+ cloudProfileSpecDiff : null ,
329+ } ,
330+ }
331+ cloudProfileStore . setNamespacedCloudProfiles ( [ namespacedProfile ] , 'garden-local' )
332+ const stored = cloudProfileStore . namespacedCloudProfileList [ 0 ]
333+ expect ( stored . status . cloudProfileSpec ) . toEqual ( parentSpec )
334+ expect ( stored . status . cloudProfileSpecDiff ) . toBeUndefined ( )
335+ } )
336+
337+ it ( 'should rehydrate profiles with non-null cloudProfileSpecDiff from parent' , ( ) => {
338+ const parentSpec = {
339+ type : 'aws' ,
340+ kubernetes : { versions : [ { version : '1.30.0' } ] } ,
341+ machineTypes : [ { name : 'm5.large' , cpu : '2' , memory : '8Gi' } ] ,
342+ }
343+ const namespacedSpec = {
344+ type : 'aws' ,
345+ kubernetes : { versions : [ { version : '1.31.0' } ] } ,
346+ machineTypes : [
347+ { name : 'm5.large' , cpu : '2' , memory : '8Gi' } ,
348+ { name : 'm5.xlarge' , cpu : '4' , memory : '16Gi' } ,
349+ ] ,
350+ }
351+ const cloudProfileSpecDiff = jsondiffpatchDiff ( parentSpec , namespacedSpec )
352+ cloudProfileStore . setCloudProfiles ( [
353+ {
354+ kind : 'CloudProfile' ,
355+ metadata : { name : 'aws' } ,
356+ spec : parentSpec ,
357+ } ,
358+ ] )
359+ const namespacedProfile = {
360+ kind : 'NamespacedCloudProfile' ,
361+ metadata : { name : 'custom' , namespace : 'garden-local' } ,
362+ spec : { parent : { kind : 'CloudProfile' , name : 'aws' } } ,
363+ status : {
364+ cloudProfileSpecDiff,
365+ } ,
366+ }
367+ cloudProfileStore . setNamespacedCloudProfiles ( [ namespacedProfile ] , 'garden-local' )
368+ const stored = cloudProfileStore . namespacedCloudProfileList [ 0 ]
369+ expect ( stored . status . cloudProfileSpec ) . toEqual ( namespacedSpec )
370+ expect ( stored . status . cloudProfileSpecDiff ) . toBeUndefined ( )
371+ } )
372+
373+ it ( 'should skip rehydration when parent is not found' , ( ) => {
374+ cloudProfileStore . setCloudProfiles ( [ ] )
375+ const namespacedProfile = {
376+ kind : 'NamespacedCloudProfile' ,
377+ metadata : { name : 'custom' , namespace : 'garden-local' } ,
378+ spec : { parent : { kind : 'CloudProfile' , name : 'nonexistent' } } ,
379+ status : {
380+ cloudProfileSpecDiff : null ,
381+ } ,
382+ }
383+ cloudProfileStore . setNamespacedCloudProfiles ( [ namespacedProfile ] , 'garden-local' )
384+ const stored = cloudProfileStore . namespacedCloudProfileList [ 0 ]
385+ // Profile should be passed through unchanged (no cloudProfileSpec added)
386+ expect ( stored . status . cloudProfileSpec ) . toBeUndefined ( )
387+ } )
388+ } )
389+
52390 describe ( 'helper' , ( ) => {
53391 describe ( '#firstItemMatchingVersionClassification' , ( ) => {
54392 it ( 'should select default item that matches version classification' , ( ) => {
0 commit comments