diff --git a/command/root_test.go b/command/root_test.go index aa786e684..305afb291 100644 --- a/command/root_test.go +++ b/command/root_test.go @@ -54,7 +54,7 @@ func captureOutput(action func()) string { action() - w.Close() + _ = w.Close() out, _ := io.ReadAll(r) os.Stderr = rescueStderr diff --git a/consumer/http.go b/consumer/http.go index 77c36fd7a..303087f23 100644 --- a/consumer/http.go +++ b/consumer/http.go @@ -201,7 +201,7 @@ func (p *httpMockProvider) displayMismatches(t *testing.T, mismatches []native.M fmt.Println("\t\tDiff:") log.Println("[INFO] pact validation failed, errors: ") for _, m := range mismatches { - formattedRequest := fmt.Sprintf("%s %s", m.Request.Method, m.Request.Path) + formattedRequest := fmt.Sprintf("%s %s", m.Method, m.Path) switch m.Type { case "missing-request": fmt.Printf("\t\texpected: \t%s (Expected request that was not received)\n", formattedRequest) diff --git a/consumer/http_v2.go b/consumer/http_v2.go index 6d4c224fa..276a403ef 100644 --- a/consumer/http_v2.go +++ b/consumer/http_v2.go @@ -35,7 +35,7 @@ func NewV2Pact(config MockHTTPProviderConfig) (*V2HTTPMockProvider, error) { // AddInteraction to the pact func (p *V2HTTPMockProvider) AddInteraction() *V2UnconfiguredInteraction { log.Println("[DEBUG] pact add V2 interaction") - interaction := p.httpMockProvider.mockserver.NewInteraction("") + interaction := p.mockserver.NewInteraction("") i := &V2UnconfiguredInteraction{ interaction: &Interaction{ diff --git a/consumer/http_v3.go b/consumer/http_v3.go index bccce0bac..32d25fb9c 100644 --- a/consumer/http_v3.go +++ b/consumer/http_v3.go @@ -35,7 +35,7 @@ func NewV3Pact(config MockHTTPProviderConfig) (*V3HTTPMockProvider, error) { // AddInteraction to the pact func (p *V3HTTPMockProvider) AddInteraction() *V3UnconfiguredInteraction { log.Println("[DEBUG] pact add V3 interaction") - interaction := p.httpMockProvider.mockserver.NewInteraction("") + interaction := p.mockserver.NewInteraction("") i := &V3UnconfiguredInteraction{ interaction: &Interaction{ diff --git a/consumer/http_v4.go b/consumer/http_v4.go index 8dd986b79..55a79c9a4 100644 --- a/consumer/http_v4.go +++ b/consumer/http_v4.go @@ -36,7 +36,7 @@ func NewV4Pact(config MockHTTPProviderConfig) (*V4HTTPMockProvider, error) { // AddInteraction to the pact func (p *V4HTTPMockProvider) AddInteraction() *V4UnconfiguredInteraction { log.Println("[DEBUG] pact add V4 interaction") - interaction := p.httpMockProvider.mockserver.NewInteraction("") + interaction := p.mockserver.NewInteraction("") i := &V4UnconfiguredInteraction{ interaction: &Interaction{ @@ -92,6 +92,15 @@ func (i *V4UnconfiguredInteraction) UponReceiving(description string) *V4Unconfi return i } +// AddExternalReference records a reference to an external resource (such as a ticket or +// pull request) against the interaction. References appear under +// comments.references[group][name] in the Pact file. May be called multiple times. +func (i *V4UnconfiguredInteraction) AddExternalReference(group, name, value string) *V4UnconfiguredInteraction { + i.interaction.interaction.WithReference(group, name, value) + + return i +} + // WithRequest provides a builder for the expected request func (i *V4UnconfiguredInteraction) WithCompleteRequest(request Request) *V4InteractionWithCompleteRequest { i.interaction.WithCompleteRequest(request) diff --git a/consumer/http_v4_test.go b/consumer/http_v4_test.go index 33165496b..efb421bde 100644 --- a/consumer/http_v4_test.go +++ b/consumer/http_v4_test.go @@ -2,6 +2,7 @@ package consumer import ( "fmt" + "net/http" "os" "strings" "testing" @@ -85,6 +86,25 @@ func TestHttpV4TypeSystem(t *testing.T) { } +func TestV4HTTPAddExternalReference(t *testing.T) { + p, err := NewV4Pact(MockHTTPProviderConfig{ + Consumer: "consumer", + Provider: "provider", + }) + assert.NoError(t, err) + + err = p.AddInteraction(). + UponReceiving("a request with an external reference"). + AddExternalReference("Jira", "TICKET-123", "https://jira.example.com/browse/TICKET-123"). + WithRequest("GET", "/", func(b *V4RequestBuilder) {}). + WillRespondWith(200, func(b *V4ResponseBuilder) {}). + ExecuteTest(t, func(msc MockServerConfig) error { + _, err := http.Get(fmt.Sprintf("http://%s:%d/", msc.Host, msc.Port)) + return err + }) + assert.NoError(t, err) +} + var Like = matchers.Like var EachLike = matchers.EachLike var Term = matchers.Term diff --git a/examples/avro/codec.go b/examples/avro/codec.go index fb4d2aca8..4ad4798ac 100644 --- a/examples/avro/codec.go +++ b/examples/avro/codec.go @@ -1,11 +1,12 @@ package avro import ( - "os" + "github.com/linkedin/goavro/v2" ) +//nolint:unused // Retained as a reusable helper for the example package. func getCodec() *goavro.Codec { schema, err := os.ReadFile("user.avsc") if err != nil { diff --git a/examples/grpc/routeguide/server/server.go b/examples/grpc/routeguide/server/server.go index fa59e7ddf..0751994b1 100644 --- a/examples/grpc/routeguide/server/server.go +++ b/examples/grpc/routeguide/server/server.go @@ -40,9 +40,8 @@ import ( "github.com/pact-foundation/pact-go/v2/examples/grpc/routeguide/data" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" - "google.golang.org/grpc/status" - - "github.com/golang/protobuf/proto" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" pb "github.com/pact-foundation/pact-go/v2/examples/grpc/routeguide" ) @@ -55,6 +54,15 @@ var ( port = flag.Int("port", 50051, "The server port") ) +// Keep example flags/entrypoint available for standalone usage. +var ( + _ = tls + _ = certFile + _ = keyFile + _ = port + _ = main +) + type routeGuideServer struct { pb.UnimplementedRouteGuideServer savedFeatures []*pb.Feature // read-only after initialized @@ -247,7 +255,9 @@ func main() { } grpcServer := grpc.NewServer(opts...) pb.RegisterRouteGuideServer(grpcServer, NewServer()) - grpcServer.Serve(lis) + if err := grpcServer.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } } // exampleData is a copy of testdata/route_guide_db.json. It's to avoid diff --git a/installer/installer.go b/installer/installer.go index 30965e1fa..5e421d089 100644 --- a/installer/installer.go +++ b/installer/installer.go @@ -302,7 +302,7 @@ var setMacOSInstallName = func(file string) error { return fmt.Errorf("error setting install name on pact lib: %s", err) } - log.Println("[DEBUG] output from command", stdoutStderr) + log.Println("[DEBUG] output from command", string(stdoutStderr)) return err } @@ -388,7 +388,7 @@ const ( var packages = map[string]packageInfo{ FFIPackage: { libName: "libpact_ffi", - version: "0.4.28", + version: "0.5.6", semverRange: ">= 0.4.0, < 1.0.0", }, } @@ -417,13 +417,17 @@ func (d *defaultDownloader) download(src string, dst string) error { if err != nil { return fmt.Errorf("failed to create output file; %w", err) } - defer f.Close() + defer func() { + _ = f.Close() + }() resp, err := http.Get(src) if err != nil { return fmt.Errorf("failed http call to %s; %w", src, err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() archive, err := gzip.NewReader(resp.Body) if err != nil { @@ -523,7 +527,9 @@ func (d *defaultHasher) hash(src string) (string, error) { if err != nil { return "", err } - defer f.Close() + defer func() { + _ = f.Close() + }() h := md5.New() if _, err := io.Copy(h, f); err != nil { diff --git a/internal/native/message_server.go b/internal/native/message_server.go index bd6e6ec3a..e32b84cca 100644 --- a/internal/native/message_server.go +++ b/internal/native/message_server.go @@ -232,7 +232,7 @@ func (m *Message) WithContents(part interactionPart, contentType string, body [] defer free(cHeader) cBody := C.CString(string(body)) - defer free(cBody) + defer free(cBody) res := C.pactffi_with_body(m.handle, C.int(part), cHeader, cBody) log.Println("[DEBUG] response from pactffi_interaction_contents", (bool(res))) @@ -264,7 +264,7 @@ func (m *MessageServer) UsingPlugin(pluginName string, pluginVersion string) err return ErrHandleNotFound default: if res != 0 { - return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code:", res) + return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code", res) } } @@ -302,7 +302,7 @@ func (m *Message) WithPluginInteractionContents(part interactionPart, contentTyp return ErrPluginSpecificError default: if res != 0 { - return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code:", res) + return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code", res) } } @@ -543,10 +543,7 @@ func (m *MessageServer) WritePactFile(dir string, overwrite bool) error { cDir := C.CString(dir) defer free(cDir) - overwritePact := false - if overwrite { - overwritePact = true - } + overwritePact := overwrite res := int(C.pactffi_write_message_pact_file(m.messagePact.handle, cDir, C.bool(overwritePact))) @@ -572,10 +569,7 @@ func (m *MessageServer) WritePactFileForServer(port int, dir string, overwrite b cDir := C.CString(dir) defer free(cDir) - overwritePact := false - if overwrite { - overwritePact = true - } + overwritePact := overwrite res := int(C.pactffi_write_pact_file(C.int(port), cDir, C.bool(overwritePact))) @@ -596,3 +590,19 @@ func (m *MessageServer) WritePactFileForServer(port int, dir string, overwrite b return fmt.Errorf("an unknown error ocurred when writing to pact file") } } + +// WithReference records an external reference (e.g. a ticket or pull request) +// against the interaction. References are stored under comments.references[group][name] +// in the Pact file. This is a V4-only feature. +func (m *Message) WithReference(group, name, value string) *Message { + cGroup := C.CString(group) + defer free(cGroup) + cName := C.CString(name) + defer free(cName) + cValue := C.CString(value) + defer free(cValue) + + C.pactffi_add_interaction_reference(m.handle, cGroup, cName, cValue) + + return m +} diff --git a/internal/native/message_server_test.go b/internal/native/message_server_test.go index 0431b8e05..73f7d140b 100644 --- a/internal/native/message_server_test.go +++ b/internal/native/message_server_test.go @@ -406,7 +406,9 @@ func TestGrpcPluginInteraction(t *testing.T) { if err != nil { l.Fatalf("did not connect: %v", err) } - defer conn.Close() + defer func() { + _ = conn.Close() + }() c := NewPactPluginClient(conn) // Contact the server and print out its response. @@ -480,7 +482,9 @@ func TestGrpcPluginInteraction_ErrorResponse(t *testing.T) { if err != nil { l.Fatalf("did not connect: %v", err) } - defer conn.Close() + defer func() { + _ = conn.Close() + }() c := NewPactPluginClient(conn) // Contact the server and print out its response. diff --git a/internal/native/mock_server.go b/internal/native/mock_server.go index 38ab72d36..b32321ee0 100644 --- a/internal/native/mock_server.go +++ b/internal/native/mock_server.go @@ -11,7 +11,9 @@ import ( "encoding/json" "fmt" "log" + "net" "os" + "strconv" "strings" "unsafe" ) @@ -134,52 +136,6 @@ func (m *MockServer) WithSpecificationVersion(version specificationVersion) { C.pactffi_with_specification(m.pact.handle, C.int(version)) } -// CreateMockServer creates a new Mock Server from a given Pact file. -// Returns the port number it started on or an error if failed -func (m *MockServer) CreateMockServer(pact string, address string, tls bool) (int, error) { - log.Println("[DEBUG] mock server starting on address:", address) - cPact := C.CString(pact) - cAddress := C.CString(address) - defer free(cPact) - defer free(cAddress) - tlsEnabled := false - if tls { - tlsEnabled = true - } - - p := C.pactffi_create_mock_server(cPact, cAddress, C.bool(tlsEnabled)) - - // | Error | Description | - // |-------|-------------| - // | -1 | A null pointer was received | - // | -2 | The pact JSON could not be parsed | - // | -3 | The mock server could not be started | - // | -4 | The method panicked | - // | -5 | The address is not valid | - // | -6 | Could not create the TLS configuration with the self-signed certificate | - port := int(p) - switch port { - case -1: - return 0, ErrInvalidMockServerConfig - case -2: - return 0, ErrInvalidPact - case -3: - return 0, ErrMockServerUnableToStart - case -4: - return 0, ErrMockServerPanic - case -5: - return 0, ErrInvalidAddress - case -6: - return 0, ErrMockServerTLSConfiguration - default: - if port > 0 { - log.Println("[DEBUG] mock server running on port:", port) - return port, nil - } - return port, fmt.Errorf("an unknown error (code: %v) occurred when starting a mock server for the test", port) - } -} - // Verify verifies that all interactions were successful. If not, returns a slice // of Mismatch-es. Does not write the pact or cleanup server. func (m *MockServer) Verify(port int, dir string) (bool, []MismatchedRequest) { @@ -281,21 +237,36 @@ func libRustFree(str *C.char) { } // Start starts up the mock HTTP server on the given address:port and TLS config -// https://docs.rs/pact_mock_server_ffi/0.0.7/pact_mock_server_ffi/fn.create_mock_server_for_pact.html +// https://docs.rs/pact_ffi/latest/pact_ffi/mock_server/fn.pactffi_create_mock_server_for_transport.html func (m *MockServer) Start(address string, tls bool) (int, error) { if len(m.interactions) == 0 { return 0, ErrNoInteractions } log.Println("[DEBUG] mock server starting on address:", address) - cAddress := C.CString(address) - defer free(cAddress) - tlsEnabled := false + + host, portStr, err := net.SplitHostPort(address) + if err != nil { + return 0, ErrInvalidAddress + } + requestedPort, err := strconv.Atoi(portStr) + if err != nil { + return 0, ErrInvalidAddress + } + cHost := C.CString(host) + defer free(cHost) + var transport string if tls { - tlsEnabled = true + transport = "https" + } else { + transport = "http" } + cTransport := C.CString(transport) + defer free(cTransport) + + cConfig := (*C.char)(nil) - p := C.pactffi_create_mock_server_for_pact(m.pact.handle, cAddress, C.bool(tlsEnabled)) + msPort := int(C.pactffi_create_mock_server_for_transport(m.pact.handle, cHost, C.ushort(requestedPort), cTransport, cConfig)) // | Error | Description | // |-------|-------------| @@ -305,26 +276,23 @@ func (m *MockServer) Start(address string, tls bool) (int, error) { // | -4 | The method panicked | // | -5 | The address is not valid | // | -6 | Could not create the TLS configuration with the self-signed certificate | - port := int(p) - switch port { + switch msPort { case -1: return 0, ErrInvalidMockServerConfig case -2: - return 0, ErrInvalidPact + return 0, ErrInvalidMockServerConfig case -3: return 0, ErrMockServerUnableToStart case -4: return 0, ErrMockServerPanic case -5: return 0, ErrInvalidAddress - case -6: - return 0, ErrMockServerTLSConfiguration default: - if port > 0 { - log.Println("[DEBUG] mock server running on port:", port) - return port, nil + if msPort > 0 { + log.Println("[DEBUG] mock server running on port:", msPort) + return msPort, nil } - return port, fmt.Errorf("an unknown error (code: %v) occurred when starting a mock server for the test", port) + return msPort, fmt.Errorf("an unknown error (code: %v) occurred when starting a mock server for the test", msPort) } } @@ -412,7 +380,7 @@ func (m *MockServer) UsingPlugin(pluginName string, pluginVersion string) error return ErrHandleNotFound default: if res != 0 { - return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code:", res) + return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code", res) } } @@ -468,7 +436,7 @@ func (i *Interaction) WithPluginInteractionContents(part interactionPart, conten return ErrPluginSpecificError default: if res != 0 { - return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code:", res) + return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code", res) } } @@ -643,6 +611,22 @@ func (i *Interaction) WithStatus(status int) *Interaction { return i } +// WithReference records an external reference (e.g. a ticket or pull request) +// against the interaction. References are stored under comments.references[group][name] +// in the Pact file. This is a V4-only feature. +func (i *Interaction) WithReference(group, name, value string) *Interaction { + cGroup := C.CString(group) + defer free(cGroup) + cName := C.CString(name) + defer free(cName) + cValue := C.CString(value) + defer free(cValue) + + C.pactffi_add_interaction_reference(i.handle, cGroup, cName, cValue) + + return i +} + type stringLike interface { String() string } @@ -778,11 +762,11 @@ var ( // Log Errors var ( - ErrCantSetLogger = fmt.Errorf("can't set logger (applying the logger failed, perhaps because one is applied already).") - ErrNoLogger = fmt.Errorf("no logger has been initialized (call `logger_init` before any other log function).") - ErrSpecifierNotUtf8 = fmt.Errorf("The sink specifier was not UTF-8 encoded.") - ErrUnknownSinkType = fmt.Errorf(`the sink type specified is not a known type (known types: "buffer", "stdout", "stderr", or "file /some/path").`) - ErrMissingFilePath = fmt.Errorf("no file path was specified in a file-type sink specification.") - ErrCantOpenSinkToFile = fmt.Errorf("opening a sink to the specified file path failed (check permissions).") + ErrCantSetLogger = fmt.Errorf("can't set logger (applying the logger failed, perhaps because one is applied already)") + ErrNoLogger = fmt.Errorf("no logger has been initialized (call `logger_init` before any other log function)") + ErrSpecifierNotUtf8 = fmt.Errorf("the sink specifier was not UTF-8 encoded") + ErrUnknownSinkType = fmt.Errorf(`the sink type specified is not a known type (known types: "buffer", "stdout", "stderr", or "file /some/path")`) + ErrMissingFilePath = fmt.Errorf("no file path was specified in a file-type sink specification") + ErrCantOpenSinkToFile = fmt.Errorf("opening a sink to the specified file path failed (check permissions)") ErrCantConstructSink = fmt.Errorf("can't construct the log sink") ) diff --git a/internal/native/mock_server_test.go b/internal/native/mock_server_test.go index aa79a6373..7e53d8057 100644 --- a/internal/native/mock_server_test.go +++ b/internal/native/mock_server_test.go @@ -17,9 +17,34 @@ func init() { Init("") } +// newSimpleMockServer creates a mock server with a simple GET /foobar → 200 interaction +// using the programmatic API, replacing the removed CreateMockServer function. +func newSimpleMockServer(t *testing.T) (*MockServer, int) { + t.Helper() + m := NewHTTPPact("consumer", "provider") + m.NewInteraction("Some name for the test"). + UponReceiving("Some name for the test"). + Given("Some state"). + WithRequest("GET", "/foobar"). + WithStatus(200) + port, err := m.Start("0.0.0.0:0", false) + if err != nil { + t.Fatalf("failed to start mock server: %v", err) + } + return m, port +} + func TestMockServer_CreateAndCleanupMockServer(t *testing.T) { - m := MockServer{} - port, _ := m.CreateMockServer(pactComplex, "0.0.0.0:0", false) + m := NewHTTPPact("consumer", "provider") + m.NewInteraction("Some complex interaction"). + UponReceiving("Some complex interaction"). + Given("Some state"). + WithRequest("GET", "/foobar"). + WithStatus(200) + port, err := m.Start("0.0.0.0:0", false) + if err != nil { + t.Fatal("failed to start mock server:", err) + } defer m.CleanupMockServer(port) if port <= 0 { @@ -28,8 +53,7 @@ func TestMockServer_CreateAndCleanupMockServer(t *testing.T) { } func TestMockServer_MismatchesSuccess(t *testing.T) { - m := MockServer{} - port, _ := m.CreateMockServer(pactSimple, "0.0.0.0:0", false) + m, port := newSimpleMockServer(t) defer m.CleanupMockServer(port) res, err := http.Get(fmt.Sprintf("http://localhost:%d/foobar", port)) @@ -48,8 +72,7 @@ func TestMockServer_MismatchesSuccess(t *testing.T) { } func TestMockServer_MismatchesFail(t *testing.T) { - m := MockServer{} - port, _ := m.CreateMockServer(pactSimple, "0.0.0.0:0", false) + m, port := newSimpleMockServer(t) defer m.CleanupMockServer(port) mismatches := m.MockServerMismatchedRequests(port) @@ -62,8 +85,7 @@ func TestMockServer_VerifySuccess(t *testing.T) { tmpPactFolder, err := os.MkdirTemp("", "pact-go") assert.NoError(t, err) - m := MockServer{} - port, _ := m.CreateMockServer(pactSimple, "0.0.0.0:0", false) + m, port := newSimpleMockServer(t) defer m.CleanupMockServer(port) _, err = http.Get(fmt.Sprintf("http://localhost:%d/foobar", port)) @@ -84,8 +106,7 @@ func TestMockServer_VerifySuccess(t *testing.T) { func TestMockServer_VerifyFail(t *testing.T) { tmpPactFolder, err := os.MkdirTemp("", "pact-go") assert.NoError(t, err) - m := MockServer{} - port, _ := m.CreateMockServer(pactSimple, "0.0.0.0:0", false) + m, port := newSimpleMockServer(t) success, mismatches := m.Verify(port, tmpPactFolder) if success { @@ -101,8 +122,7 @@ func TestMockServer_WritePactfile(t *testing.T) { tmpPactFolder, err := os.MkdirTemp("", "pact-go") assert.NoError(t, err) - m := MockServer{} - port, _ := m.CreateMockServer(pactSimple, "0.0.0.0:0", false) + m, port := newSimpleMockServer(t) defer m.CleanupMockServer(port) _, err = http.Get(fmt.Sprintf("http://localhost:%d/foobar", port)) @@ -155,7 +175,6 @@ func TestHandleBasedHTTPTests(t *testing.T) { _, err = http.Get(fmt.Sprintf("http://0.0.0.0:%d/products", port)) assert.NoError(t, err) - mismatches := m.MockServerMismatchedRequests(port) if len(mismatches) != 0 { t.Fatalf("want 0 mismatches, got '%d'", len(mismatches)) @@ -223,73 +242,3 @@ func TestPluginInteraction(t *testing.T) { err = m.WritePactFile(port, tmpPactFolder) assert.NoError(t, err) } - -var pactSimple = `{ - "consumer": { - "name": "consumer" - }, - "provider": { - "name": "provider" - }, - "interactions": [ - { - "description": "Some name for the test", - "request": { - "method": "GET", - "path": "/foobar" - }, - "response": { - "status": 200 - }, - "description": "Some name for the test", - "provider_state": "Some state" - }] -}` - -var pactComplex = `{ - "consumer": { - "name": "consumer" - }, - "provider": { - "name": "provider" - }, - "interactions": [ - { - "request": { - "method": "GET", - "path": "/foobar", - "body": { - "pass": 1234, - "user": { - "address": "some address", - "name": "someusername", - "phone": 12345678, - "plaintext": "plaintext" - } - } - }, - "response": { - "status": 200 - }, - "description": "Some name for the test", - "provider_state": "Some state", - "matchingRules": { - "$.body.pass": { - "match": "regex", - "regex": "\\d+" - }, - "$.body.user.address": { - "match": "regex", - "regex": "\\s+" - }, - "$.body.user.name": { - "match": "regex", - "regex": "\\s+" - }, - "$.body.user.phone": { - "match": "regex", - "regex": "\\d+" - } - } - }] -}` diff --git a/internal/native/pact.h b/internal/native/pact.h index c5865d040..342a601e8 100644 --- a/internal/native/pact.h +++ b/internal/native/pact.h @@ -7,7 +7,7 @@ #ifndef pact_ffi_h #define pact_ffi_h -/* Generated with cbindgen:0.26.0 */ +/* Generated with cbindgen:0.29.2 */ /* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ @@ -657,18 +657,32 @@ int pactffi_get_error_message(char *buffer, /** * Convenience function to direct all logging to stdout. + * + * This function is equivalent to using [`pactffi_logger_init`] followed by the + * [`pactffi_logger_attach_sink`] with the appropriate sink specifier, and then + * [`pactffi_logger_apply`]. */ int pactffi_log_to_stdout(int level_filter); /** * Convenience function to direct all logging to stderr. + * + * This function is equivalent to using [`pactffi_logger_init`] followed by the + * [`pactffi_logger_attach_sink`] with the appropriate sink specifier, and then + * [`pactffi_logger_apply`]. */ int pactffi_log_to_stderr(int level_filter); /** * Convenience function to direct all logging to a file. * + * + * This function is equivalent to using [`pactffi_logger_init`] followed by the + * [`pactffi_logger_attach_sink`] with the appropriate sink specifier, and then + * [`pactffi_logger_apply`]. + * * # Safety + * * This function will fail if the file_name pointer is invalid or does not point to a NULL * terminated string. */ @@ -676,6 +690,13 @@ int pactffi_log_to_file(const char *file_name, int level_filter); /** * Convenience function to direct all logging to a task local memory buffer. + * + * This function is equivalent to using [`pactffi_logger_init`] followed by the + * [`pactffi_logger_attach_sink`] with the appropriate sink specifier, and then + * [`pactffi_logger_apply`]. + * + * The contents of the buffer can be fetched using + * [`pactffi_fetch_log_buffer`]. */ int pactffi_log_to_buffer(int level_filter); @@ -721,7 +742,7 @@ void pactffi_logger_init(void); * - `-1`: Can't set logger (applying the logger failed, perhaps because one is applied already). * - `-2`: No logger has been initialized (call `pactffi_logger_init` before any other log function). * - `-3`: The sink specifier was not UTF-8 encoded. - * - `-4`: The sink type specified is not a known type (known types: "stdout", "stderr", or "file /some/path"). + * - `-4`: The sink type specified is not a known type (known types: "stdout", "stderr", "buffer", or "file /some/path"). * - `-5`: No file path was specified in a file-type sink specification. * - `-6`: Opening a sink to the specified file path failed (check permissions). * @@ -740,6 +761,12 @@ int pactffi_logger_attach_sink(const char *sink_specifier, * * This function will install a global tracing subscriber. Any attempts to modify the logger * after the call to `logger_apply` will fail. + * + * # Error Handling + * + * The return error codes are as follows: + * + * - `-1`: Can't set logger (applying the logger failed, perhaps because one is applied already). */ int pactffi_logger_apply(void); @@ -793,7 +820,7 @@ struct PactInteractionIterator *pactffi_pact_model_interaction_iterator(struct P /** * Returns the Pact specification enum that the Pact is for. */ -int pactffi_pact_spec_version(const struct Pact *pact); +enum PactSpecification pactffi_pact_spec_version(const struct Pact *pact); /** * Frees the memory used by the Pact interaction model @@ -3089,35 +3116,6 @@ struct ProviderStateIterator *pactffi_sync_message_get_provider_state_iter(struc */ void pactffi_string_delete(char *string); -/** - * [DEPRECATED] External interface to create a HTTP mock server. A pointer to the pact JSON as a NULL-terminated C - * string is passed in, as well as the port for the mock server to run on. A value of 0 for the - * port will result in a port being allocated by the operating system. The port of the mock server is returned. - * - * * `pact_str` - Pact JSON - * * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:0) - * * `tls` - boolean flag to indicate of the mock server should use TLS (using a self-signed certificate) - * - * This function is deprecated and replaced with `pactffi_create_mock_server_for_transport`. - * - * # Errors - * - * Errors are returned as negative values. - * - * | Error | Description | - * |-------|-------------| - * | -1 | A null pointer was received | - * | -2 | The pact JSON could not be parsed | - * | -3 | The mock server could not be started | - * | -4 | The method panicked | - * | -5 | The address is not valid | - * | -6 | Could not create the TLS configuration with the self-signed certificate | - * - */ -int32_t pactffi_create_mock_server(const char *pact_str, - const char *addr_str, - bool tls); - /** * Fetch the CA Certificate used to generate the self-signed certificate for the TLS mock server. * @@ -3130,34 +3128,6 @@ int32_t pactffi_create_mock_server(const char *pact_str, */ char *pactffi_get_tls_ca_certificate(void); -/** - * [DEPRECATED] External interface to create a HTTP mock server. A Pact handle is passed in, - * as well as the port for the mock server to run on. A value of 0 for the port will result in a - * port being allocated by the operating system. The port of the mock server is returned. - * - * * `pact` - Handle to a Pact model created with created with `pactffi_new_pact`. - * * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:0). Must be a valid UTF-8 NULL-terminated string. - * * `tls` - boolean flag to indicate of the mock server should use TLS (using a self-signed certificate) - * - * This function is deprecated and replaced with `pactffi_create_mock_server_for_transport`. - * - * # Errors - * - * Errors are returned as negative values. - * - * | Error | Description | - * |-------|-------------| - * | -1 | An invalid handle was received. Handles should be created with `pactffi_new_pact` | - * | -3 | The mock server could not be started | - * | -4 | The method panicked | - * | -5 | The address is not valid | - * | -6 | Could not create the TLS configuration with the self-signed certificate | - * - */ -int32_t pactffi_create_mock_server_for_pact(PactHandle pact, - const char *addr_str, - bool tls); - /** * Create a mock server for the provided Pact handle and transport. If the transport is not * provided (it is a NULL pointer or an empty string), will default to an HTTP transport. The @@ -3548,11 +3518,12 @@ bool pactffi_with_specification(PactHandle pact, int pactffi_handle_get_pact_spec_version(PactHandle pact); /** - * Sets the additional metadata on the Pact file. Common uses are to add the client library details such as the name and version - * Returns false if the interaction or Pact can't be modified (i.e. the mock server for it has already started) + * Sets the additional metadata on the Pact file. Common uses are to add the client library + * details such as the name and version. Returns false if the interaction or Pact can't be + * modified (i.e. the mock server for it has already started) or the namespace is readonly. * * * `pact` - Handle to a Pact model - * * `namespace` - the top level metadat key to set any key values on + * * `namespace` - the top level metadata key to set any key values on * * `name` - the key to set * * `value` - the value to set */ @@ -3725,30 +3696,43 @@ bool pactffi_response_status_v2(InteractionHandle interaction, const char *status); /** - * Adds the body for the interaction. Returns false if the interaction or Pact can't be - * modified (i.e. the mock server for it has already started) + * Adds the body for the interaction. Returns false if the interaction or Pact + * can't be modified (i.e. the mock server for it has already started) * - * * `part` - The part of the interaction to add the body to (Request or Response). - * * `content_type` - The content type of the body. Defaults to `text/plain`. Will be ignored if a content type - * header is already set. - * * `body` - The body contents. For JSON payloads, matching rules can be embedded in the body. See - * [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) + * * `part` - The part of the interaction to add the body to (Request or + * Response). This is ignored for asynchronous message interactions. + * * `content_type` - The content type of the body, or `NULL` to use the + * internal logic. + * * `body` - The body contents. For JSON payloads, matching rules can be + * embedded in the body. See + * [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md) * - * For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the - * part parameter will be ignored. With synchronous messages, the request contents will be overwritten, - * while a new response will be appended to the message. + * The payload's content type is determined as follows, whichever is first: + * + * - The `content_type` argument to this function if provided. If the provided + * value fails to parse, and error is logged and it will be ignored. + * - The `Content-Type` header for HTTP interaction, or `contentType` metadata + * entry for message interactions. + * - From automatic detection of the body contents. + * - Defaults to `text/plain` as a last resort. + * + * Furthermore, the `Content-Type` header or `contentType` metadata entry will + * be updated with the above determined content type, _unless_ it is already + * set. + * + * This function will overwrite the body contents if they exist, with the + * exception of the response part of synchronous message interactions, where a + * new response will be appended. * * # Safety * - * The interaction contents and content type must either be NULL pointers, or point to valid - * UTF-8 encoded NULL-terminated strings. Otherwise, behaviour is undefined. + * The interaction contents and content type must either be NULL pointers, or + * point to valid UTF-8 encoded NULL-terminated strings. Otherwise, behaviour + * is undefined. * * # Error Handling * - * If the contents is a NULL pointer, it will set the body contents as null. If the content - * type is a null pointer, or can't be parsed, it will set the content type as TEXT. - * Returns false if the interaction or Pact can't be modified (i.e. the mock server for it has - * already started) or an error has occurred. + * If the contents is a NULL pointer, it will set the body contents as null. */ bool pactffi_with_body(InteractionHandle interaction, int part, @@ -3792,6 +3776,14 @@ bool pactffi_with_binary_body(InteractionHandle interaction, size_t size); /** + *
+ * + * This function is deprecated. Use [`pactffi_with_binary_body`] in order to + * set the binary body, and use [`pactffi_with_matching_rules`] to set the + * matching rules to ensure that only the content type is being matched. + * + *
+ * * Adds a binary file as the body with the expected content type and example contents. Will use * a mime type matcher to match the body. Returns false if the interaction or Pact can't be * modified (i.e. the mock server for it has already started) @@ -3990,7 +3982,8 @@ bool pactffi_set_pending(InteractionHandle interaction, bool pending); * * # Safety * - * The comments parameter must be a valid pointer to a NULL terminated UTF-8, + * The key parameter must be a valid pointer to a NULL terminated UTF-8. + * The value parameter must be a valid pointer to a NULL terminated UTF-8, * or NULL if the comment is to be cleared. */ bool pactffi_set_comment(InteractionHandle interaction, const char *key, const char *value); @@ -4013,11 +4006,33 @@ bool pactffi_set_comment(InteractionHandle interaction, const char *key, const c * * # Safety * - * The comments parameter must be a valid pointer to a NULL terminated UTF-8, - * or NULL if the comment is to be cleared. + * The comment parameter must be a valid pointer to a NULL terminated UTF-8. */ bool pactffi_add_text_comment(InteractionHandle interaction, const char *comment); +/** + * Add an external reference to the interaction. The reference will be stored in the Pact + * file comments under the `references` key, grouped by `group`. For instance, you could + * store the AsyncAPI operation ID that the interaction corresponds to as an external reference. + * + * * `interaction` - Interaction handle to add the reference to. + * * `group` - Group name for the reference (e.g. `"asyncapi"`). + * * `name` - Name of the reference entry within the group (e.g. `"operationId"`). + * * `value` - Value of the reference. This may be any valid JSON value (parsed automatically), + * or a plain string if JSON parsing fails. + * + * This function will return `true` if the reference was successfully added. All parameters + * must be valid UTF-8 null-terminated strings. + * + * # Safety + * + * All parameters must be valid pointers to NULL terminated UTF-8 strings. + */ +bool pactffi_add_interaction_reference(InteractionHandle interaction, + const char *group, + const char *name, + const char *value); + /** * Get an iterator over all the messages of the Pact. The returned iterator needs to be * freed with `pactffi_pact_message_iter_delete`. @@ -4122,11 +4137,14 @@ void pactffi_message_expects_to_receive(MessageHandle message, const char *descr void pactffi_message_given(MessageHandle message, const char *description); /** - * Adds a provider state to the Message with a parameter key and value. + * Adds a parameter key and value to a provider state to the Message. If the provider state + * does not exist, a new one will be created, otherwise the parameter will be merged into the + * existing one. The parameter value will be parsed as JSON. * + * # Parameters * * `description` - The provider state description. It needs to be unique. * * `name` - Parameter name. - * * `value` - Parameter value. + * * `value` - Parameter value as JSON. */ void pactffi_message_given_with_param(MessageHandle message, const char *description, @@ -4297,32 +4315,13 @@ unsigned int pactffi_free_pact_handle(PactHandle pact); */ unsigned int pactffi_free_message_pact_handle(MessagePactHandle pact); -/** - * External interface to verifier a provider - * - * * `args` - the same as the CLI interface, except newline delimited - * - * # Errors - * - * Errors are returned as non-zero numeric values. - * - * | Error | Description | - * |-------|-------------| - * | 1 | The verification process failed, see output for errors | - * | 2 | A null pointer was received | - * | 3 | The method panicked | - * | 4 | Invalid arguments were provided to the verification process | - * - * # Safety - * - * Exported functions are inherently unsafe. Deal. - */ -int32_t pactffi_verify(const char *args); - /** * Get a Handle to a newly created verifier. You should call `pactffi_verifier_shutdown` when * done with the verifier to free all allocated resources. * + * By default, verification results will not be published. To enable publishing, use + * `pactffi_verifier_set_publish_options` to set the required values and enable it. + * * Deprecated: This function is deprecated. Use `pactffi_verifier_new_for_application` which allows the * calling application/framework name and version to be specified. * @@ -4338,7 +4337,10 @@ struct VerifierHandle *pactffi_verifier_new(void); /** * Get a Handle to a newly created verifier. You should call `pactffi_verifier_shutdown` when - * done with the verifier to free all allocated resources + * done with the verifier to free all allocated resources. + * + * By default, verification results will not be published. To enable publishing, use + * `pactffi_verifier_set_publish_options` to set the required values and enable it. * * # Safety * @@ -4474,16 +4476,19 @@ int pactffi_verifier_set_coloured_output(struct VerifierHandle *handle, int pactffi_verifier_set_no_pacts_is_error(struct VerifierHandle *handle, unsigned char is_error); /** - * Set the options used when publishing verification results to the Pact Broker + * Set the options used when publishing verification results to the Pact Broker. By default, + * verification results will not be published unless this function is called. * * # Args * * - `handle` - The pact verifier handle to update * - `provider_version` - Version of the provider to publish - * - `build_url` - URL to the build which ran the verification - * - `provider_tags` - Collection of tags for the provider - * - `provider_tags_len` - Number of provider tags supplied - * - `provider_branch` - Name of the branch used for verification + * - `build_url` - URL to the build which ran the verification [OPTIONAL] + * - `provider_tags` - Collection of tags for the provider [OPTIONAL] + * - `provider_tags_len` - Number of provider tags supplied [OPTIONAL] + * - `provider_branch` - Name of the branch used for verification [OPTIONAL] + * + * For optional args, a NULL pointer can be used. * * # Safety * @@ -4523,6 +4528,12 @@ void pactffi_verifier_add_custom_header(struct VerifierHandle *handle, const char *header_name, const char *header_value); +/** + * Sets whether redirects should be automatically followed. Setting the `follow` parameter + * to zero will disable following redirects. + */ +void pactffi_verifier_set_follow_redirects(struct VerifierHandle *handle, unsigned char follow); + /** * Adds a Pact file as a source to verify. * @@ -4598,26 +4609,29 @@ void pactffi_verifier_broker_source(struct VerifierHandle *handle, * If a username and password is given, then basic authentication will be used when fetching * the pact file. If a token is provided, then bearer token authentication will be used. * + * This function will return zero unless any of the consumer version selectors are not valid + * JSON, in which case, it will return -1. + * * # Safety * * All string fields must contain valid UTF-8. Invalid UTF-8 * will be replaced with U+FFFD REPLACEMENT CHARACTER. * */ -void pactffi_verifier_broker_source_with_selectors(struct VerifierHandle *handle, - const char *url, - const char *username, - const char *password, - const char *token, - unsigned char enable_pending, - const char *include_wip_pacts_since, - const char *const *provider_tags, - unsigned short provider_tags_len, - const char *provider_branch, - const char *const *consumer_version_selectors, - unsigned short consumer_version_selectors_len, - const char *const *consumer_version_tags, - unsigned short consumer_version_tags_len); +int pactffi_verifier_broker_source_with_selectors(struct VerifierHandle *handle, + const char *url, + const char *username, + const char *password, + const char *token, + unsigned char enable_pending, + const char *include_wip_pacts_since, + const char *const *provider_tags, + unsigned short provider_tags_len, + const char *provider_branch, + const char *const *consumer_version_selectors, + unsigned short consumer_version_selectors_len, + const char *const *consumer_version_tags, + unsigned short consumer_version_tags_len); /** * Runs the verification. @@ -4628,57 +4642,6 @@ void pactffi_verifier_broker_source_with_selectors(struct VerifierHandle *handle */ int pactffi_verifier_execute(struct VerifierHandle *handle); -/** - * External interface to retrieve the options and arguments available when calling the CLI interface, - * returning them as a JSON string. - * - * The purpose is to then be able to use in other languages which wrap the FFI library, to implement - * the same CLI functionality automatically without manual maintenance of arguments, help descriptions - * etc. - * - * # Example structure - * ```json - * { - * "options": [ - * { - * "long": "scheme", - * "help": "Provider URI scheme (defaults to http)", - * "possible_values": [ - * "http", - * "https" - * ], - * "default_value": "http" - * "multiple": false, - * }, - * { - * "long": "file", - * "short": "f", - * "help": "Pact file to verify (can be repeated)", - * "multiple": true - * }, - * { - * "long": "user", - * "help": "Username to use when fetching pacts from URLS", - * "multiple": false, - * "env": "PACT_BROKER_USERNAME" - * } - * ], - * "flags": [ - * { - * "long": "disable-ssl-verification", - * "help": "Disables validation of SSL certificates", - * "multiple": false - * } - * ] - * } - * ``` - * - * # Safety - * - * Exported functions are inherently unsafe. - */ -const char *pactffi_verifier_cli_args(void); - /** * Extracts the logs for the verification run. This needs the memory buffer log sink to be * setup before the verification is executed. The returned string will need to be freed with @@ -4716,6 +4679,39 @@ const char *pactffi_verifier_output(const struct VerifierHandle *handle, unsigne */ const char *pactffi_verifier_json(const struct VerifierHandle *handle); +/** + * Add a plugin to be used by the test. The plugin needs to be installed correctly for this + * function to work. + * + * * `plugin_name` is the name of the plugin to load. + * * `plugin_version` is the version of the plugin to load. It is optional, and can be NULL. + * * `completion_delay` is an arbitrary delay specified in milliseconds to add before the + * function returns to allow asynchronous tasks to complete. + * + * Returns zero on success, and a positive integer value on failure. + * + * Note that plugins run as separate processes, so will need to be cleaned up afterwards by + * calling `pactffi_cleanup_plugins` otherwise you will have plugin processes left running. + * + * # Safety + * + * `plugin_name` must be a valid pointer to a NULL terminated string. `plugin_version` may be null, + * and if not NULL must also be a valid pointer to a NULL terminated string. Invalid + * pointers will result in undefined behaviour. + * + * # Errors + * + * * `1` - A general panic was caught. + * * `2` - Failed to load the plugin. + * * `3` - Pact Handle is not valid. + * + * When an error errors, LAST_ERROR will contain the error message. + */ +unsigned int pactffi_using_plugin_with_delay(PactHandle pact, + const char *plugin_name, + const char *plugin_version, + uint64_t completion_delay); + /** * Add a plugin to be used by the test. The plugin needs to be installed correctly for this * function to work. @@ -4933,4 +4929,4 @@ const char *pactffi_matches_json_value(const struct MatchingRule *matching_rule, const char *actual_value, uint8_t cascaded); -#endif /* pact_ffi_h */ +#endif /* pact_ffi_h */ diff --git a/internal/native/plugin.go b/internal/native/plugin.go index 02a212498..8ebad1592 100644 --- a/internal/native/plugin.go +++ b/internal/native/plugin.go @@ -4,10 +4,10 @@ import "fmt" // Plugin Errors var ( - ErrPluginGenericPanic = fmt.Errorf("A general panic was caught.") - ErrPluginMockServerStarted = fmt.Errorf("The mock server has already been started.") - ErrPluginInteractionHandleInvalid = fmt.Errorf("The interaction handle is invalid. ") - ErrPluginInvalidContentType = fmt.Errorf("The content type is not valid.") - ErrPluginInvalidJson = fmt.Errorf("The contents JSON is not valid JSON.") - ErrPluginSpecificError = fmt.Errorf("The plugin returned an error.") + ErrPluginGenericPanic = fmt.Errorf("a general panic was caught") + ErrPluginMockServerStarted = fmt.Errorf("the mock server has already been started") + ErrPluginInteractionHandleInvalid = fmt.Errorf("the interaction handle is invalid") + ErrPluginInvalidContentType = fmt.Errorf("the content type is not valid") + ErrPluginInvalidJson = fmt.Errorf("the contents JSON is not valid JSON") + ErrPluginSpecificError = fmt.Errorf("the plugin returned an error") ) diff --git a/internal/native/verifier.go b/internal/native/verifier.go index 8605f9dd2..d6ff3df30 100644 --- a/internal/native/verifier.go +++ b/internal/native/verifier.go @@ -8,7 +8,6 @@ import "C" import ( "fmt" "log" - "strings" "unsafe" ) @@ -16,31 +15,6 @@ type Verifier struct { handle *C.VerifierHandle } -func (v *Verifier) Verify(args []string) error { - log.Println("[DEBUG] executing verifier FFI with args", args) - cargs := C.CString(strings.Join(args, "\n")) - defer free(cargs) - result := C.pactffi_verify(cargs) - - /// | Error | Description | - /// |-------|-------------| - /// | 1 | The verification process failed, see output for errors | - /// | 2 | A null pointer was received | - /// | 3 | The method panicked | - switch int(result) { - case 0: - return nil - case 1: - return ErrVerifierFailed - case 2: - return ErrInvalidVerifierConfig - case 3: - return ErrVerifierPanic - default: - return fmt.Errorf("an unknown error (%d) ocurred when verifying the provider (this indicates a defect in the framework)", int(result)) - } -} - // Version returns the current semver FFI interface version func (v *Verifier) Version() string { return Version() diff --git a/internal/native/verifier_test.go b/internal/native/verifier_test.go index 714d65551..5d6b3cb33 100644 --- a/internal/native/verifier_test.go +++ b/internal/native/verifier_test.go @@ -18,29 +18,6 @@ func TestVerifier_Version(t *testing.T) { fmt.Println("version: ", Version()) } -func TestVerifier_Verify(t *testing.T) { - t.Run("invalid args returns an error", func(t *testing.T) { - - v := Verifier{} - args := []string{ - "--file", - "/non/existent/path.json", - "--hostname", - "localhost", - "--port", - "55827", - "--state-change-url", - "http://localhost:55827/__setup/", - "--loglevel", - "info", - } - - res := v.Verify(args) - - assert.Error(t, res) - }) -} - func TestVerifier_NewForApplication(t *testing.T) { v := NewVerifier("pact-go", "test") diff --git a/matchers/matcher.go b/matchers/matcher.go index f0bdb9e34..c9b93baf2 100644 --- a/matchers/matcher.go +++ b/matchers/matcher.go @@ -298,8 +298,8 @@ func MatchV2(src interface{}) Matcher { // match recursively traverses the provided type and outputs a // matcher string for it that is compatible with the Pact dsl. func match(srcType reflect.Type, params params) Matcher { - switch kind := srcType.Kind(); kind { - case reflect.Ptr: + switch srcType.Kind() { + case reflect.Pointer: return match(srcType.Elem(), params) case reflect.Slice, reflect.Array: return EachLike(match(srcType.Elem(), getDefaults()), params.slice.min) diff --git a/message/v3/asynchronous_message.go b/message/v3/asynchronous_message.go index acf0af190..77085ebac 100644 --- a/message/v3/asynchronous_message.go +++ b/message/v3/asynchronous_message.go @@ -12,7 +12,6 @@ import ( "github.com/pact-foundation/pact-go/v2/command" "github.com/pact-foundation/pact-go/v2/internal/native" - mockserver "github.com/pact-foundation/pact-go/v2/internal/native" logging "github.com/pact-foundation/pact-go/v2/log" "github.com/pact-foundation/pact-go/v2/models" ) @@ -23,7 +22,7 @@ import ( // e.g. MQ, pub/sub, Websocket, Lambda // AsynchronousMessageBuilder is the main implementation of the Pact AsynchronousMessageBuilder interface. type AsynchronousMessageBuilder struct { - messageHandle *mockserver.Message + messageHandle *native.Message messagePactV3 *AsynchronousPact // Type to Marshal content into when sending back to the consumer @@ -78,7 +77,7 @@ type AsynchronousMessageBuilderWithContents struct { // WithBinaryContent accepts a binary payload func (m *UnconfiguredAsynchronousMessageBuilder) WithBinaryContent(contentType string, body []byte) *AsynchronousMessageBuilderWithContents { - m.rootBuilder.messageHandle.WithContents(mockserver.INTERACTION_PART_REQUEST, contentType, body) + m.rootBuilder.messageHandle.WithContents(native.INTERACTION_PART_REQUEST, contentType, body) return &AsynchronousMessageBuilderWithContents{ rootBuilder: m.rootBuilder, @@ -87,7 +86,7 @@ func (m *UnconfiguredAsynchronousMessageBuilder) WithBinaryContent(contentType s // WithContent specifies the payload in bytes that the consumer expects to receive func (m *UnconfiguredAsynchronousMessageBuilder) WithContent(contentType string, body []byte) *AsynchronousMessageBuilderWithContents { - m.rootBuilder.messageHandle.WithContents(mockserver.INTERACTION_PART_REQUEST, contentType, body) + m.rootBuilder.messageHandle.WithContents(native.INTERACTION_PART_REQUEST, contentType, body) return &AsynchronousMessageBuilderWithContents{ rootBuilder: m.rootBuilder, @@ -135,7 +134,7 @@ type AsynchronousPact struct { config Config // Reference to the native rust handle - messageserver *mockserver.MessageServer + messageserver *native.MessageServer } // Deprecated: use NewAsynchronousPact @@ -165,7 +164,7 @@ func (p *AsynchronousPact) validateConfig() error { p.config.PactDir = filepath.Join(dir, "pacts") } - p.messageserver = mockserver.NewMessageServer(p.config.Consumer, p.config.Provider) + p.messageserver = native.NewMessageServer(p.config.Consumer, p.config.Provider) p.messageserver.WithMetadata("pact-go", "version", strings.TrimPrefix(command.Version, "v")) return nil @@ -204,12 +203,12 @@ func (p *AsynchronousPact) verifyMessageConsumerRaw(messageToVerify *Asynchronou // 1. Strip out the matchers // Reify the message back to its "example/generated" form body, err := messageToVerify.messageHandle.GetMessageRequestContents() - log.Println("[DEBUG] reified message raw", body) + log.Println("[DEBUG] reified message raw", string(body)) if err != nil { return fmt.Errorf("unexpected response from message server, this is a bug in the framework") } - log.Println("[DEBUG] reified message raw", body) + log.Println("[DEBUG] reified message raw", string(body)) var m MessageContents // err = json.Unmarshal(body, &m) diff --git a/message/v4/asynchronous_message.go b/message/v4/asynchronous_message.go index 76bfa13a2..9e436d5c8 100644 --- a/message/v4/asynchronous_message.go +++ b/message/v4/asynchronous_message.go @@ -12,7 +12,6 @@ import ( "github.com/pact-foundation/pact-go/v2/command" "github.com/pact-foundation/pact-go/v2/internal/native" - mockserver "github.com/pact-foundation/pact-go/v2/internal/native" logging "github.com/pact-foundation/pact-go/v2/log" "github.com/pact-foundation/pact-go/v2/models" ) @@ -22,7 +21,7 @@ import ( // Builder 3: Async with plugin content + transport type AsynchronousMessageBuilder struct { - messageHandle *mockserver.Message + messageHandle *native.Message pact *AsynchronousPact // Type to Marshal content into when sending back to the consumer @@ -47,6 +46,15 @@ func (m *AsynchronousMessageBuilder) GivenWithParameter(state models.ProviderSta return m } +// AddExternalReference records a reference to an external resource (such as a ticket or +// pull request) against the interaction. References appear under +// comments.references[group][name] in the Pact file. May be called multiple times. +func (m *AsynchronousMessageBuilder) AddExternalReference(group, name, value string) *AsynchronousMessageBuilder { + m.messageHandle.WithReference(group, name, value) + + return m +} + // ExpectsToReceive specifies the content it is expecting to be // given from the Provider. The function must be able to handle this // message for the interaction to succeed. @@ -171,7 +179,7 @@ type AsynchronousMessageWithContents struct { // WithContent specifies the payload in bytes that the consumer expects to receive func (m *UnconfiguredAsynchronousMessageBuilder) WithContent(contentType string, body []byte) *AsynchronousMessageWithContents { - m.rootBuilder.messageHandle.WithContents(mockserver.INTERACTION_PART_REQUEST, contentType, body) + m.rootBuilder.messageHandle.WithContents(native.INTERACTION_PART_REQUEST, contentType, body) return &AsynchronousMessageWithContents{ rootBuilder: m.rootBuilder, @@ -219,7 +227,7 @@ type AsynchronousPact struct { config Config // Reference to the native rust handle - messageserver *mockserver.MessageServer + messageserver *native.MessageServer } func NewAsynchronousPact(config Config) (*AsynchronousPact, error) { @@ -246,8 +254,8 @@ func (p *AsynchronousPact) validateConfig() error { p.config.PactDir = filepath.Join(dir, "pacts") } - p.messageserver = mockserver.NewMessageServer(p.config.Consumer, p.config.Provider) - p.messageserver.WithSpecificationVersion(mockserver.SPECIFICATION_VERSION_V4) + p.messageserver = native.NewMessageServer(p.config.Consumer, p.config.Provider) + p.messageserver.WithSpecificationVersion(native.SPECIFICATION_VERSION_V4) p.messageserver.WithMetadata("pact-go", "version", strings.TrimPrefix(command.Version, "v")) return nil @@ -320,7 +328,7 @@ func getAsynchronousMessageWithContents(message *native.Message) (AsynchronousMe }, nil } -func getAsynchronousMessageWithReifiedContents(message *mockserver.Message, reifiedType interface{}) (AsynchronousMessage, error) { +func getAsynchronousMessageWithReifiedContents(message *native.Message, reifiedType interface{}) (AsynchronousMessage, error) { var m AsynchronousMessage var err error diff --git a/message/v4/asynchronous_message_test.go b/message/v4/asynchronous_message_test.go index e01114825..57a89a81f 100644 --- a/message/v4/asynchronous_message_test.go +++ b/message/v4/asynchronous_message_test.go @@ -40,6 +40,25 @@ func TestAsyncTypeSystem(t *testing.T) { } +func TestAsyncAddExternalReference(t *testing.T) { + p, _ := NewAsynchronousPact(Config{ + Consumer: "asyncconsumer", + Provider: "asyncprovider", + PactDir: "/tmp/", + }) + + err := p.AddAsynchronousMessage(). + AddExternalReference("GitHub", "PR-456", "https://github.com/org/repo/pull/456"). + ExpectsToReceive("a message with an external reference"). + WithJSONContent(map[string]string{"event": "user.created"}). + ConsumedBy(func(mc AsynchronousMessage) error { + return nil + }). + Verify(t) + + assert.NoError(t, err) +} + // Sync - with plugin, but no transport // TODO: ExecuteTest has been disabled for now, because it's not very useful func TestAsyncTypeSystem_CsvPlugin_Matcher(t *testing.T) { diff --git a/message/v4/synchronous_message.go b/message/v4/synchronous_message.go index d5c350074..6afa1081b 100644 --- a/message/v4/synchronous_message.go +++ b/message/v4/synchronous_message.go @@ -10,7 +10,6 @@ import ( "github.com/pact-foundation/pact-go/v2/command" "github.com/pact-foundation/pact-go/v2/internal/native" - mockserver "github.com/pact-foundation/pact-go/v2/internal/native" logging "github.com/pact-foundation/pact-go/v2/log" "github.com/pact-foundation/pact-go/v2/models" ) @@ -63,6 +62,15 @@ type UnconfiguredSynchronousMessageBuilder struct { pact *SynchronousPact } +// AddExternalReference records a reference to an external resource (such as a ticket or +// pull request) against the interaction. References appear under +// comments.references[group][name] in the Pact file. May be called multiple times. +func (m *UnconfiguredSynchronousMessageBuilder) AddExternalReference(group, name, value string) *UnconfiguredSynchronousMessageBuilder { + m.messageHandle.WithReference(group, name, value) + + return m +} + // UsingPlugin enables a plugin for use in the current test case func (m *UnconfiguredSynchronousMessageBuilder) UsingPlugin(config PluginConfig) *SynchronousMessageWithPlugin { err := m.pact.mockserver.UsingPlugin(config.Plugin, config.Version) @@ -303,7 +311,7 @@ func (m *SynchronousPact) validateConfig() error { } m.mockserver = native.NewMessageServer(m.config.Consumer, m.config.Provider) - m.mockserver.WithSpecificationVersion(mockserver.SPECIFICATION_VERSION_V4) + m.mockserver.WithSpecificationVersion(native.SPECIFICATION_VERSION_V4) m.mockserver.WithMetadata("pact-go", "version", strings.TrimPrefix(command.Version, "v")) return nil diff --git a/message/v4/synchronous_message_test.go b/message/v4/synchronous_message_test.go index 3b1d4ac24..53765610e 100644 --- a/message/v4/synchronous_message_test.go +++ b/message/v4/synchronous_message_test.go @@ -44,6 +44,27 @@ func TestSyncTypeSystem_NoPlugin(t *testing.T) { assert.NoError(t, err) } +func TestSyncAddExternalReference(t *testing.T) { + p, _ := NewSynchronousPact(Config{ + Consumer: "consumer", + Provider: "provider", + }) + + err := p.AddSynchronousMessage("a sync message with an external reference"). + AddExternalReference("Jira", "TICKET-789", "https://jira.example.com/browse/TICKET-789"). + WithRequest(func(r *SynchronousMessageWithRequestBuilder) { + r.WithJSONContent(map[string]string{"request": "ping"}) + }). + WithResponse(func(r *SynchronousMessageWithResponseBuilder) { + r.WithJSONContent(map[string]string{"response": "pong"}) + }). + ExecuteTest(t, func(m SynchronousMessage) error { + return nil + }) + + assert.NoError(t, err) +} + // Sync - with plugin, but no transport func TestSyncTypeSystem_CsvPlugin_Matcher(t *testing.T) { p, _ := NewSynchronousPact(Config{ diff --git a/message/verifier.go b/message/verifier.go index eeecc25cf..679e6664e 100644 --- a/message/verifier.go +++ b/message/verifier.go @@ -59,7 +59,9 @@ func CreateMessageHandler(messageHandlers Handlers) proxy.Middleware { // Extract message var message messageVerificationHandlerRequest body, err := io.ReadAll(r.Body) - r.Body.Close() + if closeErr := r.Body.Close(); closeErr != nil { + log.Println("[WARN] failed to close request body:", closeErr) + } log.Printf("[TRACE] message verification handler received request: %+s, %s", body, r.URL.Path) if err != nil { diff --git a/provider/verifier.go b/provider/verifier.go index 4d7472077..5834cbbc9 100644 --- a/provider/verifier.go +++ b/provider/verifier.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "net/url" + "strconv" "strings" "testing" "time" @@ -382,7 +383,7 @@ func WaitForPort(port int, network string, address string, timeoutDuration time. log.Printf("[ERROR] expected server to start < %s. %s", timeoutDuration, message) return fmt.Errorf("expected server to start < %s. %s", timeoutDuration, message) case <-time.After(50 * time.Millisecond): - _, err := net.Dial(network, fmt.Sprintf("%s:%d", address, port)) + _, err := net.Dial(network, net.JoinHostPort(address, strconv.Itoa(port))) if err == nil { return nil } diff --git a/provider/verify_request_test.go b/provider/verify_request_test.go index 1cb282678..33c81ccba 100644 --- a/provider/verify_request_test.go +++ b/provider/verify_request_test.go @@ -115,9 +115,13 @@ func TestVerifyRequest(t *testing.T) { const webhookURL, verificationUrl = "pact_changed_webhook_url", "http://localhost:1234/path/to/pact" enablePactUrlFunc := func() func() { const pactUrl = "PACT_URL" - os.Setenv(pactUrl, webhookURL) + if err := os.Setenv(pactUrl, webhookURL); err != nil { + panic(err) + } return func() { - defer os.Unsetenv(pactUrl) + if err := os.Unsetenv(pactUrl); err != nil { + panic(err) + } } } tests := []struct { diff --git a/utils/port.go b/utils/port.go index 0a8af8083..7ec7d3ccf 100644 --- a/utils/port.go +++ b/utils/port.go @@ -21,7 +21,9 @@ func GetFreePort() (int, error) { if err != nil { return 0, err } - defer l.Close() + defer func() { + _ = l.Close() + }() return l.Addr().(*net.TCPAddr).Port, nil } @@ -82,6 +84,8 @@ func checkPort(p int) error { if err != nil { return err } - defer l.Close() + defer func() { + _ = l.Close() + }() return nil } diff --git a/utils/port_test.go b/utils/port_test.go index 77e719613..a283444de 100644 --- a/utils/port_test.go +++ b/utils/port_test.go @@ -123,7 +123,9 @@ func Test_FindPortInRangeWithUsedPorts(t *testing.T) { if err != nil { t.Fatalf("Could not bind to port %s in test", s) } - defer l.Close() + defer func() { + _ = l.Close() + }() p, err := FindPortInRange(c.s) if err != nil && err.Error() != c.errorMsg { t.Fatalf("unexpected error %s", err.Error())