diff --git a/.make/test.mk b/.make/test.mk index 2a7fe7e..c6938f0 100644 --- a/.make/test.mk +++ b/.make/test.mk @@ -117,6 +117,14 @@ ALL_PKGS_EXCLUDE_PATTERN = 'vendor\|app\|tool\/cli\|design\|client\|test' GOANALYSIS_PKGS_EXCLUDE_PATTERN="vendor|app|client|tool/cli" GOANALYSIS_DIRS=$(shell go list -f {{.Dir}} ./... | grep -v -E $(GOANALYSIS_PKGS_EXCLUDE_PATTERN)) +# Folder with contract tests +CONTRACT_TESTS=$(CUR_DIR)/test/contracts + +# Configuration of contract tests +PACT_VERSION ?= 1.0.0 +PACT_BROKER_URL ?= http://pact-broker-pact-broker.193b.starter-ca-central-1.openshiftapps.com +PACT_PROVIDER_BASE_URL ?= https://auth.openshift.io + #------------------------------------------------------------------------------- # Normal test targets # @@ -159,6 +167,7 @@ test-templates-flags: test-unit: test-templates-flags prebuild-check clean-coverage-unit $(COV_PATH_UNIT) .PHONY: test-unit-no-coverage + ## Runs the unit tests and WITHOUT producing coverage files for each package. test-unit-no-coverage: test-templates-flags prebuild-check $(SOURCES) $(call log-info,"Running test: $@") @@ -183,6 +192,45 @@ test-integration-no-coverage: prebuild-check migrate-database $(SOURCES) $(eval TEST_PACKAGES:=$(shell go list ./... | grep -v $(ALL_PKGS_EXCLUDE_PATTERN))) F8_DEVELOPER_MODE_ENABLED=1 F8_RESOURCE_DATABASE=1 F8_RESOURCE_UNIT_TEST=0 F8_POSTGRES_DATABASE=postgres go test -v $(TEST_PACKAGES) +.PHONY: test-contract-auth-consumer +## Runs the consumer side contract tests of the Auth service and produces pact files. +test-contract-auth-consumers: + cd $(CONTRACT_TESTS)/auth && \ + PACT_DIR=$(CONTRACT_TESTS)/pacts \ + PACT_VERSION=$(PACT_VERSION) \ + PACT_BROKER_URL=$(PACT_BROKER_URL) \ + ./consumer-contracts.sh + +.PHONY: test-contract-auth-publish +## Publishes the generated files to a Pact broker. +test-contract-auth-publish: + cd $(CONTRACT_TESTS)/auth && \ + PACT_VERSION=$(PACT_VERSION) \ + PACT_BROKER_URL=$(PACT_BROKER_URL) \ + ./publish-contracts.sh + +.PHONY: test-contract-auth-verify +## Verifies the contracts against the living provider. The pact files are taken from pact directory. +test-contract-auth-verify: + cd $(CONTRACT_TESTS)/auth && \ + PACT_PROVIDER_BASE_URL=$(PACT_PROVIDER_BASE_URL) \ + ./verify-contracts.sh + +.PHONY: test-contract-auth-verify-broker +## Verifies the contracts against the living provider. The pact files are taken from the Pact broker. +test-contract-auth-verify-broker: + cd $(CONTRACT_TESTS)/auth && \ + PACT_VERSION=$(PACT_VERSION) \ + PACT_BROKER_URL=$(PACT_BROKER_URL) \ + PACT_PROVIDER_BASE_URL=$(PACT_PROVIDER_BASE_URL) \ + ./verify-contracts-broker.sh + +.PHONY: clean-test-contract-auth +## Runs the consumer side contract tests and produces pact files. +clean-test-contract-auth: + cd $(CONTRACT_TESTS)/auth && \ + rm -rvf pacts log logs + .PHONY: test-remote ## Runs the remote tests and produces coverage files for each package. test-remote: prebuild-check clean-coverage-remote $(COV_PATH_REMOTE) diff --git a/test/contracts/.gitignore b/test/contracts/.gitignore new file mode 100644 index 0000000..0d4e497 --- /dev/null +++ b/test/contracts/.gitignore @@ -0,0 +1,4 @@ +**/*log +**/*logs +**/*pacts +.password \ No newline at end of file diff --git a/test/contracts/auth/auth_api_status.go b/test/contracts/auth/auth_api_status.go new file mode 100644 index 0000000..6cf52dd --- /dev/null +++ b/test/contracts/auth/auth_api_status.go @@ -0,0 +1,59 @@ +// Package contracts contains a runnable Consumer Pact test example. +package contracts + +import ( + "fmt" + "log" + "net/http" + "testing" + + "github.com/pact-foundation/pact-go/dsl" +) + +// AuthAPIStatus defines contract of /api/status endpoint +func AuthAPIStatus(t *testing.T, pact *dsl.Pact) { + // Pass in test case + var test = func() error { + u := fmt.Sprintf("http://localhost:%d/api/status", pact.Server.Port) + req, err := http.NewRequest("GET", u, nil) + + req.Header.Set("Content-Type", "application/json") + if err != nil { + return err + } + + _, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + return err + } + + type STATUS struct { + buildTime string `json:"buildTime" pact:"example=2018-10-05T10:03:04Z"` + commit string `json:"commit" pact:"example=0f9921980549b2baeb43f6f16cbe794f430f498c"` + configurationStatus string `json:"configurationStatus" pact:"example=OK"` + databaseStatus string `json:"databaseStatus" pact:"example=OK"` + startTime string `json:"startTime" pact:"example=2018-10-09T15:04:50Z"` + } + + // Set up our expected interactions. + pact. + AddInteraction(). + UponReceiving("A request to get status"). + WithRequest(dsl.Request{ + Method: "GET", + Path: dsl.String("/api/status"), + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/json")}, + }). + WillRespondWith(dsl.Response{ + Status: 200, + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/vnd.status+json")}, + Body: dsl.Match(STATUS{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} diff --git a/test/contracts/auth/auth_api_user.go b/test/contracts/auth/auth_api_user.go new file mode 100644 index 0000000..f8d0045 --- /dev/null +++ b/test/contracts/auth/auth_api_user.go @@ -0,0 +1,264 @@ +// Package contracts contains a runnable Consumer Pact test example. +package contracts + +import ( + "fmt" + "log" + "net/http" + "os" + "testing" + + "github.com/pact-foundation/pact-go/dsl" +) + +type Data struct { + Attributes struct { + Bio string `json:"bio" pact:"example=n/a"` + Cluster string `json:"cluster" pact:"example=https://api.starter-us-east-2a.openshift.com/"` + Company string `json:"company" pact:"example=n/a"` + ContextInformation struct { + RecentContexts []struct { + User string `json:"user" pact:"example=c46445eb-2448-4c91-916a-2c1de3e6f63e"` + } `json:"recentContexts"` + RecentSpaces []string `json:"recentSpaces"` + } `json:"contextInformation"` + CreatedAt string `json:"created-at" pact:"example=2018-03-16T14:34:31.615511Z"` + Email string `json:"email" pact:"example=osio-ci+ee10@redhat.com"` + EmailPrivate bool `json:"emailPrivate" pact:"example=false"` + EmailVerified bool `json:"emailVerified" pact:"example=true"` + FeatureLevel string `json:"featureLevel" pact:"example=internal"` + FullName string `json:"fullName" pact:"example=Osio10 Automated Tests"` + IdentityID string `json:"identityID" pact:"example=c46445eb-2448-4c91-916a-2c1de3e6f63e"` + ImageURL string `json:"imageURL" pact:"example=n/a"` + ProviderType string `json:"providerType" pact:"example=kc"` + RegistrationCompleted bool `json:"registrationCompleted" pact:"example=true"` + UpdatedAt string `json:"updated-at" pact:"example=2018-05-30T11:05:23.513612Z"` + URL string `json:"url" pact:"example=n/a"` + UserID string `json:"userID" pact:"example=5f41b66e-6f84-42b3-ab5f-8d9ef21149b1"` + Username string `json:"username" pact:"example=osio-ci-ee10"` + } `json:"attributes"` + ID string `json:"id" pact:"example=c46445eb-2448-4c91-916a-2c1de3e6f63e"` + Links struct { + Related string `json:"related" pact:"example=https://auth.openshift.io/api/users/c46445eb-2448-4c91-916a-2c1de3e6f63e"` + Self string `json:"self" pact:"example=https://auth.openshift.io/api/users/c46445eb-2448-4c91-916a-2c1de3e6f63e"` + } `json:"links"` + Type string `json:"type" pact:"example=identities"` +} + +type User struct { + data Data `json:"data"` +} + +type Users struct { + data []Data `json:"data"` +} + +type InvalidToken struct { + Errors []struct { + Code string `json:"code" pact:"example=token_validation_failed"` + Detail string `json:"detail" pact:"example=token is invalid"` + ID string `json:"id" pact:"example=76J0ww+6"` + Status string `json:"status" pact:"example=401"` + Title string `json:"title" pact:"example=Unauthorized"` + } `json:"errors"` +} + +type MissingToken struct { + Errors []struct { + Code string `json:"code" pact:"example=jwt_security_error"` + Detail string `json:"detail" pact:"example=missing header \"Authorization\""` + ID string `json:"id" pact:"example=FRzHbogQ"` + Status string `json:"status" pact:"example=401"` + Title string `json:"title" pact:"example=Unauthorized"` + } `json:"errors"` +} + +const jwsRegex = "[a-zA-Z0-9\\-_]+?\\.?[a-zA-Z0-9\\-_]+?\\.?([a-zA-Z0-9\\-_]+)?" +const userNameRegex = "[a-zA-Z\\-0-9]+" + +// AuthAPIUserByNameConsumer defines contract of /api/users?filter[username]= endpoint +func AuthAPIUserByNameConsumer(t *testing.T, pact *dsl.Pact) { + userName := os.Getenv("OSIO_USERNAME") + + // Pass in test case + var test = func() error { + url := fmt.Sprintf("http://localhost:%d/api/users?filter[username]=%s", pact.Server.Port, userName) + req, err := http.NewRequest("GET", url, nil) + + req.Header.Set("Content-Type", "application/json") + if err != nil { + return err + } + + _, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + return err + } + + // Set up our expected interactions. + pact. + AddInteraction(). + UponReceiving("A request to get user's information by name"). + WithRequest(dsl.Request{ + Method: "GET", + Path: dsl.String("/api/users"), + Query: dsl.MapMatcher{ + "filter[username]": dsl.Term( + userName, + userNameRegex, + ), + }, + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/json")}, + }). + WillRespondWith(dsl.Response{ + Status: 200, + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/vnd.api+json")}, + Body: dsl.Match(Users{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} + +// AuthAPIUserByIDConsumer defines contract of /api/users/ endpoint +func AuthAPIUserByIDConsumer(t *testing.T, pact *dsl.Pact) { + userID := os.Getenv("OSIO_USER_ID") + + // Pass in test case + var test = func() error { + url := fmt.Sprintf("http://localhost:%d/api/users/%s", pact.Server.Port, userID) + req, err := http.NewRequest("GET", url, nil) + + req.Header.Set("Content-Type", "application/json") + if err != nil { + return err + } + + _, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + return err + } + + // Set up our expected interactions. + pact. + AddInteraction(). + UponReceiving("A request to get user's information by ID"). + WithRequest(dsl.Request{ + Method: "GET", + Path: dsl.Term( + fmt.Sprintf("/api/users/%s", userID), + fmt.Sprintf("/api/users/%s", userNameRegex), + ), + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/json")}, + }). + WillRespondWith(dsl.Response{ + Status: 200, + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/vnd.api+json")}, + Body: dsl.Match(User{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} + +// AuthAPIUserInvalidToken defines contract of /api/user endpoint with invalid auth token +func AuthAPIUserInvalidToken(t *testing.T, pact *dsl.Pact) { + + // Base64 encoded '{"alg":"RS256","kid":"1aA2bBc3CDDdEEefff7gGHH_ii9jJjkkkLl2mmm4NNO","typ":"JWT"}somerandombytes' + var invalidToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFhQTJiQmMzQ0REZEVFZWZmZjdnR0hIX2lpOWpKamtra0xsMm1tbTROTk8iLCJ0eXAiOiJKV1QifXNvbWVyYW5kb21ieXRlcw" + + // Pass in test case + var test = func() error { + url := fmt.Sprintf("http://localhost:%d/api/user", pact.Server.Port) + req, err := http.NewRequest("GET", url, nil) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", invalidToken)) + if err != nil { + return err + } + + _, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + return err + } + + // Set up our expected interactions. + pact. + AddInteraction(). + UponReceiving("A request to get user's information with invalid auth token "). + WithRequest(dsl.Request{ + Method: "GET", + Path: dsl.String("/api/user"), + Headers: dsl.MapMatcher{ + "Content-Type": dsl.String("application/json"), + "Authorization": dsl.Term( + fmt.Sprintf("Bearer %s", invalidToken), + fmt.Sprintf("^Bearer %s$", jwsRegex), + ), + }, + }). + WillRespondWith(dsl.Response{ + Status: 401, + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/vnd.api+json")}, + Body: dsl.Match(InvalidToken{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} + +// AuthAPIUserNoToken defines contract of /api/user endpoint with invalid auth token +func AuthAPIUserNoToken(t *testing.T, pact *dsl.Pact) { + + // Pass in test case + var test = func() error { + url := fmt.Sprintf("http://localhost:%d/api/user", pact.Server.Port) + req, err := http.NewRequest("GET", url, nil) + + req.Header.Set("Content-Type", "application/json") + if err != nil { + return err + } + + _, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + return err + } + + // Set up our expected interactions. + pact. + AddInteraction(). + UponReceiving("A request to get user's information with no auth token "). + WithRequest(dsl.Request{ + Method: "GET", + Path: dsl.String("/api/user"), + Headers: dsl.MapMatcher{ + "Content-Type": dsl.String("application/json"), + }, + }). + WillRespondWith(dsl.Response{ + Status: 401, + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/vnd.api+json")}, + Body: dsl.Match(MissingToken{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} diff --git a/test/contracts/auth/consumer-contracts.sh b/test/contracts/auth/consumer-contracts.sh new file mode 100755 index 0000000..c1d4c69 --- /dev/null +++ b/test/contracts/auth/consumer-contracts.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +. ./setenv.sh + +# run test +go test -v -run 'Test*' +TEST_EXIT=$? + +if [ "$TEST_EXIT" == "0" ]; then + ./publish-contracts.sh +fi \ No newline at end of file diff --git a/test/contracts/auth/contract_test.go b/test/contracts/auth/contract_test.go new file mode 100644 index 0000000..40604ce --- /dev/null +++ b/test/contracts/auth/contract_test.go @@ -0,0 +1,31 @@ +// Package contracts contains a runnable Consumer Pact test example. +package contracts + +import ( + "os" + "testing" + + "github.com/pact-foundation/pact-go/dsl" +) + +// TestAuthAPI runs all user related tests +func TestAuthAPI(t *testing.T) { + // Create Pact connecting to local Daemon + pact := &dsl.Pact{ + Consumer: os.Getenv("PACT_CONSUMER"), + Provider: os.Getenv("PACT_PROVIDER"), + PactDir: os.Getenv("PACT_DIR"), + Host: "localhost", + PactFileWriteMode: "merge", + } + defer pact.Teardown() + + // Test interactions + AuthAPIStatus(t, pact) + AuthAPIUserByNameConsumer(t, pact) + AuthAPIUserByIDConsumer(t, pact) + + // Negative tests + AuthAPIUserInvalidToken(t, pact) + AuthAPIUserNoToken(t, pact) +} diff --git a/test/contracts/auth/publish-contracts.sh b/test/contracts/auth/publish-contracts.sh new file mode 100755 index 0000000..ba19fd9 --- /dev/null +++ b/test/contracts/auth/publish-contracts.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +. ./setenv.sh + +PACT_BROKER_BASIC_AUTH=$(echo -n "${PACT_BROKER_USERNAME}:${PACT_BROKER_PASSWORD}" | base64) + +for PACT_FILE in $(find "$PACT_DIR" -name "*.json"); do + echo "Publishing $PACT_FILE to a Pact broker at $PACT_BROKER_URL" + + PACT_CONSUMER=$(jq '.["consumer"]["name"]' "$PACT_FILE" | tr -d '"') + PACT_PROVIDER=$(jq '.["provider"]["name"]' "$PACT_FILE" | tr -d '"') + + result=$(curl --silent -XPUT -H "Content-Type: application/json" -H "Authorization: Basic ${PACT_BROKER_BASIC_AUTH}" -d@$PACT_FILE "$PACT_BROKER_URL/pacts/provider/$PACT_PROVIDER/consumer/$PACT_CONSUMER/version/$PACT_VERSION") + + if [[ $result = *'"consumer":{"name":"'$PACT_CONSUMER'"},"provider":{"name":"'$PACT_PROVIDER'"}'* ]]; then + echo "Pact successfully published." + else + echo "Unable to publish pact:" + echo "$result" + fi +done diff --git a/test/contracts/auth/setenv.sh b/test/contracts/auth/setenv.sh new file mode 100755 index 0000000..66494d3 --- /dev/null +++ b/test/contracts/auth/setenv.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -a + +# Add the current directory to Go path. +GOPATH="$GOPATH:$(pwd)" + +# A directory to save pact files +PACT_DIR="${PACT_DIR:-$(pwd)/pacts}" +PACT_CONSUMER="${PACT_CONSUMER:-Fabric8TenantService}" +PACT_PROVIDER="${PACT_PROVIDER:-Fabric8AuthService}" + +PACT_BROKER_URL="${PACT_BROKER_URL:-http://pact-broker-pact-broker.193b.starter-ca-central-1.openshiftapps.com}" +PACT_BROKER_USERNAME="${PACT_BROKER_USERNAME:-pact_broker}" +if [ -z "$PACT_BROKER_PASSWORD" ]; then + if [ -f .password ]; then + PACT_BROKER_PASSWORD="$(cat .password)" + fi +fi + +PACT_PROVIDER_BASE_URL="${PACT_PROVIDER_BASE_URL:-https://auth.openshift.io}" diff --git a/test/contracts/auth/verify-contracts-broker.sh b/test/contracts/auth/verify-contracts-broker.sh new file mode 100755 index 0000000..461eec4 --- /dev/null +++ b/test/contracts/auth/verify-contracts-broker.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +. ./setenv.sh + +for PACT_FILE in $(find "$PACT_DIR" -name "*.json"); do + PACT_CONSUMER=$(jq '.["consumer"]["name"]' "$PACT_FILE" | tr -d '"') + PACT_PROVIDER=$(jq '.["provider"]["name"]' "$PACT_FILE" | tr -d '"') + + pact-provider-verifier "$PACT_BROKER_URL/pacts/provider/$PACT_PROVIDER/consumer/$PACT_CONSUMER/versions/$PACT_VERSION" --broker-username="$PACT_BROKER_USERNAME" --broker-password="$PACT_BROKER_PASSWORD" --provider-base-url "$PACT_PROVIDER_BASE_URL" +done diff --git a/test/contracts/auth/verify-contracts.sh b/test/contracts/auth/verify-contracts.sh new file mode 100755 index 0000000..356a1e3 --- /dev/null +++ b/test/contracts/auth/verify-contracts.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +. ./setenv.sh + +for PACT_FILE in $(find "$PACT_DIR" -name "*.json"); do + pact-provider-verifier "$PACT_FILE" --provider-base-url "$PACT_PROVIDER_BASE_URL" +done