1+ import { Secret } from '../../src/types' ;
2+ import {
3+ parseSecretReference ,
4+ resolveSecretReferences ,
5+ normalizeKey
6+ } from '../../src/utils/secretReferencing' ;
7+
8+ describe ( 'Secret Referencing Utils' , ( ) => {
9+ describe ( 'parseSecretReference' , ( ) => {
10+ it ( 'should parse local secret reference' , ( ) => {
11+ const result = parseSecretReference ( '${SECRET_KEY}' ) ;
12+ expect ( result ) . toEqual ( {
13+ app : null ,
14+ env : null ,
15+ path : '/' ,
16+ key : 'SECRET_KEY'
17+ } ) ;
18+ } ) ;
19+
20+ it ( 'should parse local folder secret reference' , ( ) => {
21+ const result = parseSecretReference ( '${/path/to/SECRET_KEY}' ) ;
22+ expect ( result ) . toEqual ( {
23+ app : null ,
24+ env : null ,
25+ path : '/path/to' ,
26+ key : 'SECRET_KEY'
27+ } ) ;
28+ } ) ;
29+
30+ it ( 'should parse cross-env secret reference' , ( ) => {
31+ const result = parseSecretReference ( '${prod.SECRET_KEY}' ) ;
32+ expect ( result ) . toEqual ( {
33+ app : null ,
34+ env : 'prod' ,
35+ path : '/' ,
36+ key : 'SECRET_KEY'
37+ } ) ;
38+ } ) ;
39+
40+ it ( 'should parse cross-env folder secret reference' , ( ) => {
41+ const result = parseSecretReference ( '${prod./path/to/SECRET_KEY}' ) ;
42+ expect ( result ) . toEqual ( {
43+ app : null ,
44+ env : 'prod' ,
45+ path : '/path/to' ,
46+ key : 'SECRET_KEY'
47+ } ) ;
48+ } ) ;
49+
50+ it ( 'should parse cross-app secret reference' , ( ) => {
51+ const result = parseSecretReference ( '${app-name::SECRET_KEY}' ) ;
52+ expect ( result ) . toEqual ( {
53+ app : 'app-name' ,
54+ env : null ,
55+ path : '/' ,
56+ key : 'SECRET_KEY'
57+ } ) ;
58+ } ) ;
59+
60+ it ( 'should parse cross-app folder secret reference' , ( ) => {
61+ const result = parseSecretReference ( '${app-name::/path/to/SECRET_KEY}' ) ;
62+ expect ( result ) . toEqual ( {
63+ app : 'app-name' ,
64+ env : null ,
65+ path : '/path/to' ,
66+ key : 'SECRET_KEY'
67+ } ) ;
68+ } ) ;
69+
70+ it ( 'should parse cross-app cross-env secret reference' , ( ) => {
71+ const result = parseSecretReference ( '${app-name::prod.SECRET_KEY}' ) ;
72+ expect ( result ) . toEqual ( {
73+ app : 'app-name' ,
74+ env : 'prod' ,
75+ path : '/' ,
76+ key : 'SECRET_KEY'
77+ } ) ;
78+ } ) ;
79+
80+ it ( 'should parse cross-app cross-env folder secret reference' , ( ) => {
81+ const result = parseSecretReference ( '${app-name::prod./path/to/SECRET_KEY}' ) ;
82+ expect ( result ) . toEqual ( {
83+ app : 'app-name' ,
84+ env : 'prod' ,
85+ path : '/path/to' ,
86+ key : 'SECRET_KEY'
87+ } ) ;
88+ } ) ;
89+
90+ it ( 'should handle invalid reference format' , ( ) => {
91+ expect ( ( ) => parseSecretReference ( 'invalid' ) ) . toThrow ( 'Invalid secret reference format' ) ;
92+ } ) ;
93+ } ) ;
94+
95+ describe ( 'normalizeKey' , ( ) => {
96+ it ( 'should normalize key without app' , ( ) => {
97+ const result = normalizeKey ( 'prod' , '/path/to' , 'SECRET_KEY' ) ;
98+ expect ( result ) . toBe ( 'prod:/path/to:SECRET_KEY' ) ;
99+ } ) ;
100+
101+ it ( 'should normalize key with app' , ( ) => {
102+ const result = normalizeKey ( 'prod' , '/path/to' , 'SECRET_KEY' , 'app-name' ) ;
103+ expect ( result ) . toBe ( 'app-name:prod:/path/to:SECRET_KEY' ) ;
104+ } ) ;
105+
106+ it ( 'should handle trailing slashes in path' , ( ) => {
107+ const result = normalizeKey ( 'prod' , '/path/to/' , 'SECRET_KEY' ) ;
108+ expect ( result ) . toBe ( 'prod:/path/to:SECRET_KEY' ) ;
109+ } ) ;
110+ } ) ;
111+
112+ describe ( 'resolveSecretReferences' , ( ) => {
113+ const mockFetcher = jest . fn ( ) ;
114+ const cache = new Map < string , string > ( ) ;
115+
116+ beforeEach ( ( ) => {
117+ mockFetcher . mockClear ( ) ;
118+ cache . clear ( ) ;
119+ } ) ;
120+
121+ it ( 'should resolve local secret reference' , async ( ) => {
122+ mockFetcher . mockResolvedValueOnce ( {
123+ value : 'secret-value' ,
124+ key : 'SECRET_KEY' ,
125+ environment : 'dev' ,
126+ path : '/' ,
127+ id : '1' ,
128+ comment : '' ,
129+ tags : [ ] ,
130+ keyDigest : '' ,
131+ createdAt : undefined ,
132+ updatedAt : new Date ( ) . toISOString ( ) ,
133+ version : 1
134+ } ) ;
135+
136+ const result = await resolveSecretReferences (
137+ '${SECRET_KEY}' ,
138+ 'dev' ,
139+ '/' ,
140+ mockFetcher ,
141+ null ,
142+ cache
143+ ) ;
144+
145+ expect ( result ) . toBe ( 'secret-value' ) ;
146+ expect ( mockFetcher ) . toHaveBeenCalledWith ( 'dev' , '/' , 'SECRET_KEY' , null ) ;
147+ } ) ;
148+
149+ it ( 'should resolve cross-env secret reference' , async ( ) => {
150+ mockFetcher . mockResolvedValueOnce ( {
151+ value : 'prod-secret' ,
152+ key : 'SECRET_KEY' ,
153+ environment : 'prod' ,
154+ path : '/' ,
155+ id : '1' ,
156+ comment : '' ,
157+ tags : [ ] ,
158+ keyDigest : '' ,
159+ createdAt : undefined ,
160+ updatedAt : new Date ( ) . toISOString ( ) ,
161+ version : 1
162+ } ) ;
163+
164+ const result = await resolveSecretReferences (
165+ '${prod.SECRET_KEY}' ,
166+ 'dev' ,
167+ '/' ,
168+ mockFetcher ,
169+ null ,
170+ cache
171+ ) ;
172+
173+ expect ( result ) . toBe ( 'prod-secret' ) ;
174+ expect ( mockFetcher ) . toHaveBeenCalledWith ( 'prod' , '/' , 'SECRET_KEY' , null ) ;
175+ } ) ;
176+
177+ it ( 'should resolve cross-app secret reference' , async ( ) => {
178+ mockFetcher . mockResolvedValueOnce ( {
179+ value : 'app-secret' ,
180+ key : 'SECRET_KEY' ,
181+ environment : 'dev' ,
182+ path : '/' ,
183+ id : '1' ,
184+ comment : '' ,
185+ tags : [ ] ,
186+ keyDigest : '' ,
187+ createdAt : undefined ,
188+ updatedAt : new Date ( ) . toISOString ( ) ,
189+ version : 1
190+ } ) ;
191+
192+ const result = await resolveSecretReferences (
193+ '${app-name::SECRET_KEY}' ,
194+ 'dev' ,
195+ '/' ,
196+ mockFetcher ,
197+ null ,
198+ cache
199+ ) ;
200+
201+ expect ( result ) . toBe ( 'app-secret' ) ;
202+ expect ( mockFetcher ) . toHaveBeenCalledWith ( 'dev' , '/' , 'SECRET_KEY' , 'app-name' ) ;
203+ } ) ;
204+
205+ it ( 'should resolve nested secret references' , async ( ) => {
206+ // First level reference
207+ mockFetcher . mockResolvedValueOnce ( {
208+ value : '${NESTED_KEY}' ,
209+ key : 'SECRET_KEY' ,
210+ environment : 'dev' ,
211+ path : '/' ,
212+ id : '1' ,
213+ comment : '' ,
214+ tags : [ ] ,
215+ keyDigest : '' ,
216+ createdAt : undefined ,
217+ updatedAt : new Date ( ) . toISOString ( ) ,
218+ version : 1
219+ } ) ;
220+
221+ // Second level reference
222+ mockFetcher . mockResolvedValueOnce ( {
223+ value : 'final-value' ,
224+ key : 'NESTED_KEY' ,
225+ environment : 'dev' ,
226+ path : '/' ,
227+ id : '2' ,
228+ comment : '' ,
229+ tags : [ ] ,
230+ keyDigest : '' ,
231+ createdAt : undefined ,
232+ updatedAt : new Date ( ) . toISOString ( ) ,
233+ version : 1
234+ } ) ;
235+
236+ const result = await resolveSecretReferences (
237+ '${SECRET_KEY}' ,
238+ 'dev' ,
239+ '/' ,
240+ mockFetcher ,
241+ null ,
242+ cache
243+ ) ;
244+
245+ expect ( result ) . toBe ( 'final-value' ) ;
246+ expect ( mockFetcher ) . toHaveBeenCalledTimes ( 2 ) ;
247+ } ) ;
248+
249+ it ( 'should detect circular references' , async ( ) => {
250+ // First level reference
251+ mockFetcher . mockResolvedValueOnce ( {
252+ value : '${CIRCULAR_KEY}' ,
253+ key : 'SECRET_KEY' ,
254+ environment : 'dev' ,
255+ path : '/' ,
256+ id : '1' ,
257+ comment : '' ,
258+ tags : [ ] ,
259+ keyDigest : '' ,
260+ createdAt : undefined ,
261+ updatedAt : new Date ( ) . toISOString ( ) ,
262+ version : 1
263+ } ) ;
264+
265+ // Circular reference
266+ mockFetcher . mockResolvedValueOnce ( {
267+ value : '${SECRET_KEY}' ,
268+ key : 'CIRCULAR_KEY' ,
269+ environment : 'dev' ,
270+ path : '/' ,
271+ id : '2' ,
272+ comment : '' ,
273+ tags : [ ] ,
274+ keyDigest : '' ,
275+ createdAt : undefined ,
276+ updatedAt : new Date ( ) . toISOString ( ) ,
277+ version : 1
278+ } ) ;
279+
280+ await expect ( resolveSecretReferences (
281+ '${SECRET_KEY}' ,
282+ 'dev' ,
283+ '/' ,
284+ mockFetcher ,
285+ null ,
286+ cache
287+ ) ) . rejects . toThrow ( 'Circular reference detected' ) ;
288+ } ) ;
289+
290+ it ( 'should handle multiple references in a single value' , async ( ) => {
291+ mockFetcher
292+ . mockResolvedValueOnce ( {
293+ value : 'first-value' ,
294+ key : 'FIRST_KEY' ,
295+ environment : 'dev' ,
296+ path : '/' ,
297+ id : '1' ,
298+ comment : '' ,
299+ tags : [ ] ,
300+ keyDigest : '' ,
301+ createdAt : undefined ,
302+ updatedAt : new Date ( ) . toISOString ( ) ,
303+ version : 1
304+ } )
305+ . mockResolvedValueOnce ( {
306+ value : 'second-value' ,
307+ key : 'SECOND_KEY' ,
308+ environment : 'dev' ,
309+ path : '/' ,
310+ id : '2' ,
311+ comment : '' ,
312+ tags : [ ] ,
313+ keyDigest : '' ,
314+ createdAt : undefined ,
315+ updatedAt : new Date ( ) . toISOString ( ) ,
316+ version : 1
317+ } ) ;
318+
319+ const result = await resolveSecretReferences (
320+ '${FIRST_KEY}-${SECOND_KEY}' ,
321+ 'dev' ,
322+ '/' ,
323+ mockFetcher ,
324+ null ,
325+ cache
326+ ) ;
327+
328+ expect ( result ) . toBe ( 'first-value-second-value' ) ;
329+ expect ( mockFetcher ) . toHaveBeenCalledTimes ( 2 ) ;
330+ } ) ;
331+
332+ it ( 'should handle folder paths correctly' , async ( ) => {
333+ mockFetcher . mockResolvedValueOnce ( {
334+ value : 'folder-secret' ,
335+ key : 'SECRET_KEY' ,
336+ environment : 'dev' ,
337+ path : '/path/to' ,
338+ id : '1' ,
339+ comment : '' ,
340+ tags : [ ] ,
341+ keyDigest : '' ,
342+ createdAt : undefined ,
343+ updatedAt : new Date ( ) . toISOString ( ) ,
344+ version : 1
345+ } ) ;
346+
347+ const result = await resolveSecretReferences (
348+ '${/path/to/SECRET_KEY}' ,
349+ 'dev' ,
350+ '/' ,
351+ mockFetcher ,
352+ null ,
353+ cache
354+ ) ;
355+
356+ expect ( result ) . toBe ( 'folder-secret' ) ;
357+ expect ( mockFetcher ) . toHaveBeenCalledWith ( 'dev' , '/path/to' , 'SECRET_KEY' , null ) ;
358+ } ) ;
359+
360+ it ( 'should handle mixed references with literals' , async ( ) => {
361+ mockFetcher . mockResolvedValueOnce ( {
362+ value : 'secret-value' ,
363+ key : 'SECRET_KEY' ,
364+ environment : 'dev' ,
365+ path : '/' ,
366+ id : '1' ,
367+ comment : '' ,
368+ tags : [ ] ,
369+ keyDigest : '' ,
370+ createdAt : undefined ,
371+ updatedAt : new Date ( ) . toISOString ( ) ,
372+ version : 1
373+ } ) ;
374+
375+ const result = await resolveSecretReferences (
376+ 'prefix-${SECRET_KEY}-suffix' ,
377+ 'dev' ,
378+ '/' ,
379+ mockFetcher ,
380+ null ,
381+ cache
382+ ) ;
383+
384+ expect ( result ) . toBe ( 'prefix-secret-value-suffix' ) ;
385+ } ) ;
386+ } ) ;
387+ } ) ;
0 commit comments