@@ -13,6 +13,7 @@ import (
1313 "net/url"
1414 "os"
1515 "path/filepath"
16+ "regexp"
1617 "sort"
1718 "strconv"
1819 "strings"
@@ -216,6 +217,7 @@ func (l *LocalClient) RegisterSteps(s *godog.ScenarioContext) {
216217 s .Step (`^I should have(.*) response with body, that matches JSON from file$` , l .iShouldHaveResponseWithBodyThatMatchesJSONFromFile )
217218 s .Step (`^I should have(.*) response with body, that matches JSON$` , l .iShouldHaveResponseWithBodyThatMatchesJSON )
218219 s .Step (`^I should have(.*) response with body, that matches JSON paths$` , l .iShouldHaveResponseWithBodyThatMatchesJSONPaths )
220+ s .Step (`^I should have(.*) response with body, that matches regular expression$` , l .iShouldHaveResponseWithBodyThatMatchesRegexp )
219221
220222 s .Step (`^I should have(.*) other responses with status "([^"]*)"$` , l .iShouldHaveOtherResponsesWithStatus )
221223 s .Step (`^I should have(.*) other responses with header "([^"]*): ([^"]*)"$` , l .iShouldHaveOtherResponsesWithHeader )
@@ -226,6 +228,7 @@ func (l *LocalClient) RegisterSteps(s *godog.ScenarioContext) {
226228 s .Step (`^I should have(.*) other responses with body, that matches JSON$` , l .iShouldHaveOtherResponsesWithBodyThatMatchesJSON )
227229 s .Step (`^I should have(.*) other responses with body, that matches JSON from file$` , l .iShouldHaveOtherResponsesWithBodyThatMatchesJSONFromFile )
228230 s .Step (`^I should have(.*) other responses with body, that matches JSON paths$` , l .iShouldHaveOtherResponsesWithBodyThatMatchesJSONPaths )
231+ s .Step (`^I should have(.*) other responses with body, that matches regular expression$` , l .iShouldHaveOtherResponsesWithBodyThatMatchesRegexp )
229232
230233 s .After (l .afterScenario )
231234}
@@ -767,6 +770,99 @@ func (l *LocalClient) iShouldHaveResponseWithBodyThatContains(ctx context.Contex
767770 })
768771}
769772
773+ func (l * LocalClient ) iShouldHaveResponseWithBodyThatMatchesRegexp (ctx context.Context , service , bodyDoc string ) (context.Context , error ) {
774+ ctx = l .VS .PrepareContext (ctx )
775+
776+ return l .expectResponse (ctx , service , func (c * httpmock.Client ) error {
777+ return c .ExpectResponseBodyCallback (func (received []byte ) error {
778+ return l .matches (ctx , received , bodyDoc )
779+ })
780+ })
781+ }
782+
783+ func (l * LocalClient ) matches (ctx context.Context , received []byte , pattern string ) error {
784+ capture , matched , err := l .extractNamedMatches (received , pattern )
785+ if err != nil {
786+ return err
787+ }
788+
789+ if ! matched {
790+ return augmentBodyErr (ctx , fmt .Errorf ("%w %q in %q" , errDoesNotContain , pattern , received ))
791+ }
792+
793+ _ , vs := l .VS .Vars (ctx )
794+
795+ for k , v := range capture {
796+ k := "$" + k
797+
798+ _ , err := l .varCollected (vs , k , string (v ))
799+ if err != nil {
800+ return augmentBodyErr (ctx , err )
801+ }
802+ }
803+
804+ return nil
805+ }
806+
807+ func (l * LocalClient ) extractNamedMatches (data []byte , pattern string ) (map [string ][]byte , bool , error ) {
808+ re , err := regexp .Compile (pattern )
809+ if err != nil {
810+ return nil , false , err
811+ }
812+
813+ indices := re .FindSubmatchIndex (data )
814+ if indices == nil {
815+ return nil , false , nil // no full match
816+ }
817+
818+ names := re .SubexpNames ()
819+ result := make (map [string ][]byte , len (names )- 1 )
820+
821+ // i=0 → full match
822+ // i≥1 → capture groups
823+ for i , name := range names {
824+ if i == 0 || name == "" {
825+ continue // skip whole match and unnamed groups
826+ }
827+
828+ start , end := indices [i * 2 ], indices [i * 2 + 1 ]
829+ if start == - 1 {
830+ result [name ] = nil // optional group didn't match
831+
832+ continue
833+ }
834+
835+ result [name ] = data [start :end ] // zero-copy []byte slice
836+ }
837+
838+ return result , true , nil
839+ }
840+
841+ func (l * LocalClient ) varCollected (vs * shared.Vars , s string , v interface {}) (bool , error ) {
842+ if vs == nil || ! vs .IsVar (s ) {
843+ return false , nil
844+ }
845+
846+ if n , ok := v .(json.Number ); ok {
847+ v = shared .DecodeJSONNumber (n )
848+ } else if f , ok := v .(float64 ); ok && f == float64 (int64 (f )) {
849+ v = int64 (f )
850+ }
851+
852+ fv , found := vs .Get (s )
853+ if ! found {
854+ vs .Set (s , v )
855+
856+ return true , nil
857+ }
858+
859+ if fv != v {
860+ return false , fmt .Errorf ("unexpected variable %s value, expected %v, received %v" , s , fv , v )
861+ }
862+
863+ return false , nil
864+ }
865+
770866func (l * LocalClient ) iShouldHaveOtherResponsesWithBodyThatContains (ctx context.Context , service , bodyDoc string ) (context.Context , error ) {
771867 ctx = l .VS .PrepareContext (ctx )
772868
@@ -865,6 +961,16 @@ func (l *LocalClient) iShouldHaveOtherResponsesWithBodyThatMatchesJSONPaths(ctx
865961 })
866962}
867963
964+ func (l * LocalClient ) iShouldHaveOtherResponsesWithBodyThatMatchesRegexp (ctx context.Context , service , bodyDoc string ) (context.Context , error ) {
965+ ctx = l .VS .PrepareContext (ctx )
966+
967+ return l .expectResponse (ctx , service , func (c * httpmock.Client ) error {
968+ return c .ExpectOtherResponsesBodyCallback (func (received []byte ) error {
969+ return l .matches (ctx , received , bodyDoc )
970+ })
971+ })
972+ }
973+
868974func (l * LocalClient ) iShouldHaveOtherResponsesWithBodyThatMatchesJSONFromFile (ctx context.Context , service , filePath string ) (context.Context , error ) {
869975 ctx = l .VS .PrepareContext (ctx )
870976
0 commit comments