diff --git a/daemon/daemon.go b/daemon/daemon.go index dd94e5ff4..3ace6b402 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -99,8 +99,7 @@ func (d Daemon) Shutdown() { func (d Daemon) StartServer(request types.MockServer, reply *types.MockServer) error { log.Println("[DEBUG] daemon - starting mock server with args:", request.Args) server := &types.MockServer{} - port, svc := d.pactMockSvcManager.NewService(request.Args) - server.Port = port + svc := d.pactMockSvcManager.NewService(request.Args) server.Status = -1 cmd := svc.Start() server.Pid = cmd.Process.Pid @@ -124,7 +123,7 @@ func (d Daemon) VerifyProvider(request types.VerifyRequest, reply *types.Command } var out bytes.Buffer - _, svc := d.verificationSvcManager.NewService(request.Args) + svc := d.verificationSvcManager.NewService(request.Args) cmd, err := svc.Run(&out) if err == nil && cmd.ProcessState != nil && cmd.ProcessState.Success() { diff --git a/daemon/mock_service.go b/daemon/mock_service.go index a658cd496..43c193b19 100644 --- a/daemon/mock_service.go +++ b/daemon/mock_service.go @@ -2,11 +2,9 @@ package daemon import ( "fmt" - "log" "path/filepath" "github.com/kardianos/osext" - "github.com/pact-foundation/pact-go/utils" ) // MockService is a wrapper for the Pact Mock Service. @@ -15,19 +13,14 @@ type MockService struct { } // NewService creates a new MockService with default settings. -func (m *MockService) NewService(args []string) (int, Service) { - port, _ := utils.GetFreePort() - log.Println("[DEBUG] starting mock service on port:", port) - +func (m *MockService) NewService(args []string) Service { m.Args = []string{ "service", - "--port", - fmt.Sprintf("%d", port), } m.Args = append(m.Args, args...) m.Command = getMockServiceCommandPath() - return port, m + return m } func getMockServiceCommandPath() string { diff --git a/daemon/mock_service_test.go b/daemon/mock_service_test.go index fe709fa7d..052528c40 100644 --- a/daemon/mock_service_test.go +++ b/daemon/mock_service_test.go @@ -4,17 +4,13 @@ import "testing" func TestMockService_NewService(t *testing.T) { s := &MockService{} - port, svc := s.NewService([]string{"--foo"}) - - if port <= 0 { - t.Fatalf("Expected non-zero port but got: %d", port) - } + svc := s.NewService([]string{"--foo"}) if svc == nil { t.Fatalf("Expected a non-nil object but got nil") } - if s.Args[3] != "--foo" { + if s.Args[1] != "--foo" { t.Fatalf("Expected '--foo' argument to be passed") } } diff --git a/daemon/service.go b/daemon/service.go index 84b57f630..f72766b55 100644 --- a/daemon/service.go +++ b/daemon/service.go @@ -13,5 +13,5 @@ type Service interface { List() map[int]*exec.Cmd Start() *exec.Cmd Run(io.Writer) (*exec.Cmd, error) - NewService(args []string) (int, Service) + NewService(args []string) Service } diff --git a/daemon/service_mock.go b/daemon/service_mock.go index ac742d28b..b86c1f8b5 100644 --- a/daemon/service_mock.go +++ b/daemon/service_mock.go @@ -66,6 +66,6 @@ func (s *ServiceMock) Start() *exec.Cmd { } // NewService creates a new MockService with default settings. -func (s *ServiceMock) NewService(args []string) (int, Service) { - return s.ServicePort, s +func (s *ServiceMock) NewService(args []string) Service { + return s } diff --git a/daemon/verification_service.go b/daemon/verification_service.go index b1389d506..b37a729d9 100644 --- a/daemon/verification_service.go +++ b/daemon/verification_service.go @@ -24,12 +24,12 @@ type VerificationService struct { // --broker-password // --publish_verification_results // --provider_app_version -func (m *VerificationService) NewService(args []string) (int, Service) { +func (m *VerificationService) NewService(args []string) Service { log.Printf("[DEBUG] starting verification service with args: %v\n", args) m.Args = args m.Command = getVerifierCommandPath() - return -1, m + return m } func getVerifierCommandPath() string { diff --git a/daemon/verification_service_test.go b/daemon/verification_service_test.go index e11acc244..064c338c5 100644 --- a/daemon/verification_service_test.go +++ b/daemon/verification_service_test.go @@ -4,11 +4,7 @@ import "testing" func TestVerificationService_NewService(t *testing.T) { s := &VerificationService{} - port, svc := s.NewService([]string{"--foo bar"}) - - if port != -1 { - t.Fatalf("Expected port to be -1 but got: %d", port) - } + svc := s.NewService([]string{"--foo bar"}) if svc == nil { t.Fatalf("Expected a non-nil object but got nil") diff --git a/dsl/client.go b/dsl/client.go index 4a50f0aef..382fe2640 100644 --- a/dsl/client.go +++ b/dsl/client.go @@ -90,20 +90,22 @@ var waitForPort = func(port int, network string, address string, message string) } // StartServer starts a remote Pact Mock Server. -func (p *PactClient) StartServer(args []string) *types.MockServer { +func (p *PactClient) StartServer(args []string, port int) *types.MockServer { log.Println("[DEBUG] client: starting a server") var res types.MockServer client, err := getHTTPClient(p.Port, p.getNetworkInterface(), p.Address) if err == nil { + args = append(args, []string{"--port", strconv.Itoa(port)}...) err = client.Call(commandStartServer, types.MockServer{Args: args}, &res) + res.Port = port if err != nil { log.Println("[ERROR] rpc:", err.Error()) } } if err == nil { - waitForPort(res.Port, p.getNetworkInterface(), p.Address, fmt.Sprintf(`Timed out waiting for Mock Server to - start on port %d - are you sure it's running?`, res.Port)) + waitForPort(port, p.getNetworkInterface(), p.Address, fmt.Sprintf(`Timed out waiting for Mock Server to + start on port %d - are you sure it's running?`, port)) } return &res diff --git a/dsl/client_test.go b/dsl/client_test.go index 2cafdb19a..720a6cec8 100644 --- a/dsl/client_test.go +++ b/dsl/client_test.go @@ -130,7 +130,7 @@ func TestClient_List(t *testing.T) { func TestClient_ListFail(t *testing.T) { timeoutDuration = 50 * time.Millisecond client := &PactClient{ /* don't supply port */ } - client.StartServer([]string{}) + client.StartServer([]string{}, 0) list := client.ListServers() if len(list.Servers) != 0 { @@ -146,7 +146,8 @@ func TestClient_StartServer(t *testing.T) { defer waitForDaemonToShutdown(port, t) client := &PactClient{Port: port} - client.StartServer([]string{}) + mport, _ := utils.GetFreePort() + client.StartServer([]string{}, mport) if svc.ServiceStartCount != 1 { t.Fatalf("Expected 1 server to have been started, got %d", svc.ServiceStartCount) } @@ -189,7 +190,7 @@ func TestClient_RPCErrors(t *testing.T) { return client.StopServer(&types.MockServer{}) }, &types.MockServer{}: func() interface{} { - return client.StartServer([]string{}) + return client.StartServer([]string{}, 0) }, &types.PactListResponse{}: func() interface{} { return client.ListServers() @@ -297,7 +298,7 @@ func TestClient_StartServerFail(t *testing.T) { timeoutDuration = 50 * time.Millisecond client := &PactClient{ /* don't supply port */ } - server := client.StartServer([]string{}) + server := client.StartServer([]string{}, 0) if server.Port != 0 { t.Fatalf("Expected server to be empty %v", server) } diff --git a/dsl/pact.go b/dsl/pact.go index 34db69ddf..a2c929896 100644 --- a/dsl/pact.go +++ b/dsl/pact.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/logutils" "github.com/pact-foundation/pact-go/types" + "github.com/pact-foundation/pact-go/utils" ) // Pact is the container structure to run the Consumer Pact test cases. @@ -69,6 +70,10 @@ type Pact struct { // Examples include 'tcp', 'tcp4', 'tcp6' // Defaults to 'tcp' Network string + + // Ports MockServer can be deployed to, can be CSV or Range with a dash + // Example "1234", "12324,5667", "1234-5667" + AllowedMockServerPorts string } // AddInteraction creates a new Pact interaction, initialising all @@ -114,6 +119,19 @@ func (p *Pact) Setup(startMockServer bool) *Pact { p.pactClient = client } + // Need to predefine due to scoping + var port int + var perr error + if p.AllowedMockServerPorts != "" { + port, perr = utils.FindPortInRange(p.AllowedMockServerPorts) + } else { + port, perr = utils.GetFreePort() + } + if perr != nil { + log.Println("[ERROR] unable to find free port, mockserver will fail to start") + } + log.Println("[DEBUG] starting mock service on port:", port) + if p.Server == nil && startMockServer { args := []string{ "--pact-specification-version", @@ -128,7 +146,7 @@ func (p *Pact) Setup(startMockServer bool) *Pact { p.Provider, } - p.Server = p.pactClient.StartServer(args) + p.Server = p.pactClient.StartServer(args, port) } return p diff --git a/dsl/pact_test.go b/dsl/pact_test.go index 356155152..b1fcfa92f 100644 --- a/dsl/pact_test.go +++ b/dsl/pact_test.go @@ -168,6 +168,7 @@ func TestPact_VerifyFail(t *testing.T) { } func TestPact_Setup(t *testing.T) { + t.Log("testing pact setup") port, _ := utils.GetFreePort() createDaemon(port, true) @@ -176,17 +177,72 @@ func TestPact_Setup(t *testing.T) { if pact.Server == nil { t.Fatalf("Expected server to be created") } - - pact = &Pact{Port: port, LogLevel: "DEBUG"} - pact.Setup(false) - if pact.Server != nil { + pact2 := &Pact{Port: port, LogLevel: "DEBUG"} + pact2.Setup(false) + if pact2.Server != nil { t.Fatalf("Expected server to be nil") } - if pact.pactClient == nil { + if pact2.pactClient == nil { t.Fatalf("Needed to still have a client") } } +func TestPact_SetupWithMockServerPort(t *testing.T) { + port, _ := utils.GetFreePort() + createDaemon(port, true) + + pact := &Pact{Port: port, LogLevel: "DEBUG", AllowedMockServerPorts: "32768"} + pact.Setup(true) + if pact.Server == nil { + t.Fatalf("Expected server to be created") + } + if pact.Server.Port != 32768 { + t.Fatalf("Expected mock daemon to be started on specific port") + } +} + +func TestPact_SetupWithMockServerPortCSV(t *testing.T) { + port, _ := utils.GetFreePort() + createDaemon(port, true) + + pact := &Pact{Port: port, LogLevel: "DEBUG", AllowedMockServerPorts: "32768,32769"} + pact.Setup(true) + if pact.Server == nil { + t.Fatalf("Expected server to be created") + } + if pact.Server.Port != 32768 { + t.Fatalf("Expected mock daemon to be started on specific port") + } +} + +func TestPact_SetupWithMockServerPortRange(t *testing.T) { + port, _ := utils.GetFreePort() + createDaemon(port, true) + + pact := &Pact{Port: port, LogLevel: "DEBUG", AllowedMockServerPorts: "32768-32770"} + pact.Setup(true) + if pact.Server == nil { + t.Fatalf("Expected server to be created") + } + if pact.Server.Port != 32768 { + t.Fatalf("Expected mock daemon to be started on specific port") + } +} + +func TestPact_Invalidrange(t *testing.T) { + port, _ := utils.GetFreePort() + createDaemon(port, true) + + pact := &Pact{Port: port, LogLevel: "DEBUG", AllowedMockServerPorts: "abc-32770"} + pact.Setup(true) + if pact.Server == nil { + t.Fatalf("Expected server to be created") + } + if pact.Server.Port != 0 { + t.Fatalf("Expected mock daemon to be started on specific port") + } +} + func TestPact_Teardown(t *testing.T) { old := waitForPort defer func() { waitForPort = old }() diff --git a/utils/port.go b/utils/port.go index 7e7242d81..605fcfa10 100644 --- a/utils/port.go +++ b/utils/port.go @@ -1,7 +1,13 @@ // Package utils contains a number of helper / utility functions. package utils -import "net" +import ( + "errors" + "fmt" + "net" + "strconv" + "strings" +) // GetFreePort Gets an available port by asking the kernal for a random port // ready and available for use. @@ -18,3 +24,64 @@ func GetFreePort() (int, error) { defer l.Close() return l.Addr().(*net.TCPAddr).Port, nil } + +// FindPortInRange Iterate through CSV or Range of ports to find open port +// Valid inputs are "8081", "8081,8085", "8081-8085". Do not combine +// list and range +func FindPortInRange(s string) (int, error) { + // Take care of csv and single value + if !strings.Contains(s, "-") { + ports := strings.Split(strings.TrimSpace(s), ",") + for _, p := range ports { + i, err := strconv.Atoi(p) + if err != nil { + return 0, err + } + err = checkPort(i) + if err != nil { + continue + } + return i, nil + } + return 0, errors.New("all passed ports are unusable") + } + // Now take care of ranges + ports := strings.Split(strings.TrimSpace(s), "-") + if len(ports) != 2 { + return 0, errors.New("invalid range passed") + } + lower, err := strconv.Atoi(ports[0]) + if err != nil { + return 0, err + } + upper, err := strconv.Atoi(ports[1]) + if err != nil { + return 0, err + } + if upper < lower { + return 0, errors.New("invalid range passed") + } + for i := lower; i <= upper; i++ { + err = checkPort(i) + if err != nil { + continue + } + return i, nil + } + return 0, errors.New("all passed ports are unusable") +} + +func checkPort(p int) error { + s := fmt.Sprintf("localhost:%d", p) + addr, err := net.ResolveTCPAddr("tcp", s) + if err != nil { + return err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return err + } + defer l.Close() + return nil +} diff --git a/utils/port_test.go b/utils/port_test.go index b33ec3906..e9caa4b00 100644 --- a/utils/port_test.go +++ b/utils/port_test.go @@ -1,6 +1,9 @@ package utils -import "testing" +import ( + "net" + "testing" +) func Test_GetFreePort(t *testing.T) { port, err := GetFreePort() @@ -13,3 +16,131 @@ func Test_GetFreePort(t *testing.T) { t.Fatalf("Expected a port > 0 to be available, got %d", port) } } + +func Test_FindPortInRange(t *testing.T) { + cases := []struct { + description string + s string + port int + errorMsg string + }{ + { + description: "single value", + s: "6667", + port: 6667, + errorMsg: "", + }, + { + description: "csv", + s: "6667,6668,6669", + port: 6667, + errorMsg: "", + }, + { + description: "range", + s: "6668-6669", + port: 6668, + errorMsg: "", + }, + { + description: "invalid single", + s: "abc", + port: 0, + errorMsg: `strconv.Atoi: parsing "abc": invalid syntax`, + }, + { + description: "invalid lower range", + s: "abc-123", + port: 0, + errorMsg: `strconv.Atoi: parsing "abc": invalid syntax`, + }, + { + description: "invalid upper range", + s: "123-abc", + port: 0, + errorMsg: `strconv.Atoi: parsing "abc": invalid syntax`, + }, + { + description: "invalid range", + s: "8888-7777", + port: 0, + errorMsg: "invalid range passed", + }, + { + description: "double range", + s: "6668-6669,7000-7001", + port: 0, + errorMsg: "invalid range passed", + }, + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + p, err := FindPortInRange(c.s) + if err != nil && err.Error() != c.errorMsg { + t.Fatalf("unexpected error %s", err.Error()) + } else if err == nil && c.errorMsg != "" { + t.Fatalf("expected error %s", c.errorMsg) + } + if p != c.port { + t.Fatalf("Expected port to be %d got %d", c.port, p) + } + }) + } +} + +// Need to differentiate from above cases because this one requires +// us to use a port. Because of this the values must remain the same +func Test_FindPortInRangeWithUsedPorts(t *testing.T) { + cases := []struct { + description string + s string + port int + errorMsg string + }{ + { + description: "all ports used csv", + s: "6667", + port: 0, + errorMsg: "all passed ports are unusable", + }, + { + description: "all ports used range", + s: "6667-6667", + port: 0, + errorMsg: "all passed ports are unusable", + }, + } + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + s := "localhost:6667" + addr, err := net.ResolveTCPAddr("tcp", s) + if err != nil { + t.Fatalf("Could not resolve address %s in test", s) + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + t.Fatalf("Could not bind to port in test", s) + } + defer l.Close() + p, err := FindPortInRange(c.s) + if err != nil && err.Error() != c.errorMsg { + t.Fatalf("unexpected error %s", err.Error()) + } else if err == nil && c.errorMsg != "" { + t.Fatalf("expected error %s", c.errorMsg) + } + if p != c.port { + t.Fatalf("Expected port to be %d got %d", c.port, p) + } + }) + } +} + +func Test_checkPort(t *testing.T) { + // Most cases tested above just have this one to test + err := checkPort(-100) + if err == nil { + t.Fatalf("Expected error got none") + } +}