1- import { getParameter , getParameters } from '@aws-github-runner/aws-ssm-util' ;
1+ import { getParameter } from '@aws-github-runner/aws-ssm-util' ;
2+ import { getRunnerMatcherConfigStore , type RunnerMatcherConfigStore } from '@aws-github-runner/storage-providers' ;
23import { ConfigWebhook , ConfigWebhookEventBridge , ConfigDispatcher } from './ConfigLoader' ;
34
45import { logger } from '@aws-github-runner/aws-powertools-util' ;
56import { RunnerMatcherConfig } from './sqs' ;
67import { describe , it , expect , beforeEach , vi } from 'vitest' ;
78
89vi . mock ( '@aws-github-runner/aws-ssm-util' ) ;
10+ vi . mock ( '@aws-github-runner/storage-providers' ) ;
11+
12+ const runnerMatcherConfigStore = {
13+ get : vi . fn ( ) ,
14+ } satisfies RunnerMatcherConfigStore ;
915
1016describe ( 'ConfigLoader Tests' , ( ) => {
1117 beforeEach ( ( ) => {
@@ -14,6 +20,7 @@ describe('ConfigLoader Tests', () => {
1420 ConfigWebhookEventBridge . reset ( ) ;
1521 ConfigDispatcher . reset ( ) ;
1622 logger . setLogLevel ( 'DEBUG' ) ;
23+ vi . mocked ( getRunnerMatcherConfigStore ) . mockReturnValue ( runnerMatcherConfigStore ) ;
1724
1825 // clear process.env
1926 for ( const key of Object . keys ( process . env ) ) {
@@ -24,7 +31,6 @@ describe('ConfigLoader Tests', () => {
2431 describe ( 'Check base object' , ( ) => {
2532 function setupConfiguration ( ) : void {
2633 process . env . EVENT_BUS_NAME = 'event-bus' ;
27- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config' ;
2834 process . env . PARAMETER_GITHUB_APP_WEBHOOK_SECRET = '/path/to/webhook/secret' ;
2935 const matcherConfig = [
3036 {
@@ -36,15 +42,8 @@ describe('ConfigLoader Tests', () => {
3642 } ,
3743 } ,
3844 ] ;
39- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
40- if ( paramPath === '/path/to/matcher/config' ) {
41- return JSON . stringify ( matcherConfig ) ;
42- }
43- if ( paramPath === '/path/to/webhook/secret' ) {
44- return 'secret' ;
45- }
46- return '' ;
47- } ) ;
45+ runnerMatcherConfigStore . get . mockResolvedValue ( JSON . stringify ( matcherConfig ) ) ;
46+ vi . mocked ( getParameter ) . mockResolvedValue ( 'secret' ) ;
4847 }
4948
5049 it ( 'should return the same instance of ConfigWebhook (singleton)' , async ( ) => {
@@ -53,7 +52,8 @@ describe('ConfigLoader Tests', () => {
5352 const config2 = await ConfigWebhook . load ( ) ;
5453
5554 expect ( config1 ) . toBe ( config2 ) ;
56- expect ( getParameter ) . toHaveBeenCalledTimes ( 2 ) ;
55+ expect ( getParameter ) . toHaveBeenCalledOnce ( ) ;
56+ expect ( runnerMatcherConfigStore . get ) . toHaveBeenCalledOnce ( ) ;
5757 } ) ;
5858
5959 it ( 'should return the same instance of ConfigWebhookEventBridge (singleton)' , async ( ) => {
@@ -63,6 +63,7 @@ describe('ConfigLoader Tests', () => {
6363
6464 expect ( config1 ) . toBe ( config2 ) ;
6565 expect ( getParameter ) . toHaveBeenCalledTimes ( 1 ) ;
66+ expect ( runnerMatcherConfigStore . get ) . not . toHaveBeenCalled ( ) ;
6667 } ) ;
6768
6869 it ( 'should return the same instance of ConfigDispatcher (singleton)' , async ( ) => {
@@ -71,7 +72,8 @@ describe('ConfigLoader Tests', () => {
7172 const config2 = await ConfigDispatcher . load ( ) ;
7273
7374 expect ( config1 ) . toBe ( config2 ) ;
74- expect ( getParameter ) . toHaveBeenCalledTimes ( 1 ) ;
75+ expect ( getParameter ) . not . toHaveBeenCalled ( ) ;
76+ expect ( runnerMatcherConfigStore . get ) . toHaveBeenCalledOnce ( ) ;
7577 } ) ;
7678
7779 it ( 'should filter secrets from being logged' , async ( ) => {
@@ -95,7 +97,6 @@ describe('ConfigLoader Tests', () => {
9597 it ( 'should load config successfully' , async ( ) => {
9698 process . env . REPOSITORY_ALLOW_LIST = '["repo1", "repo2"]' ;
9799 process . env . PARAMETER_GITHUB_APP_WEBHOOK_SECRET = '/path/to/webhook/secret' ;
98- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config' ;
99100 const matcherConfig = [
100101 {
101102 id : '1' ,
@@ -106,15 +107,8 @@ describe('ConfigLoader Tests', () => {
106107 } ,
107108 } ,
108109 ] ;
109- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
110- if ( paramPath === '/path/to/matcher/config' ) {
111- return JSON . stringify ( matcherConfig ) ;
112- }
113- if ( paramPath === '/path/to/webhook/secret' ) {
114- return 'secret' ;
115- }
116- return '' ;
117- } ) ;
110+ runnerMatcherConfigStore . get . mockResolvedValue ( JSON . stringify ( matcherConfig ) ) ;
111+ vi . mocked ( getParameter ) . mockResolvedValue ( 'secret' ) ;
118112
119113 const config : ConfigWebhook = await ConfigWebhook . load ( ) ;
120114
@@ -124,7 +118,6 @@ describe('ConfigLoader Tests', () => {
124118 } ) ;
125119
126120 it ( 'should load config successfully' , async ( ) => {
127- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config' ;
128121 process . env . PARAMETER_GITHUB_APP_WEBHOOK_SECRET = '/path/to/webhook/secret' ;
129122 const matcherConfig = [
130123 {
@@ -136,15 +129,8 @@ describe('ConfigLoader Tests', () => {
136129 } ,
137130 } ,
138131 ] ;
139- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
140- if ( paramPath === '/path/to/matcher/config' ) {
141- return JSON . stringify ( matcherConfig ) ;
142- }
143- if ( paramPath === '/path/to/webhook/secret' ) {
144- return 'secret' ;
145- }
146- return '' ;
147- } ) ;
132+ runnerMatcherConfigStore . get . mockResolvedValue ( JSON . stringify ( matcherConfig ) ) ;
133+ vi . mocked ( getParameter ) . mockResolvedValue ( 'secret' ) ;
148134
149135 const config : ConfigWebhook = await ConfigWebhook . load ( ) ;
150136
@@ -155,74 +141,42 @@ describe('ConfigLoader Tests', () => {
155141 } ) ;
156142
157143 it ( 'should throw error if config loading fails' , async ( ) => {
158- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config' ;
159-
160- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
161- if ( paramPath === '/path/to/matcher/config' ) {
162- throw new Error ( 'Failed to load matcher config' ) ;
163- }
164- return '' ;
165- } ) ;
144+ runnerMatcherConfigStore . get . mockRejectedValue (
145+ new Error (
146+ 'Failed to load parameter for matcherConfig from path /path/to/matcher/config: Failed to load matcher config' ,
147+ ) ,
148+ ) ;
149+ vi . mocked ( getParameter ) . mockResolvedValue ( '' ) ;
166150
167151 await expect ( ConfigWebhook . load ( ) ) . rejects . toThrow (
168152 'Failed to load config: Failed to load parameter for matcherConfig from path /path/to/matcher/config: Failed to load matcher config' ,
169153 ) ;
170154 } ) ;
171155
172- it ( 'should load config successfully from multiple paths' , async ( ) => {
173- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config-1:/path/to/matcher/config-2' ;
156+ it ( 'should load combined matcher config returned by the store' , async ( ) => {
174157 process . env . PARAMETER_GITHUB_APP_WEBHOOK_SECRET = '/path/to/webhook/secret' ;
175158
176- const partialMatcher1 =
177- '[{"id":"1","arn":"arn:aws:sqs:queue1","matcherConfig":{"labelMatchers":[["a"]],"exactMatch":true}}' ;
178- const partialMatcher2 =
179- ',{"id":"2","arn":"arn:aws:sqs:queue2","matcherConfig":{"labelMatchers":[["b"]],"exactMatch":true}}]' ;
180-
181159 const combinedMatcherConfig = [
182160 { id : '1' , arn : 'arn:aws:sqs:queue1' , matcherConfig : { labelMatchers : [ [ 'a' ] ] , exactMatch : true } } ,
183161 { id : '2' , arn : 'arn:aws:sqs:queue2' , matcherConfig : { labelMatchers : [ [ 'b' ] ] , exactMatch : true } } ,
184162 ] ;
185-
186- // Mock getParameters for batch fetching multiple paths
187- vi . mocked ( getParameters ) . mockResolvedValue (
188- new Map ( [
189- [ '/path/to/matcher/config-1' , partialMatcher1 ] ,
190- [ '/path/to/matcher/config-2' , partialMatcher2 ] ,
191- ] ) ,
192- ) ;
193-
194- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
195- if ( paramPath === '/path/to/webhook/secret' ) return 'secret' ;
196- return '' ;
197- } ) ;
163+ runnerMatcherConfigStore . get . mockResolvedValue ( JSON . stringify ( combinedMatcherConfig ) ) ;
164+ vi . mocked ( getParameter ) . mockResolvedValue ( 'secret' ) ;
198165
199166 const config : ConfigWebhook = await ConfigWebhook . load ( ) ;
200167
201168 expect ( config . matcherConfig ) . toEqual ( combinedMatcherConfig ) ;
202169 expect ( config . webhookSecret ) . toBe ( 'secret' ) ;
203170 } ) ;
204171
205- it ( 'should throw error if config loading fails from multiple paths' , async ( ) => {
206- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config-1:/path/to/matcher/config-2' ;
172+ it ( 'should propagate an error from the matcher config store' , async ( ) => {
207173 process . env . PARAMETER_GITHUB_APP_WEBHOOK_SECRET = '/path/to/webhook/secret' ;
208-
209- const partialMatcher1 =
210- '[{"id":"1","arn":"arn:aws:sqs:queue1","matcherConfig":{"labelMatchers":[["a"]],"exactMatch":true}}' ;
211- const partialMatcher2 =
212- ',{"id":"2","arn":"arn:aws:sqs:queue2","matcherConfig":{"labelMatchers":[["b"]],"exactMatch":true}}' ;
213-
214- // Mock getParameters for batch fetching - returns incomplete JSON that will fail to parse
215- vi . mocked ( getParameters ) . mockResolvedValue (
216- new Map ( [
217- [ '/path/to/matcher/config-1' , partialMatcher1 ] ,
218- [ '/path/to/matcher/config-2' , partialMatcher2 ] ,
219- ] ) ,
174+ runnerMatcherConfigStore . get . mockRejectedValue (
175+ new Error (
176+ "Failed to load/parse combined matcher config: Expected ',' or ']' after array element in JSON at position 196" ,
177+ ) ,
220178 ) ;
221-
222- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
223- if ( paramPath === '/path/to/webhook/secret' ) return 'secret' ;
224- return '' ;
225- } ) ;
179+ vi . mocked ( getParameter ) . mockResolvedValue ( 'secret' ) ;
226180
227181 await expect ( ConfigWebhook . load ( ) ) . rejects . toThrow (
228182 "Failed to load config: Failed to load/parse combined matcher config: Expected ',' or ']' after array element in JSON at position 196" ,
@@ -248,6 +202,7 @@ describe('ConfigLoader Tests', () => {
248202 expect ( config . allowedEvents ) . toEqual ( [ 'push' , 'pull_request' ] ) ;
249203 expect ( config . eventBusName ) . toBe ( 'event-bus' ) ;
250204 expect ( config . webhookSecret ) . toBe ( 'secret' ) ;
205+ expect ( runnerMatcherConfigStore . get ) . not . toHaveBeenCalled ( ) ;
251206 } ) ;
252207
253208 it ( 'should throw error if config loading fails' , async ( ) => {
@@ -264,7 +219,6 @@ describe('ConfigLoader Tests', () => {
264219 describe ( 'ConfigDispatcher' , ( ) => {
265220 it ( 'should load config successfully' , async ( ) => {
266221 process . env . REPOSITORY_ALLOW_LIST = '["repo1", "repo2"]' ;
267- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config' ;
268222
269223 const matcherConfig : RunnerMatcherConfig [ ] = [
270224 {
@@ -276,40 +230,22 @@ describe('ConfigLoader Tests', () => {
276230 } ,
277231 } ,
278232 ] ;
279- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
280- if ( paramPath === '/path/to/matcher/config' ) {
281- return JSON . stringify ( matcherConfig ) ;
282- }
283- return '' ;
284- } ) ;
233+ runnerMatcherConfigStore . get . mockResolvedValue ( JSON . stringify ( matcherConfig ) ) ;
285234
286235 const config : ConfigDispatcher = await ConfigDispatcher . load ( ) ;
287236
288237 expect ( config . repositoryAllowList ) . toEqual ( [ 'repo1' , 'repo2' ] ) ;
289238 expect ( config . matcherConfig ) . toEqual ( matcherConfig ) ;
290239 } ) ;
291240
292- it ( 'should load config successfully from multiple paths with repo allow list' , async ( ) => {
241+ it ( 'should load combined matcher config returned by the store with repo allow list' , async ( ) => {
293242 process . env . REPOSITORY_ALLOW_LIST = '["repo1", "repo2"]' ;
294- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config-1:/path/to/matcher/config-2' ;
295-
296- const partial1 =
297- '[{"id":"1","arn":"arn:aws:sqs:queue1","matcherConfig":{"labelMatchers":[["x"]],"exactMatch":true}}' ;
298- const partial2 =
299- ',{"id":"2","arn":"arn:aws:sqs:queue2","matcherConfig":{"labelMatchers":[["y"]],"exactMatch":true}}]' ;
300243
301244 const combined : RunnerMatcherConfig [ ] = [
302245 { id : '1' , arn : 'arn:aws:sqs:queue1' , matcherConfig : { labelMatchers : [ [ 'x' ] ] , exactMatch : true } } ,
303246 { id : '2' , arn : 'arn:aws:sqs:queue2' , matcherConfig : { labelMatchers : [ [ 'y' ] ] , exactMatch : true } } ,
304247 ] ;
305-
306- // Mock getParameters for batch fetching multiple paths
307- vi . mocked ( getParameters ) . mockResolvedValue (
308- new Map ( [
309- [ '/path/to/matcher/config-1' , partial1 ] ,
310- [ '/path/to/matcher/config-2' , partial2 ] ,
311- ] ) ,
312- ) ;
248+ runnerMatcherConfigStore . get . mockResolvedValue ( JSON . stringify ( combined ) ) ;
313249
314250 const config : ConfigDispatcher = await ConfigDispatcher . load ( ) ;
315251
@@ -318,18 +254,15 @@ describe('ConfigLoader Tests', () => {
318254 } ) ;
319255
320256 it ( 'should throw error if config loading fails' , async ( ) => {
321- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
322- throw new Error ( `Parameter ${ paramPath } not found` ) ;
323- } ) ;
257+ runnerMatcherConfigStore . get . mockRejectedValue ( new Error ( 'Matcher config store is unavailable' ) ) ;
324258
325259 await expect ( ConfigDispatcher . load ( ) ) . rejects . toThrow (
326- 'Failed to load config: Failed to load parameter for matcherConfig from path undefined: Parameter undefined not found ' ,
260+ 'Failed to load config: Matcher config store is unavailable ' ,
327261 ) ;
328262 } ) ;
329263
330264 it ( 'should rely on default when optionals are not set.' , async ( ) => {
331265 process . env . ACCEPT_EVENTS = 'null' ;
332- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config' ;
333266 const matcherConfig : RunnerMatcherConfig [ ] = [
334267 {
335268 arn : 'arn:aws:sqs:eu-central-1:123456:npalm-default-queued-builds' ,
@@ -340,12 +273,7 @@ describe('ConfigLoader Tests', () => {
340273 } ,
341274 } ,
342275 ] ;
343- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
344- if ( paramPath === '/path/to/matcher/config' ) {
345- return JSON . stringify ( matcherConfig ) ;
346- }
347- return '' ;
348- } ) ;
276+ runnerMatcherConfigStore . get . mockResolvedValue ( JSON . stringify ( matcherConfig ) ) ;
349277
350278 const config : ConfigDispatcher = await ConfigDispatcher . load ( ) ;
351279
@@ -355,14 +283,7 @@ describe('ConfigLoader Tests', () => {
355283
356284 it ( 'should throw an error if runner matcher config is empty.' , async ( ) => {
357285 process . env . REPOSITORY_ALLOW_LIST = '["repo1", "repo2"]' ;
358- process . env . PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config' ;
359-
360- vi . mocked ( getParameter ) . mockImplementation ( async ( paramPath : string ) => {
361- if ( paramPath === '/path/to/matcher/config' ) {
362- return JSON . stringify ( '' ) ;
363- }
364- return '' ;
365- } ) ;
286+ runnerMatcherConfigStore . get . mockResolvedValue ( JSON . stringify ( '' ) ) ;
366287
367288 await expect ( ConfigDispatcher . load ( ) ) . rejects . toThrow ( 'Failed to load config: Matcher config is empty' ) ;
368289 } ) ;
0 commit comments