• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pact-foundation / pact-go / 5504842958

pending completion
5504842958

push

github

web-flow
Merge pull request #303 from pact-foundation/fix/issue-288

Fix/issue 288

0 of 44 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1843 of 4936 relevant lines covered (37.34%)

5.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/provider/verifier.go
1
package provider
2

3
import (
4
        "encoding/json"
5
        "fmt"
6
        "io"
7
        "io/ioutil"
8
        "log"
9
        "net"
10
        "net/http"
11
        "net/url"
12
        "strings"
13
        "testing"
14
        "time"
15

16
        "github.com/pact-foundation/pact-go/v2/command"
17
        "github.com/pact-foundation/pact-go/v2/internal/native"
18
        logging "github.com/pact-foundation/pact-go/v2/log"
19
        "github.com/pact-foundation/pact-go/v2/message"
20
        "github.com/pact-foundation/pact-go/v2/models"
21
        "github.com/pact-foundation/pact-go/v2/proxy"
22
        "github.com/pact-foundation/pact-go/v2/utils"
23
)
24

25
const MESSAGE_PATH = "/__messages"
26

27
// Verifier is used to verify the provider side of an HTTP API contract
28
type Verifier struct {
29
        // ClientTimeout specifies how long to wait for the provider to start
30
        // Can be increased to reduce likelihood of intermittent failure
31
        // Defaults to 10s
32
        ClientTimeout time.Duration
33

34
        // Hostname to run any
35
        Hostname string
36

37
        handle *native.Verifier
38
}
39

40
func NewVerifier() *Verifier {
×
41
        native.Init(string(logging.LogLevel()))
×
42

×
43
        return &Verifier{
×
44
                handle: native.NewVerifier("pact-go", command.Version),
×
45
        }
×
46

×
47
}
×
48

49
func (v *Verifier) validateConfig() error {
×
50
        if v.ClientTimeout == 0 {
×
51
                v.ClientTimeout = 10 * time.Second
×
52
        }
×
53
        if v.Hostname == "" {
×
54
                v.Hostname = "localhost"
×
55
        }
×
56

57
        return nil
×
58
}
59

60
// If no HTTP server is given, we must start one up in order
61
// to provide a target for state changes etc.
62
func (v *Verifier) startDefaultHTTPServer(port int) {
×
63
        mux := http.NewServeMux()
×
64

×
65
        http.ListenAndServe(fmt.Sprintf("%s:%d", v.Hostname, port), mux)
×
66
}
×
67

68
// VerifyProviderRaw reads the provided pact files and runs verification against
69
// a running Provider API, providing raw response from the Verification process.
70
//
71
// Order of events: BeforeEach, stateHandlers, requestFilter(pre <execute provider> post), AfterEach
72
func (v *Verifier) verifyProviderRaw(request VerifyRequest, writer outputWriter) error {
×
73

×
74
        // proxy target
×
75
        var u *url.URL
×
76

×
77
        err := v.validateConfig()
×
78
        if err != nil {
×
79
                return err
×
80
        }
×
81

82
        // Check if a provider has been given. If none, start a dummy service to attach the proxy to
83
        if request.ProviderBaseURL == "" {
×
84
                log.Println("[DEBUG] setting up a dummy server for verification, as none was provided")
×
85
                port, err := utils.GetFreePort()
×
86
                if err != nil {
×
87
                        log.Panic("unable to allocate a port for verification:", err)
×
88
                }
×
89
                go v.startDefaultHTTPServer(port)
×
90

×
91
                request.ProviderBaseURL = fmt.Sprintf("http://localhost:%d", port)
×
92
        }
93

94
        u, err = url.Parse(request.ProviderBaseURL)
×
95
        if err != nil {
×
96
                log.Panic("unable to parse the provider URL", err)
×
97
        }
×
98

99
        m := []proxy.Middleware{}
×
100

×
101
        if request.BeforeEach != nil {
×
102
                m = append(m, beforeEachMiddleware(request.BeforeEach))
×
103
        }
×
104

105
        if len(request.StateHandlers) > 0 {
×
NEW
106
                m = append(m, stateHandlerMiddleware(request.StateHandlers, request.AfterEach))
×
107
        }
×
108

109
        if len(request.MessageHandlers) > 0 {
×
110
                m = append(m, message.CreateMessageHandler(request.MessageHandlers))
×
111
        }
×
112

113
        if request.RequestFilter != nil {
×
114
                m = append(m, request.RequestFilter)
×
115
        }
×
116

117
        // Configure HTTP Verification Proxy
118
        opts := proxy.Options{
×
119
                TargetAddress:             fmt.Sprintf("%s:%s", u.Hostname(), u.Port()),
×
120
                TargetScheme:              u.Scheme,
×
121
                TargetPath:                u.Path,
×
122
                Middleware:                m,
×
123
                InternalRequestPathPrefix: providerStatesSetupPath,
×
124
                CustomTLSConfig:           request.CustomTLSConfig,
×
125
        }
×
126

×
127
        // Starts the message wrapper API with hooks back to the state handlers
×
128
        // This maps the 'description' field of a message pact, to a function handler
×
129
        // that will implement the message producer. This function must return an object and optionally
×
130
        // and error. The object will be marshalled to JSON for comparison.
×
131
        port, err := proxy.HTTPReverseProxy(opts)
×
132

×
133
        if err != nil {
×
134
                return err
×
135
        }
×
136

137
        // Add any message targets
138
        if len(request.MessageHandlers) > 0 {
×
139
                request.Transports = append(request.Transports, Transport{
×
140
                        Path:     MESSAGE_PATH,
×
141
                        Protocol: "message",
×
142
                        Port:     uint16(port),
×
143
                })
×
144
        }
×
145

146
        // Backwards compatibility, setup old provider states URL if given
147
        // Otherwise point to proxy
148
        if request.ProviderStatesSetupURL == "" && len(request.StateHandlers) > 0 {
×
149
                request.ProviderStatesSetupURL = fmt.Sprintf("http://localhost:%d%s", port, providerStatesSetupPath)
×
150
        }
×
151

152
        // Provider target should be the proxy
153
        request.ProviderBaseURL = fmt.Sprintf("http://localhost:%d", port)
×
154

×
155
        err = request.validate(v.handle)
×
156
        if err != nil {
×
157
                return err
×
158
        }
×
159

160
        portErr := WaitForPort(port, "tcp", "localhost", v.ClientTimeout,
×
161
                fmt.Sprintf(`Timed out waiting for http verification proxy on port %d - check for errors`, port))
×
162

×
163
        if portErr != nil {
×
164
                log.Fatal("Error:", err)
×
165
                return portErr
×
166
        }
×
167

168
        log.Println("[DEBUG] pact provider verification")
×
169

×
170
        return request.Verify(v.handle, writer)
×
171
}
172

173
// VerifyProvider accepts an instance of `*testing.T`
174
// running the provider verification with granular test reporting and
175
// automatic failure reporting for nice, simple tests.
176
func (v *Verifier) VerifyProvider(t *testing.T, request VerifyRequest) error {
×
177
        err := v.verifyProviderRaw(request, t)
×
178

×
179
        // TODO: granular test reporting
×
180
        // runTestCases(t, res)
×
181

×
182
        t.Run("Provider pact verification", func(t *testing.T) {
×
183
                if err != nil {
×
184
                        t.Error(err)
×
185
                }
×
186
        })
187

188
        return err
×
189
}
190

191
// beforeEachMiddleware is invoked before any other, only on the __setup
192
// request (to avoid duplication)
193
func beforeEachMiddleware(BeforeEach Hook) proxy.Middleware {
×
194
        return func(next http.Handler) http.Handler {
×
195
                return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
×
196
                        if r.URL.Path == providerStatesSetupPath {
×
NEW
197
                                state, err := getStateFromRequest(r)
×
198

×
NEW
199
                                // Before each should only fire on the "setup" phase
×
NEW
200
                                if err == nil && state.Action == "setup" {
×
NEW
201
                                        log.Println("[DEBUG] executing before hook")
×
NEW
202
                                        err := BeforeEach()
×
203

×
NEW
204
                                        if err != nil {
×
NEW
205
                                                log.Println("[ERROR] error executing before hook:", err)
×
NEW
206
                                                w.WriteHeader(http.StatusInternalServerError)
×
NEW
207
                                        }
×
208
                                }
209
                        }
210
                        next.ServeHTTP(w, r)
×
211
                })
212
        }
213
}
214

215
// {"action":"teardown","id":"foo","state":"User foo exists"}
216
type stateHandlerAction struct {
217
        Action string `json:"action"`
218
        State  string `json:"state"`
219
        Params map[string]interface{}
220
}
221

NEW
222
func getStateFromRequest(r *http.Request) (stateHandlerAction, error) {
×
NEW
223
        var state stateHandlerAction
×
NEW
224
        buf := new(strings.Builder)
×
NEW
225
        tr := io.TeeReader(r.Body, buf)
×
NEW
226
        io.ReadAll(tr)
×
NEW
227

×
NEW
228
        // Body is consumed above, need to put it back after ;P
×
NEW
229
        r.Body = ioutil.NopCloser(strings.NewReader(buf.String()))
×
NEW
230
        log.Println("[TRACE] getStateFromRequest received raw input", buf.String())
×
NEW
231

×
NEW
232
        err := json.Unmarshal([]byte(buf.String()), &state)
×
NEW
233
        log.Println("[TRACE] getStateFromRequest parsed input (without params)", state)
×
NEW
234

×
NEW
235
        if err != nil {
×
NEW
236
                log.Println("[ERROR] getStateFromRequest unable to decode incoming state change payload", err)
×
NEW
237
                return stateHandlerAction{}, err
×
NEW
238
        }
×
239

NEW
240
        return state, nil
×
241
}
242

243
// stateHandlerMiddleware responds to the various states that are
244
// given during provider verification
245
//
246
// statehandler accepts a state object from the verifier and executes
247
// any state handlers associated with the provider.
248
// It will not execute further middleware if it is the designted "state" request
NEW
249
func stateHandlerMiddleware(stateHandlers models.StateHandlers, afterEach Hook) proxy.Middleware {
×
250
        return func(next http.Handler) http.Handler {
×
251
                return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
×
252
                        if r.URL.Path == providerStatesSetupPath {
×
253
                                log.Println("[INFO] executing state handler middleware")
×
254
                                var state stateHandlerAction
×
255
                                buf := new(strings.Builder)
×
NEW
256
                                tr := io.TeeReader(r.Body, buf)
×
NEW
257
                                io.ReadAll(tr)
×
NEW
258

×
NEW
259
                                // Body is consumed above, need to put it back after ;P
×
NEW
260
                                r.Body = ioutil.NopCloser(strings.NewReader(buf.String()))
×
261
                                log.Println("[TRACE] state handler received raw input", buf.String())
×
262

×
263
                                err := json.Unmarshal([]byte(buf.String()), &state)
×
264
                                log.Println("[TRACE] state handler parsed input (without params)", state)
×
265

×
266
                                if err != nil {
×
267
                                        log.Println("[ERROR] unable to decode incoming state change payload", err)
×
268
                                        w.WriteHeader(http.StatusInternalServerError)
×
269
                                        return
×
270
                                }
×
271

272
                                // Extract the params from the payload. They are in the root, so we need to do some more work to achieve this
273
                                var params models.ProviderStateResponse
×
274
                                err = json.Unmarshal([]byte(buf.String()), &params)
×
275
                                if err != nil {
×
276
                                        log.Println("[ERROR] unable to decode incoming state change payload", err)
×
277
                                        w.WriteHeader(http.StatusInternalServerError)
×
278
                                        return
×
279
                                }
×
280

281
                                // TODO: update rust code - params should be in a sub-property, to avoid top-level key conflicts
282
                                // i.e. it's possible action/state are actually something a users wants to supply
283
                                delete(params, "action")
×
284
                                delete(params, "state")
×
285
                                state.Params = params
×
286
                                log.Println("[TRACE] state handler completed parsing input (with params)", state)
×
287

×
288
                                // Find the provider state handler
×
289
                                sf, stateFound := stateHandlers[state.State]
×
290

×
291
                                if !stateFound {
×
292
                                        log.Printf("[WARN] no state handler found for state: %v", state.State)
×
293
                                } else {
×
294
                                        // Execute state handler
×
295
                                        res, err := sf(state.Action == "setup", models.ProviderState{Name: state.State, Parameters: state.Params})
×
296

×
297
                                        if err != nil {
×
298
                                                log.Printf("[ERROR] state handler for '%v' errored: %v", state.State, err)
×
299
                                                w.WriteHeader(http.StatusInternalServerError)
×
300
                                                return
×
301
                                        }
×
302

NEW
303
                                        if state.Action == "teardown" && afterEach != nil {
×
NEW
304
                                                err := afterEach()
×
NEW
305

×
NEW
306
                                                if err != nil {
×
NEW
307
                                                        log.Printf("[ERROR] after each hook for test errored: %v", err)
×
NEW
308
                                                        w.WriteHeader(http.StatusInternalServerError)
×
NEW
309
                                                        return
×
NEW
310
                                                }
×
311
                                        }
312

313
                                        // Return provider state values for generator
314
                                        if res != nil {
×
315
                                                log.Println("[TRACE] returning values from provider state (raw)", res)
×
316
                                                resBody, err := json.Marshal(res)
×
317
                                                log.Println("[TRACE] returning values from provider state (JSON)", string(resBody))
×
318

×
319
                                                if err != nil {
×
320
                                                        log.Printf("[ERROR] state handler for '%v' errored: %v", state.State, err)
×
321
                                                        w.WriteHeader(http.StatusInternalServerError)
×
322

×
323
                                                        return
×
324
                                                }
×
325

326
                                                w.Header().Add("content-type", "application/json")
×
NEW
327
                                                w.WriteHeader(http.StatusOK)
×
UNCOV
328
                                                w.Write(resBody)
×
NEW
329
                                                return
×
330
                                        }
331
                                }
332

333
                                w.WriteHeader(http.StatusOK)
×
334
                                return
×
335
                        }
336

337
                        log.Println("[TRACE] skipping state handler for request", r.RequestURI)
×
338

×
339
                        // Pass through to application
×
340
                        next.ServeHTTP(w, r)
×
341
                })
342
        }
343
}
344

345
// Use this to wait for a port to be running prior
346
// to running tests.
347
func WaitForPort(port int, network string, address string, timeoutDuration time.Duration, message string) error {
×
348
        log.Println("[DEBUG] waiting for port", port, "to become available")
×
349
        timeout := time.After(timeoutDuration)
×
350

×
351
        for {
×
352
                select {
×
353
                case <-timeout:
×
354
                        log.Printf("[ERROR] expected server to start < %s. %s", timeoutDuration, message)
×
355
                        return fmt.Errorf("expected server to start < %s. %s", timeoutDuration, message)
×
356
                case <-time.After(50 * time.Millisecond):
×
357
                        _, err := net.Dial(network, fmt.Sprintf("%s:%d", address, port))
×
358
                        if err == nil {
×
359
                                return nil
×
360
                        }
×
361
                }
362
        }
363
}
364

365
const providerStatesSetupPath = "/__setup"
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc