@@ -8,14 +8,15 @@ import {
88} from '@aws-sdk/client-ssm'
99import {
1010 codeBlockOrThrow ,
11- noMatch ,
12- type StepRunResult ,
1311 type StepRunner ,
14- type StepRunnerArgs ,
12+ regExpMatchedStep ,
1513} from '@nordicsemiconductor/bdd-markdown'
1614import { randomUUID } from 'node:crypto'
1715import { check , not , objectMatching , objectWithKeys } from 'tsmatchers'
1816import { type World } from './run-features.js'
17+ import { Type } from '@sinclair/typebox'
18+ import pRetry from 'p-retry'
19+
1920export const steps = ( {
2021 ssm,
2122} : {
@@ -24,118 +25,133 @@ export const steps = ({
2425 let Names : string [ ] = [ ]
2526 return {
2627 steps : [
27- async ( {
28- step,
29- context,
30- log : {
31- step : { debug } ,
32- } ,
33- } : StepRunnerArgs < World > ) : Promise < StepRunResult > => {
34- const match = / ^ ` (?< value > [ ^ ` ] + ) ` i s s t o r e d i n ` (?< name > [ ^ ` ] + ) ` $ / . exec (
35- step . title ,
36- )
37- if ( match === null ) return noMatch
38- const Name = `/${ context . stackName } /public/${ match . groups ?. name } `
39- const value = match . groups ?. value ?? ''
40- debug ( `${ Name } : ${ value } ` )
41- await ssm . send (
42- new PutParameterCommand ( {
43- Name,
44- Value : `${ value } ` ,
45- Type : ParameterType . STRING ,
46- Overwrite : true ,
28+ regExpMatchedStep (
29+ {
30+ regExp : / ^ ` (?< value > [ ^ ` ] + ) ` i s s t o r e d i n ` (?< name > [ ^ ` ] + ) ` $ / ,
31+ schema : Type . Object ( {
32+ value : Type . String ( { minLength : 1 } ) ,
33+ name : Type . String ( { minLength : 1 } ) ,
4734 } ) ,
48- )
49- Names . push ( Name )
50- } ,
51- async ( {
52- step,
53- context,
54- } : StepRunnerArgs < World > ) : Promise < StepRunResult > => {
55- const match =
56- / ^ a r a n d o m (?< type > s t r i n g | n u m b e r ) i s s t o r e d i n ` (?< storageName > [ ^ ` ] + ) ` $ / . exec (
57- step . title ,
35+ } ,
36+ async ( { context, log : { debug } , match : { name, value } } ) => {
37+ const Name = `/${ context . stackName } /public/${ name } `
38+ debug ( `${ Name } : ${ value } ` )
39+ await ssm . send (
40+ new PutParameterCommand ( {
41+ Name,
42+ Value : `${ value } ` ,
43+ Type : ParameterType . STRING ,
44+ Overwrite : true ,
45+ } ) ,
5846 )
59- if ( match === null ) return noMatch
60- const value =
61- match . groups ?. type === 'string'
62- ? randomUUID ( )
63- : Math . floor ( Math . random ( ) * 1000000 )
64- context [ match . groups ?. storageName ?? '' ] = value
65- } ,
66- async ( {
67- step,
68- context,
69- log : {
70- step : { debug } ,
47+ Names . push ( Name )
48+ } ,
49+ ) ,
50+ regExpMatchedStep (
51+ {
52+ regExp :
53+ / ^ a r a n d o m (?< type > s t r i n g | n u m b e r ) i s s t o r e d i n ` (?< storageName > [ ^ ` ] + ) ` $ / ,
54+ schema : Type . Object ( {
55+ type : Type . Union ( [ Type . Literal ( 'string' ) , Type . Literal ( 'number' ) ] ) ,
56+ storageName : Type . String ( { minLength : 1 } ) ,
57+ } ) ,
58+ } ,
59+ async ( { match : { type, storageName } , context } ) => {
60+ const value =
61+ type === 'string'
62+ ? randomUUID ( )
63+ : Math . floor ( Math . random ( ) * 1000000 )
64+ context [ storageName ?? '' ] = value
7165 } ,
72- } : StepRunnerArgs < World > ) : Promise < StepRunResult > => {
73- const match = / ^ ` (?< name > [ ^ ` ] + ) ` i s d e l e t e d $ / . exec ( step . title )
74- if ( match === null ) return noMatch
75- const Name = `/${ context . stackName } /public/${ match . groups ?. name } `
76- debug ( Name )
77- await ssm . send (
78- new DeleteParameterCommand ( {
79- Name,
66+ ) ,
67+ regExpMatchedStep (
68+ {
69+ regExp : / ^ ` (?< name > [ ^ ` ] + ) ` i s d e l e t e d $ / ,
70+ schema : Type . Object ( {
71+ name : Type . String ( { minLength : 1 } ) ,
8072 } ) ,
81- )
82- Names = Names . filter ( ( n ) => n !== Name )
83- } ,
84- async ( {
85- step,
86- log : {
87- step : { debug } ,
8873 } ,
89- } : StepRunnerArgs < World > ) : Promise < StepRunResult > => {
90- const match =
91- / ^ t h e r e s u l t o f G E T ` (?< url > [ ^ ` ] + ) ` s h o u l d m a t c h t h i s J S O N $ / . exec (
92- step . title ,
74+ async ( { match : { name } , context, log : { debug } } ) => {
75+ const Name = `/${ context . stackName } /public/${ name } `
76+ debug ( Name )
77+ await ssm . send (
78+ new DeleteParameterCommand ( {
79+ Name,
80+ } ) ,
9381 )
94- if ( match === null ) return noMatch
95- const res = await fetch ( match ?. groups ?. url ?? '' )
96- res . headers . forEach ( ( v , k ) => debug ( `${ k } : ${ v } ` ) )
97- const body = await res . text ( )
98- debug ( body )
99- let registry : Record < string , any > = { }
100- try {
101- registry = JSON . parse ( body )
102- } catch {
103- throw new Error ( `Failed to parse body as JSON: ${ body } ` )
104- }
105- check ( registry ) . is (
106- objectMatching ( JSON . parse ( codeBlockOrThrow ( step ) . code ) ) ,
107- )
108- return registry
109- } ,
110- async ( {
111- step,
112- log : {
113- step : { debug } ,
82+ Names = Names . filter ( ( n ) => n !== Name )
11483 } ,
115- } : StepRunnerArgs < World > ) : Promise < StepRunResult > => {
116- const match =
117- / ^ t h e S 3 f i l e ` (?< s3Url > [ ^ ` ] + ) ` s h o u l d n o t h a v e p r o p e r t y ` (?< property > [ ^ ` ] + ) ` $ / . exec (
118- step . title ,
84+ ) ,
85+ regExpMatchedStep (
86+ {
87+ regExp : / ^ t h e r e s u l t o f G E T ` (?< url > [ ^ ` ] + ) ` s h o u l d m a t c h t h i s J S O N $ / ,
88+ schema : Type . Object ( {
89+ url : Type . String ( { minLength : 1 } ) ,
90+ } ) ,
91+ } ,
92+ async ( { match : { url } , step, log : { debug } } ) => {
93+ await pRetry (
94+ async ( ) => {
95+ const res = await fetch ( url ?? '' )
96+ check ( res . ok ) . is ( true )
97+ res . headers . forEach ( ( v , k ) => debug ( `${ k } : ${ v } ` ) )
98+ const body = await res . text ( )
99+ debug ( body )
100+ let result : Record < string , any > = { }
101+ try {
102+ result = JSON . parse ( body )
103+ } catch {
104+ throw new Error ( `Failed to parse body as JSON: ${ body } ` )
105+ }
106+ check ( result ) . is (
107+ objectMatching ( JSON . parse ( codeBlockOrThrow ( step ) . code ) ) ,
108+ )
109+ } ,
110+ {
111+ retries : 5 ,
112+ minTimeout : 5000 ,
113+ factor : 1.5 ,
114+ } ,
119115 )
120- if ( match === null ) return noMatch
121- const [ bucket , file ] = ( match . groups ?. s3Url ?? '' ) . split ( '/' )
122- const res = await new S3Client ( { } ) . send (
123- new GetObjectCommand ( {
124- Bucket : bucket ,
125- Key : file ,
116+ } ,
117+ ) ,
118+ regExpMatchedStep (
119+ {
120+ regExp :
121+ / ^ t h e S 3 f i l e ` (?< s3Url > [ ^ ` ] + ) ` s h o u l d n o t h a v e p r o p e r t y ` (?< property > [ ^ ` ] + ) ` $ / ,
122+ schema : Type . Object ( {
123+ s3Url : Type . String ( { minLength : 1 } ) ,
124+ property : Type . String ( { minLength : 1 } ) ,
126125 } ) ,
127- )
128- const body = ( await res . Body ?. transformToString ( ) ) ?? ''
129- debug ( body )
130- let registry : Record < string , any > = { }
131- try {
132- registry = JSON . parse ( body )
133- } catch {
134- throw new Error ( `Failed to parse body as JSON: ${ body } ` )
135- }
136- check ( registry ) . is ( not ( objectWithKeys ( match . groups ?. property ?? '' ) ) )
137- return registry
138- } ,
126+ } ,
127+ async ( { match : { s3Url, property } , log : { debug } } ) => {
128+ await pRetry (
129+ async ( ) => {
130+ const [ bucket , file ] = ( s3Url ?? '' ) . split ( '/' )
131+ const res = await new S3Client ( { } ) . send (
132+ new GetObjectCommand ( {
133+ Bucket : bucket ,
134+ Key : file ,
135+ } ) ,
136+ )
137+ const body = ( await res . Body ?. transformToString ( ) ) ?? ''
138+ debug ( body )
139+ let result : Record < string , any > = { }
140+ try {
141+ result = JSON . parse ( body )
142+ } catch {
143+ throw new Error ( `Failed to parse body as JSON: ${ body } ` )
144+ }
145+ check ( result ) . is ( not ( objectWithKeys ( property ?? '' ) ) )
146+ } ,
147+ {
148+ retries : 5 ,
149+ minTimeout : 5000 ,
150+ factor : 1.5 ,
151+ } ,
152+ )
153+ } ,
154+ ) ,
139155 ] ,
140156 cleanup : async ( ) => {
141157 if ( Names . length === 0 ) return
0 commit comments