-
Notifications
You must be signed in to change notification settings - Fork 1
/
plugin.go
439 lines (363 loc) · 14.4 KB
/
plugin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"github.com/google/uuid"
plugin "github.com/mefellows/pact-matt-plugin/io_pact_plugin"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
type serverDetails struct {
Port int
ServerKey string
}
// The shape of the JSON object given to the pact test
type configuration struct {
Request configurationRequest
Response configurationResponse
}
type configurationRequest struct {
Body string
}
type configurationResponse struct {
Body string
}
func startPluginServer(details serverDetails) {
log.Println("[INFO] server on port", details.Port)
lis, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", details.Port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
fmt.Printf(`{"port": %d, "serverKey": "%s"}%s`, details.Port, details.ServerKey, "\n")
var opts []grpc.ServerOption
grpcServer := grpc.NewServer(opts...)
plugin.RegisterPactPluginServer(grpcServer, newServer())
grpcServer.Serve(lis)
}
func newServer() *mattPluginServer {
s := &mattPluginServer{}
return s
}
type mattPluginServer struct {
plugin.UnimplementedPactPluginServer
}
// // Check that the plugin loaded OK. Returns the catalogue entries describing what the plugin provides
func (m *mattPluginServer) InitPlugin(ctx context.Context, req *plugin.InitPluginRequest) (*plugin.InitPluginResponse, error) {
log.Println("[INFO] InitPlugin request:", req.Implementation, req.Version)
return &plugin.InitPluginResponse{
Catalogue: []*plugin.CatalogueEntry{
{
Key: "matt",
Type: plugin.CatalogueEntry_CONTENT_MATCHER,
Values: map[string]string{
"content-types": "text/matt;application/matt",
},
},
{
Key: "matt",
Type: plugin.CatalogueEntry_TRANSPORT,
},
},
}, nil
}
// Use in the mock server
var expectedRequest, requestedResponse string
// Request to configure/setup the interaction for later verification. Data returned will be persisted in the pact file.
// Validate the request
// Setup the pact interaction (including parsing matching rules and setting up generators)
func (m *mattPluginServer) ConfigureInteraction(ctx context.Context, req *plugin.ConfigureInteractionRequest) (*plugin.ConfigureInteractionResponse, error) {
log.Println("[INFO] ConfigureInteraction request:", req.ContentType, req.ContentsConfig)
// req.ContentsConfig <- protobuf struct, equivalent to what can be represented in JSON
// TODO: extract the actual request part and put into below
config, err := protoStructToConfigMap(req.ContentsConfig)
log.Println("[INFO] ContentsConfig:", config.Request.Body, config.Response.Body, err)
expectedRequest = config.Request.Body
requestedResponse = config.Response.Body
if err != nil {
log.Println("[DEBUG] unmarshalling ContentsConfig from JSON:", err)
return &plugin.ConfigureInteractionResponse{
Error: err.Error(),
}, nil
}
var interactions = make([]*plugin.InteractionResponse, 0)
if config.Request.Body != "" {
interactions = append(interactions, &plugin.InteractionResponse{
Contents: &plugin.Body{
ContentType: "application/matt",
Content: wrapperspb.Bytes([]byte(generateMattMessage(config.Request.Body))),
},
PartName: "request",
})
}
if config.Response.Body != "" {
interactions = append(interactions, &plugin.InteractionResponse{
Contents: &plugin.Body{
ContentType: "application/matt",
Content: wrapperspb.Bytes([]byte(generateMattMessage(config.Response.Body))),
},
PartName: "response",
})
}
return &plugin.ConfigureInteractionResponse{
Interaction: interactions,
}, nil
}
// Store mismatches for re-use in GetMockServerResults
// TODO: this doesn't work - comparecontetns isn't colled for
var mismatches = make(map[string]*plugin.ContentMismatches)
// Request to perform a comparison of some contents (matching request)
// This is not used for plugins that also provide a transport,
// so the matching functions should be separated into a shared function
func (m *mattPluginServer) CompareContents(ctx context.Context, req *plugin.CompareContentsRequest) (*plugin.CompareContentsResponse, error) {
log.Println("[INFO] CompareContents request:", req)
var mismatch string
actual := parseMattMessage(string(req.Actual.Content.Value))
expected := parseMattMessage(string(req.Expected.Content.Value))
if actual != expected {
mismatch = fmt.Sprintf("expected body '%s' is not equal to actual body '%s'", expected, actual)
log.Println("[INFO] found:", mismatch)
mismatches = map[string]*plugin.ContentMismatches{
// "foo.bar.baz...." // hierarchical
// "column:1" // tabular
"$": {
Mismatches: []*plugin.ContentMismatch{
{
Expected: wrapperspb.Bytes([]byte(expected)),
Actual: wrapperspb.Bytes([]byte(actual)),
Mismatch: mismatch,
Path: "$",
},
},
},
}
return &plugin.CompareContentsResponse{
Results: mismatches,
}, nil
}
return &plugin.CompareContentsResponse{}, nil
}
// Request to generate the content using any defined generators
// If there are no generators, this should just return back the given data
func (m *mattPluginServer) GenerateContent(ctx context.Context, req *plugin.GenerateContentRequest) (*plugin.GenerateContentResponse, error) {
log.Println("[INFO] GenerateContent request:", req.Contents, req.Generators, req.PluginConfiguration)
var config configuration
err := json.Unmarshal(req.Contents.Content.Value, &config)
if err != nil {
log.Println("[INFO] :", err)
}
return &plugin.GenerateContentResponse{
Contents: &plugin.Body{
ContentType: "application/matt",
Content: wrapperspb.Bytes([]byte(generateMattMessage(config.Response.Body))),
},
}, nil
}
// Updated catalogue. This will be sent when the core catalogue has been updated (probably by a plugin loading).
func (m *mattPluginServer) UpdateCatalogue(ctx context.Context, cat *plugin.Catalogue) (*emptypb.Empty, error) {
log.Println("[INFO] UpdateCatalogue request:", cat.Catalogue)
return &emptypb.Empty{}, nil
}
// Start a mock server
func (m *mattPluginServer) StartMockServer(ctx context.Context, req *plugin.StartMockServerRequest) (*plugin.StartMockServerResponse, error) {
log.Println("[INFO] StartMockServer request:", req)
var err error
port := int(req.Port)
id := uuid.NewString()
if port == 0 {
port, err = GetFreePort()
if err != nil {
log.Println("[INFO] unable to find a free port:", err)
return &plugin.StartMockServerResponse{
Response: &plugin.StartMockServerResponse_Error{
Error: err.Error(),
},
}, err
}
}
go startTCPServer(id, port, expectedRequest, requestedResponse, mismatches)
return &plugin.StartMockServerResponse{
Response: &plugin.StartMockServerResponse_Details{
Details: &plugin.MockServerDetails{
Key: id,
Port: uint32(port),
Address: fmt.Sprintf("tcp://%s:%d", req.HostInterface, port),
},
},
}, nil
// TODO: parse the interactions and then store for future responses
}
// Shutdown a running mock server
func (m *mattPluginServer) ShutdownMockServer(ctx context.Context, req *plugin.ShutdownMockServerRequest) (*plugin.ShutdownMockServerResponse, error) {
log.Println("[INFO] ShutdownMockServer request:", req)
err := stopTCPServer(req.ServerKey)
if err != nil {
return &plugin.ShutdownMockServerResponse{ // duplicate / same info to GetMockServerResults
Ok: false,
Results: []*plugin.MockServerResult{
{
Error: err.Error(),
},
},
}, nil
}
return &plugin.ShutdownMockServerResponse{ // duplicate / same info to GetMockServerResults
Ok: true,
Results: []*plugin.MockServerResult{},
}, nil
}
// Get the matching results from a running mock server
func (m *mattPluginServer) GetMockServerResults(ctx context.Context, req *plugin.MockServerRequest) (*plugin.MockServerResults, error) {
log.Println("[INFO] GetMockServerResults request:", req)
// TODO: error if server not called, or mismatches found
// ComtpareContents won't get called if there is a mock server. in protobufs,
// The mock server is responsible for comparing its contents
// In the case of a plugin that implements both content + Protocols, you would likely share the mismatch function
// or persist the mismatches (as is the case here)
if len(mismatches) > 0 {
results := make([]*plugin.MockServerResult, 0)
for path, mismatch := range mismatches {
results = append(results, &plugin.MockServerResult{
Path: path,
Mismatches: mismatch.Mismatches,
})
}
return &plugin.MockServerResults{
Results: results,
}, nil
}
return &plugin.MockServerResults{}, nil
}
var requestMessage = ""
var responseMessage = ""
// Prepare an interaction for verification. This should return any data required to construct any request
// so that it can be amended before the verification is run
// Example: authentication headers
// If no modification is necessary, this should simply send the unmodified request back to the framework
func (m *mattPluginServer) PrepareInteractionForVerification(ctx context.Context, req *plugin.VerificationPreparationRequest) (*plugin.VerificationPreparationResponse, error) {
// 2022/10/27 23:06:42 Received PrepareInteractionForVerification request: pact:"{\"consumer\":{\"name\":\"matttcpconsumer\"},\"interactions\":[{\"description\":\"Matt message\",\"key\":\"f27f2917655cb542\",\"pending\":false,\"request\":{\"contents\":{\"content\":\"MATThellotcpMATT\",\"contentType\":\"application/matt\",\"contentTypeHint\":\"DEFAULT\",\"encoded\":false}},\"response\":[{\"contents\":{\"content\":\"MATTtcpworldMATT\",\"contentType\":\"application/matt\",\"contentTypeHint\":\"DEFAULT\",\"encoded\":false}}],\"transport\":\"matt\",\"type\":\"Synchronous/Messages\"}],\"metadata\":{\"pactRust\":{\"ffi\":\"0.3.13\",\"mockserver\":\"0.9.4\",\"models\":\"0.4.5\"},\"pactSpecification\":{\"version\":\"4.0\"},\"plugins\":[{\"configuration\":{},\"name\":\"matt\",\"version\":\"0.0.1\"}]},\"provider\":{\"name\":\"matttcpprovider\"}}" interactionKey:"f27f2917655cb542" config:{fields:{key:"host" value:{string_value:"localhost"}} fields:{key:"port" value:{number_value:8444}}}
log.Println("[INFO] PrepareInteractionForVerification request:", req)
requestMessage, responseMessage = extractRequestAndResponseMessages(req.Pact, req.InteractionKey)
log.Println("[DEBUG] request body:", requestMessage)
log.Println("[DEBUG] response body:", responseMessage)
return &plugin.VerificationPreparationResponse{
Response: &plugin.VerificationPreparationResponse_InteractionData{
InteractionData: &plugin.InteractionData{
Body: &plugin.Body{
ContentType: "application/matt",
Content: wrapperspb.Bytes([]byte(generateMattMessage(requestMessage))), // <- TODO: this needs to come from the pact struct
},
},
},
}, nil
}
// Execute the verification for the interaction.
func (m *mattPluginServer) VerifyInteraction(ctx context.Context, req *plugin.VerifyInteractionRequest) (*plugin.VerifyInteractionResponse, error) {
log.Println("[INFO] received VerifyInteraction request:", req)
// Issue the call to the provider
host := req.Config.AsMap()["host"].(string)
port := req.Config.AsMap()["port"].(float64)
log.Println("[INFO] calling TCP service at host", host, "and port", port)
actual, err := callMattServiceTCP(host, int(port), requestMessage)
log.Println("[INFO] actual:", actual, "wanted:", responseMessage, "err:", err)
// Report on the results
if actual != responseMessage {
return &plugin.VerifyInteractionResponse{
Response: &plugin.VerifyInteractionResponse_Result{
Result: &plugin.VerificationResult{
Success: false,
Output: []string{""},
Mismatches: []*plugin.VerificationResultItem{
{
Result: &plugin.VerificationResultItem_Mismatch{
Mismatch: &plugin.ContentMismatch{
Expected: wrapperspb.Bytes([]byte(responseMessage)),
Actual: wrapperspb.Bytes([]byte(actual)),
Path: "$",
Mismatch: fmt.Sprintf("Expected '%s' but got '%s'", responseMessage, actual),
},
},
},
},
},
},
}, nil
}
return &plugin.VerifyInteractionResponse{
Response: &plugin.VerifyInteractionResponse_Result{
Result: &plugin.VerificationResult{
Success: true,
},
},
}, nil
}
func protoStructToConfigMap(s *structpb.Struct) (configuration, error) {
var config configuration
bytes, err := s.MarshalJSON()
if err != nil {
log.Println("[ERROR] error marshalling ContentsConfig to JSON:", err)
return config, nil
}
err = json.Unmarshal(bytes, &config)
if err != nil {
log.Println("[ERROR] error unmarshalling ContentsConfig from JSON:", err)
return config, nil
}
return config, nil
}
// GetFreePort Gets an available port by asking the kernal for a random port
// ready and available for use.
func GetFreePort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
if err != nil {
return 0, err
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, err
}
port := l.Addr().(*net.TCPAddr).Port
defer l.Close()
return port, nil
}
// Accepts a Pact JSON string and interactionKey, and extracts the relevant messages
func extractRequestAndResponseMessages(pact string, interactionKey string) (request string, response string) {
var p pactv4
err := json.Unmarshal([]byte(pact), &p)
if err != nil {
log.Println("[ERROR] unable to extract payload for verification:", err)
}
// Find the current interaction in the Pact
for _, inter := range p.Interactions {
log.Println("[DEBUG] looking for interaction by key", interactionKey)
log.Println(inter)
switch i := inter.(type) {
case *httpInteraction:
log.Println("[DEBUG] keys", i.interaction.Key, interactionKey)
if i.Key == interactionKey {
log.Println("[DEBUG] HTTP interaction")
return parseMattMessage(i.Request.Body.Content), parseMattMessage(i.Response.Body.Content)
}
case *asyncMessageInteraction:
log.Println("[DEBUG] keys", i.interaction.Key, interactionKey)
if i.Key == interactionKey {
log.Println("[DEBUG] async interaction")
return parseMattMessage(i.Contents.Content), ""
}
case *syncMessageInteraction:
log.Println("[DEBUG] keys", i.interaction.Key, interactionKey)
if i.Key == interactionKey {
log.Println("[DEBUG] sync interaction")
return parseMattMessage(i.Request.Contents.Content), parseMattMessage(i.Response[0].Contents.Content)
}
default:
log.Printf("unknown interaction type: '%+v'", i)
return "", ""
}
}
return "", ""
}