Skip to content

Commit

Permalink
feat(examples): add vanilla golang ServeMux, update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
mefellows committed Sep 26, 2016
1 parent 1c059cd commit 52b7e19
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 8 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ how to get going.
* Unzip the package into a known location, and add to the `PATH`.
* Run `pact-go` to see what options are available.

*NOTE*: Don't despair! We are [working](https://github.com/pact-foundation/pact-go/tree/feature/native)
*NOTE*: Don't despair! We are [working](https://github.com/pact-foundation/pact-go/tree/feature/native)
on a pure Go implementation that won't require this install step - please be
patient or help us implement the [roadmap](https://github.com/pact-foundation/pact-go/wiki/Native-implementation-roadmap).

Expand Down Expand Up @@ -363,7 +363,8 @@ ProviderStatesURL: GET URL to fetch all available states (see types.Provider
ProviderStatesSetupURL: POST URL to set the provider state (see types.ProviderState)
```

Example routes using the standard Go http package might look like this:
Example routes using the standard Go http package might look like this, note
the `/states` endpoint returns a list of available states for each known consumer:

```go
// Return known provider states to the verifier (ProviderStatesURL):
Expand Down Expand Up @@ -459,6 +460,7 @@ pact := Pact{
## Examples

* [API Consumer](https://github.com/pact-foundation/pact-go/tree/master/examples/)
* [Golang ServeMux](https://github.com/pact-foundation/pact-go/tree/master/examples/mux)
* [Go Kit](https://github.com/pact-foundation/pact-go/tree/master/examples/go-kit)
* [Gin](https://github.com/pact-foundation/pact-go/tree/master/examples/gin)

Expand Down
10 changes: 8 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,14 @@ Provider side Pact testing, involves verifying that the contract - the Pact file
A typical Provider side test would like something like:
func TestProvider_PactContract(t *testing.T) {
// Create Pact, connecting to local Daemon
// Ensure the port matches the daemon port!
pact := Pact{
Port: 6666,
}
go startMyAPI("http://localhost:8000")
response := pact.VerifyProvider(types.VerifyRequest{
err := pact.VerifyProvider(types.VerifyRequest{
ProviderBaseURL: "http://localhost:8000",
PactURLs: []string{"./pacts/my_consumer-my_provider.json"},
ProviderStatesURL: "http://localhost:8000/states",
Expand Down Expand Up @@ -218,7 +223,8 @@ are:
ProviderStatesURL: GET URL to fetch all available states (see types.ProviderStates)
ProviderStatesSetupURL: POST URL to set the provider state (see types.ProviderState)
Example routes using the standard http package might look like this:
Example routes using the standard Go http package might look like this, note
the `/states` endpoint returns a list of available states for each known consumer:
// Return known provider states to the verifier (ProviderStatesURL):
mux.HandleFunc("/states", func(w http.ResponseWriter, req *http.Request) {
Expand Down
9 changes: 6 additions & 3 deletions examples/gin/provider/user_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ func providerStateSetup(c *gin.Context) {
}
}

// Fetch available Provider States
// This path returns all states available for the consumer 'billy'
// Note that the key for the array is the consumer name (in this case, 'billy')
// The values should match a Given("...") block in the Interaction. Essentially,
// this broadcasts the allowed states of the provider for verification, it is not
// necessary for all consumers to use all states.
func providerStates(c *gin.Context) {
c.JSON(http.StatusOK, map[string][]string{
"billy": []string{
Expand Down Expand Up @@ -100,9 +104,8 @@ var billyUnauthorized = &examples.UserRepository{
// Setup the Pact client.
func createPact() dsl.Pact {
// Create Pact connecting to local Daemon
pactDaemonPort := 6666
return dsl.Pact{
Port: pactDaemonPort,
Port: 6666,
Consumer: "billy",
Provider: "bobby",
LogDir: logDir,
Expand Down
6 changes: 5 additions & 1 deletion examples/go-kit/provider/user_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ func startInstrumentedProvider() {
logger.Log("[DEBUG] configured provider state: ", state.State)
})

// This path returns all states available for
// This path returns all states available for the onsumer 'billy'
// Note that the key for the array is the onsumer name (in this case, 'billy')
// The values should match a Given("...") block in the Interaction. Essentially,
// this broadcasts the allowed states of the provider for verification, it is not
// necessary for all consumers to use all states.
router.Methods("GET").Path("/states").HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
logger.Log("[DEBUG] returning available provider states")
w.Header().Add("Content-Type", "application/json")
Expand Down
64 changes: 64 additions & 0 deletions examples/mux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Example - HTTP Mux

Example using the standard libraries Mux Router.

The following example is a simple Login UI ([Consumer](#consumer)) that calls a
User Service ([Provider](#provider)) using JSON over HTTP.

The API currently exposes a single `Login` endpoint at `POST /users/login`, which
the Consumer uses to authenticate a User.

We test 3 scenarios, highlighting the use of [Provider States](/pact-foundation/pact-go#provider#provider-states):

1. When the user "Billy" exists, and we perform a login, we expect an HTTP `200`
1. When the user "Billy" does not exists, and we perform a login, we expect an HTTP `404`
1. When the user "Billy" is unauthorized, and we perform a login, we expect an HTTP `403`

# Getting started

Before any of these tests can be run, ensure Pact Go is installed and run the
daemon in the background:

```
go get ./...
<path to>/pact-go daemon
```


## Provider

The "Provider" is a real HTTP API containing the `/users/login` API call:

```
cd provider
go test -v .
```

This will spin up the Provider API with extra routes added for the handling of
provider states, run the verification process and report back success/failure.

### Running the Provider

The provider can be run as a standalone service:

```
go run cmd/usersvc/main.go
# 200
curl -v -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{
"username":"billy",
"password":"issilly"
}' "http://localhost:8080/users/login"
# 403
curl -v -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{
"username":"billy",
"password":"issilly"
}' "http://localhost:8080/users/login"
# 404
curl -v -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{
"username":"someoneelse",
"password":"issilly"
}' "http://localhost:8080/users/login"
```
26 changes: 26 additions & 0 deletions examples/mux/provider/cmd/usersvc/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"fmt"
"log"
"net"
"net/http"

"github.com/pact-foundation/pact-go/examples/mux/provider"
"github.com/pact-foundation/pact-go/utils"
)

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users/login", provider.UserLogin)

port, _ := utils.GetFreePort()
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
log.Fatal(err)
}
defer ln.Close()

log.Printf("API starting: port %d (%s)", port, ln.Addr())
log.Printf("API terminating: %v", http.Serve(ln, mux))
}
51 changes: 51 additions & 0 deletions examples/mux/provider/user_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package provider

import (
"encoding/json"
"io/ioutil"
"net/http"

"github.com/pact-foundation/pact-go/examples/types"
)

var userRepository = &types.UserRepository{
Users: map[string]*types.User{
"billy": &types.User{
Name: "billy",
Username: "billy",
Password: "issilly",
},
},
}

// UserLogin is the login route.
var UserLogin = func(w http.ResponseWriter, r *http.Request) {
var login types.LoginRequest
w.Header().Set("Content-Type", "application/json; charset=utf-8")

body, err := ioutil.ReadAll(r.Body)
defer r.Body.Close()

if err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
return
}

err = json.Unmarshal(body, &login)
if err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
return
}

user, err := userRepository.ByUsername(login.Username)
if err != nil {
w.WriteHeader(http.StatusNotFound)
} else if user.Username != login.Username || user.Password != login.Password {
w.WriteHeader(http.StatusUnauthorized)
} else {
w.WriteHeader(http.StatusOK)
res := types.LoginResponse{User: user}
resBody, _ := json.Marshal(res)
w.Write(resBody)
}
}
144 changes: 144 additions & 0 deletions examples/mux/provider/user_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package provider

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"testing"

"github.com/pact-foundation/pact-go/dsl"
examples "github.com/pact-foundation/pact-go/examples/types"
"github.com/pact-foundation/pact-go/types"
"github.com/pact-foundation/pact-go/utils"
)

// The actual Provider test itself
func TestPact_Provider(t *testing.T) {
go startInstrumentedProvider()

pact := createPact()

// Verify the Provider with local Pact Files
err := pact.VerifyProvider(types.VerifyRequest{
ProviderBaseURL: fmt.Sprintf("http://localhost:%d", port),
PactURLs: []string{fmt.Sprintf("%s/billy-bobby.json", pactDir)},
ProviderStatesURL: fmt.Sprintf("http://localhost:%d/states", port),
ProviderStatesSetupURL: fmt.Sprintf("http://localhost:%d/setup", port),
})

if err != nil {
t.Fatal("Error:", err)
}
}

// Starts the provider API with hooks for provider states.
// This essentially mirrors the main.go file, with extra routes added.
func startInstrumentedProvider() {
mux := http.NewServeMux()
mux.HandleFunc("/users/login", UserLogin)
mux.HandleFunc("/setup", providerStateSetupFunc)
mux.HandleFunc("/states", providerStatesFunc)

ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
log.Fatal(err)
}
defer ln.Close()

log.Printf("API starting: port %d (%s)", port, ln.Addr())
log.Printf("API terminating: %v", http.Serve(ln, mux))

}

// Get all states route.
var providerStatesFunc = func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
body, _ := json.Marshal(providerStates)
w.Write(body)
}

// Set current provider state route.
var providerStateSetupFunc = func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
var state types.ProviderState

body, err := ioutil.ReadAll(r.Body)
defer r.Body.Close()

if err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
return
}

err = json.Unmarshal(body, &state)
if err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
return
}

// Setup database for different states
if state.State == "User billy exists" {
userRepository = billyExists
} else if state.State == "User billy is unauthorized" {
userRepository = billyUnauthorized
} else {
userRepository = billyDoesNotExist
}
}

// This path returns all states available for the consumer 'billy'
// Note that the key for the array is the consumer name (in this case, 'billy')
// The values should match a Given("...") block in the Interaction. Essentially,
// this broadcasts the allowed states of the provider for verification, it is not
// necessary for all consumers to use all states.
var providerStates = map[string][]string{
"billy": []string{
"User billy exists",
"User billy does not exist",
"User billy is unauthorized"},
}

// Configuration / Test Data
var dir, _ = os.Getwd()
var pactDir = fmt.Sprintf("%s/../../pacts", dir)
var logDir = fmt.Sprintf("%s/log", dir)
var port, _ = utils.GetFreePort()

// Provider States data sets
var billyExists = &examples.UserRepository{
Users: map[string]*examples.User{
"billy": &examples.User{
Name: "billy",
Username: "billy",
Password: "issilly",
},
},
}

var billyDoesNotExist = &examples.UserRepository{}

var billyUnauthorized = &examples.UserRepository{
Users: map[string]*examples.User{
"billy": &examples.User{
Name: "billy",
Username: "billy",
Password: "issilly1",
},
},
}

// Setup the Pact client.
func createPact() dsl.Pact {
// Create Pact connecting to local Daemon
return dsl.Pact{
Port: 6666,
Consumer: "billy",
Provider: "bobby",
LogDir: logDir,
PactDir: pactDir,
}
}

0 comments on commit 52b7e19

Please sign in to comment.