diff --git a/.make/test.mk b/.make/test.mk index 2e3c5451..9e373a93 100644 --- a/.make/test.mk +++ b/.make/test.mk @@ -171,6 +171,37 @@ test-integration-benchmark: prebuild-check migrate-database $(SOURCES) $(eval TEST_PACKAGES:=$(shell go list ./... | grep -v $(ALL_PKGS_EXCLUDE_PATTERN))) AUTH_DEVELOPER_MODE_ENABLED=1 AUTH_LOG_LEVEL=error AUTH_RESOURCE_DATABASE=1 AUTH_RESOURCE_UNIT_TEST=0 F8_LOG_LEVEL=$(F8_LOG_LEVEL) go test -vet off -run=^$$ -bench=. -cpu 1,2,4 -test.benchmem $(GO_TEST_VERBOSITY_FLAG) $(TEST_PACKAGES) +.PHONY: test-contracts-consumer +## Runs the consumer part of the contract tests to re-generate the local pact file +test-contracts-consumer: + $(call log-info,"Running test: $@") + $(eval TEST_PACKAGES:=$(shell go list ./... | grep 'contracts/consumer')) + PACT_DIR=$(PWD)/test/contracts/pacts \ + PACT_CONSUMER=Fabric8AuthGeneralConsumer \ + PACT_PROVIDER=Fabric8Auth \ + PACT_VERSION=1.0.0 \ + go test -count=1 $(GO_TEST_VERBOSITY_FLAG) $(TEST_PACKAGES) + +.PHONY: test-contracts-no-coverage +## Runs the contract tests WITHOUT producing coverage files for each package. +## Make sure you ran "make dev" before you run this target. +## The following env variables needs to be set in environment: +## - RHD account credentials: +## OSIO_USERNAME +## OSIO_PASSWORD +## - Service account credentials (according to https://github.com/fabric8-services/fabric8-auth/blob/master/configuration/conf-files/service-account-secrets.conf#L30) +## AUTH_SERVICE_ACCOUNT_CLIENT_ID +## AUTH_SERVICE_ACCOUNT_CLIENT_SERCRET +test-contracts-no-coverage: prebuild-check migrate-database $(SOURCES) + $(call log-info,"Running test: $@") + $(eval TEST_PACKAGES:=$(shell go list ./... | grep 'contracts/provider')) + PACT_DIR=$(PWD)/test/contracts/pacts \ + PACT_CONSUMER=Fabric8AuthGeneralConsumer \ + PACT_PROVIDER=Fabric8Auth \ + PACT_VERSION=1.0.0 \ + PACT_PROVIDER_BASE_URL=http://localhost:8089 \ + go test -count=1 $(GO_TEST_VERBOSITY_FLAG) $(TEST_PACKAGES) + .PHONY: test-remote-with-coverage ## Runs the remote tests and produces coverage files for each package. test-remote-with-coverage: prebuild-check clean-coverage-remote $(COV_PATH_REMOTE) diff --git a/Gopkg.lock b/Gopkg.lock index 212b2c71..aca5f0c3 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -163,6 +163,12 @@ packages = ["proto"] revision = "8ee79997227bf9b34611aee7946ae64735e6fd93" +[[projects]] + name = "github.com/google/uuid" + packages = ["."] + revision = "d460ce9f8df2e77fb1ba55ca87fafed96c607494" + version = "v1.0.0" + [[projects]] name = "github.com/hashicorp/go-immutable-radix" packages = ["."] @@ -188,6 +194,12 @@ ] revision = "37ab263305aaeb501a60eb16863e808d426e37f2" +[[projects]] + name = "github.com/hashicorp/logutils" + packages = ["."] + revision = "a335183dfd075f638afcc820c90591ca3c97eba6" + version = "v1.0.0" + [[projects]] name = "github.com/howeyc/fsnotify" packages = ["."] @@ -270,6 +282,16 @@ packages = ["."] revision = "5a0325d7fafaac12dda6e7fb8bd222ec1b69875e" +[[projects]] + name = "github.com/pact-foundation/pact-go" + packages = [ + "dsl", + "types", + "utils" + ] + revision = "v1.0.0-beta.2" + version = "v1.0.0-beta.2" + [[projects]] name = "github.com/pelletier/go-buffruneio" packages = ["."] @@ -302,6 +324,16 @@ revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" +[[projects]] + branch = "master" + name = "github.com/pmacik/loginusers-go" + packages = [ + "common", + "config", + "loginusers" + ] + revision = "a5075e2f6b4e063f3b3eb27eb008d0da844b5b54" + [[projects]] name = "github.com/pmezard/go-difflib" packages = ["difflib"] @@ -392,6 +424,11 @@ revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" version = "v1.2.1" +[[projects]] + name = "github.com/tebeka/selenium" + packages = ["."] + revision = "master" + [[projects]] branch = "master" name = "github.com/wadey/gocovmerge" @@ -492,6 +529,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a445ea99112a636e11bab7e3c551d1ec3646c7a29ba8136e0c56af24e6363740" + inputs-digest = "2c2ef4adf2f747c6ea769746a9bb8ec4edfa66b875c2373e7808b18f7404f0b1" solver-name = "gps-cdcl" solver-version = 1 diff --git a/test/contracts/.gitignore b/test/contracts/.gitignore new file mode 100644 index 00000000..08d5753c --- /dev/null +++ b/test/contracts/.gitignore @@ -0,0 +1,3 @@ +consumer/logs +provider/log +pacts/provider-*.json \ No newline at end of file diff --git a/test/contracts/consumer/auth_api_status.go b/test/contracts/consumer/auth_api_status.go new file mode 100644 index 00000000..d765b315 --- /dev/null +++ b/test/contracts/consumer/auth_api_status.go @@ -0,0 +1,55 @@ +package consumer + +import ( + "fmt" + "log" + "net/http" + "testing" + + "github.com/fabric8-services/fabric8-auth/test/contracts/model" + "github.com/pact-foundation/pact-go/dsl" +) + +// AuthAPIStatus defines contract of /api/status endpoint +func AuthAPIStatus(t *testing.T, pact *dsl.Pact) { + + log.Printf("Invoking AuthAPIStatus now\n") + + // 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 + } + + // Set up our expected interactions. + pact. + AddInteraction(). + Given("Auth service is up and running."). + 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(model.APIStatusMessage{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} diff --git a/test/contracts/consumer/auth_api_token.go b/test/contracts/consumer/auth_api_token.go new file mode 100644 index 00000000..95df709b --- /dev/null +++ b/test/contracts/consumer/auth_api_token.go @@ -0,0 +1,55 @@ +package consumer + +import ( + "fmt" + "log" + "net/http" + "testing" + + "github.com/fabric8-services/fabric8-auth/test/contracts/model" + "github.com/pact-foundation/pact-go/dsl" +) + +// AuthAPITokenKeys defines contract of /api/status endpoint +func AuthAPITokenKeys(t *testing.T, pact *dsl.Pact) { + + log.Printf("Invoking AuthAPITokenKeys now\n") + + // Pass in test case + var test = func() error { + u := fmt.Sprintf("http://localhost:%d/api/token/keys", pact.Server.Port) + req, err := http.NewRequest("GET", u, nil) + + req.Header.Set("Accept", "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(). + Given("Auth service is up and running."). + UponReceiving("A request to get public keys"). + WithRequest(dsl.Request{ + Method: "GET", + Path: dsl.String("/api/token/keys"), + Headers: dsl.MapMatcher{"Accept": dsl.String("application/json")}, + }). + WillRespondWith(dsl.Response{ + Status: 200, + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/vnd.publickeys+json")}, + Body: dsl.Match(model.TokenKeys{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} diff --git a/test/contracts/consumer/auth_api_user.go b/test/contracts/consumer/auth_api_user.go new file mode 100644 index 00000000..865fd5b2 --- /dev/null +++ b/test/contracts/consumer/auth_api_user.go @@ -0,0 +1,257 @@ +package consumer + +import ( + "fmt" + "log" + "net/http" + "testing" + + "github.com/fabric8-services/fabric8-auth/test/contracts/model" + "github.com/pact-foundation/pact-go/dsl" +) + +// AuthAPIUserByName defines contract of /api/users?filter[username]= endpoint +func AuthAPIUserByName(t *testing.T, pact *dsl.Pact, userName string) { + + log.Println("Invoking AuthAPIUserByName test interaction now\n", 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(). + Given("User with a given username exists."). + UponReceiving("A request to get user's information by username"). + WithRequest(dsl.Request{ + Method: "GET", + Path: dsl.String("/api/users"), + Query: dsl.MapMatcher{ + "filter[username]": dsl.Term( + userName, + model.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(model.Users{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} + +// AuthAPIUserByID defines contract of /api/users/ endpoint +func AuthAPIUserByID(t *testing.T, pact *dsl.Pact, userID string) { + + log.Printf("Invoking AuthAPIUserByID test interaction now\n") + + // 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(). + Given("User with a given ID exists."). + 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", model.UserIDRegex), + ), + 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(model.User{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} + +// AuthAPIUserByToken defines contract of /api/user endpoint with valid auth token +// passed as 'Authorization: Bearer ...' header +func AuthAPIUserByToken(t *testing.T, pact *dsl.Pact, userToken string) { + + log.Printf("Invoking AuthAPIUserByToken test interaction now\n") + + // 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", userToken)) + if err != nil { + return err + } + + _, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + return err + } + + // Set up our expected interactions. + pact. + AddInteraction(). + Given("A user exists with the given valid token."). + UponReceiving("A request to get user's information with valid 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", userToken), + fmt.Sprintf("^Bearer %s$", model.JWSRegex), + ), + }, + }). + WillRespondWith(dsl.Response{ + Status: 200, + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/vnd.api+json")}, + Body: dsl.Match(model.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, invalidToken string) { + + log.Printf("Invoking AuthAPIUserInvalidToken test interaction now\n") + + // 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(). + Given("No user exists with the given token valid."). + 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$", model.JWSRegex), + ), + }, + }). + WillRespondWith(dsl.Response{ + Status: 401, + Headers: dsl.MapMatcher{"Content-Type": dsl.String("application/vnd.api+json")}, + Body: dsl.Match(model.InvalidTokenMessage{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} + +// AuthAPIUserNoToken defines contract of /api/user endpoint with missing auth token +func AuthAPIUserNoToken(t *testing.T, pact *dsl.Pact) { + + log.Printf("Invoking AuthAPIUserNoToken test interaction now\n") + + // 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(). + Given("Any user exists but no auth token was provided."). + 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(model.MissingTokenMessage{}), + }) + + // Verify + if err := pact.Verify(test); err != nil { + log.Fatalf("Error on Verify: %v", err) + } +} diff --git a/test/contracts/consumer/consumer_test.go b/test/contracts/consumer/consumer_test.go new file mode 100644 index 00000000..98cba1c0 --- /dev/null +++ b/test/contracts/consumer/consumer_test.go @@ -0,0 +1,72 @@ +package consumer + +import ( + "fmt" + "log" + "os" + "strings" + "testing" + + "github.com/fabric8-services/fabric8-auth/test/contracts/model" + "github.com/pact-foundation/pact-go/dsl" + "github.com/pact-foundation/pact-go/types" +) + +// TestAuthAPIConsumer runs all user related tests +func TestAuthAPIConsumer(t *testing.T) { + + log.SetOutput(os.Stdout) + + var pactDir = os.Getenv("PACT_DIR") + var pactConsumer = os.Getenv("PACT_CONSUMER") + var pactProvider = os.Getenv("PACT_PROVIDER") + var pactVersion = os.Getenv("PACT_VERSION") + + var pactBrokerURL = os.Getenv("PACT_BROKER_URL") + var pactBrokerUsername = os.Getenv("PACT_BROKER_USERNAME") + var pactBrokerPassword = os.Getenv("PACT_BROKER_PASSWORD") + + // Create Pact connecting to local Daemon + pact := &dsl.Pact{ + Consumer: pactConsumer, + Provider: pactProvider, + PactDir: pactDir, + Host: "localhost", + LogLevel: "INFO", + PactFileWriteMode: "overwrite", + SpecificationVersion: 2, + } + defer pact.Teardown() + + // Test interactions + AuthAPIStatus(t, pact) + AuthAPIUserByName(t, pact, model.TestUserName) + AuthAPIUserByID(t, pact, model.TestUserID) + AuthAPIUserByToken(t, pact, model.TestJWSToken) + AuthAPITokenKeys(t, pact) + + // Negative tests + AuthAPIUserInvalidToken(t, pact, model.TestInvalidJWSToken) + AuthAPIUserNoToken(t, pact) + + log.Printf("All tests done, writting a pact to %s directory.\n", pactDir) + pact.WritePact() + + if pactBrokerURL != "" { + log.Printf("Publishing pact to a broker %s\n", pactBrokerURL) + + p := dsl.Publisher{} + err := p.Publish(types.PublishRequest{ + PactURLs: []string{fmt.Sprintf("%s/%s-%s.json", pactDir, strings.ToLower(pactConsumer), strings.ToLower(pactProvider))}, + PactBroker: pactBrokerURL, + BrokerUsername: pactBrokerUsername, + BrokerPassword: pactBrokerPassword, + ConsumerVersion: pactVersion, + Tags: []string{"latest"}, + }) + + if err != nil { + log.Fatalf("Unable to publish pact to a broker %s:\n%q\n", pactBrokerURL, err) + } + } +} diff --git a/test/contracts/model/model.go b/test/contracts/model/model.go new file mode 100644 index 00000000..da047283 --- /dev/null +++ b/test/contracts/model/model.go @@ -0,0 +1,109 @@ +package model + +// APIStatusMessage represents a service status message returned by /api/status endpoint. +type APIStatusMessage 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"` +} + +// UserData represents a JSON object containing user's info. +type UserData struct { + Attributes struct { + Bio string `json:"bio" pact:"example=n/a,regex=^[ a-zA-Z0-9,\\./]*$"` + Cluster string `json:"cluster" pact:"example=openshift.developer.osio/"` + Company string `json:"company" pact:"example=n/a,regex=^[ a-zA-Z0-9,\\./]*$"` + CreatedAt string `json:"created-at" pact:"example=2018-03-16T14:34:31.615511Z"` + Email string `json:"email" pact:"example=developer@email.com"` + EmailPrivate bool `json:"emailPrivate" pact:"example=false,regex=^[(true)(false)]$"` + EmailVerified bool `json:"emailVerified" pact:"example=true,regex=^[(true)(false)]$"` + FeatureLevel string `json:"featureLevel" pact:"example=internal"` + FullName string `json:"fullName" pact:"example=Osio Developer"` + IdentityID string `json:"identityID" pact:"example=00000000-0000-4000-a000-000000000000"` + ImageURL string `json:"imageURL" pact:"example=n/a"` + ProviderType string `json:"providerType" pact:"example=kc"` + RegistrationCompleted bool `json:"registrationCompleted" pact:"example=true,regex=^[(true)(false)]$"` + 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=developer"` + } `json:"attributes"` + ID string `json:"id" pact:"example=00000000-0000-4000-a000-000000000000"` + Links struct { + Related string `json:"related" pact:"example=http://localhost:8089/api/users/00000000-0000-4000-a000-000000000000"` + Self string `json:"self" pact:"example=http://localhost:8089/api/users/00000000-0000-4000-a000-000000000000"` + } `json:"links"` + Type string `json:"type" pact:"example=identities"` +} + +// User represents a JSON object of a single user. +type User struct { + Data UserData `json:"data"` +} + +// Users represents a JSON object of a collection of users. +type Users struct { + Data []UserData `json:"data"` +} + +// EmptyData represents an empty message returned by API +type EmptyData struct { + Data []interface{} `json:"data"` +} + +// InvalidTokenMessage represents a message returned when the Authorization header is invalid in secured endpoint calls +type InvalidTokenMessage 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"` +} + +// MissingTokenMessage represents a message returned when the Authorization header is missing to secured endpoint calls +type MissingTokenMessage 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"` +} + +// TokenKeys represents JSON message returned by /api/token/keys endpoint +type TokenKeys struct { + Keys []struct { + Alg string `json:"alg" pact:"example=RS256"` + E string `json:"e" pact:"example=AQAB"` + Kid string `json:"kid" pact:"example=abcdefghijklmnopqrstuvwxyz-0123456789_ABCDE,regex=^[a-zA-Z0-9_-]{43}$"` + Kty string `json:"kty" pact:"example=RSA"` + N string `json:"n" pact:"example=abcdefghijklmnopqrstuvwxyz-0123456789_ABCDE,regex=^[a-zA-Z0-9_-]+"` + Use string `json:"use" pact:"example=sig"` + } `json:"keys"` +} + +// JWSRegex is a regular expression for matching JWS tokens +const JWSRegex = "[a-zA-Z0-9\\-_]+?\\.?[a-zA-Z0-9\\-_]+?\\.?([a-zA-Z0-9\\-_]+)?" + +// TestInvalidJWSToken Base64 encoded '{"alg":"RS256","kid":"1111111111111111111111111111111111111111111","typ":"JWT"}somerandombytes' +const TestInvalidJWSToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEiLCJ0eXAiOiJKV1QifXNvbWVyYW5kb21ieXRlcw" + +// TestJWSToken contains Base64 encoded '{"alg":"RS256","kid":"0000000000000000000000000000000000000000000","typ":"JWT"}somerandombytes' +const TestJWSToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJ0eXAiOiJKV1QifXNvbWVyYW5kb21ieXRlcw" + +// UserNameRegex is a regular expression for matching usernames. +const UserNameRegex = "[a-zA-Z\\-0-9]+" + +// UserIDRegex is a regular expression for matching user IDs. +const UserIDRegex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}" + +//TestUserID contains user id placeholder +const TestUserID = "00000000-0000-4000-a000-000000000000" + +//TestUserName contains username placeholder +const TestUserName = "testuser00000000" diff --git a/test/contracts/pacts/fabric8authgeneralconsumer-fabric8auth.json b/test/contracts/pacts/fabric8authgeneralconsumer-fabric8auth.json new file mode 100644 index 00000000..a65f945a --- /dev/null +++ b/test/contracts/pacts/fabric8authgeneralconsumer-fabric8auth.json @@ -0,0 +1,589 @@ +{ + "consumer": { + "name": "Fabric8AuthGeneralConsumer" + }, + "provider": { + "name": "Fabric8Auth" + }, + "interactions": [ + { + "description": "A request to get status", + "providerState": "Auth service is up and running.", + "request": { + "method": "GET", + "path": "/api/status", + "headers": { + "Content-Type": "application/json" + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.status+json" + }, + "body": { + "buildTime": "2018-10-05T10:03:04Z", + "commit": "0f9921980549b2baeb43f6f16cbe794f430f498c", + "configurationStatus": "OK", + "databaseStatus": "OK", + "startTime": "2018-10-09T15:04:50Z" + }, + "matchingRules": { + "$.body.buildTime": { + "match": "type" + }, + "$.body.commit": { + "match": "type" + }, + "$.body.configurationStatus": { + "match": "type" + }, + "$.body.databaseStatus": { + "match": "type" + }, + "$.body.startTime": { + "match": "type" + } + } + } + }, + { + "description": "A request to get user's information by username", + "providerState": "User with a given username exists.", + "request": { + "method": "GET", + "path": "/api/users", + "query": "filter[username]=testuser00000000", + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "$.query.filter[username][0]": { + "match": "regex", + "regex": "[a-zA-Z\\-0-9]+" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.api+json" + }, + "body": { + "data": [ + { + "attributes": { + "bio": "n/a", + "cluster": "openshift.developer.osio/", + "company": "n/a", + "created-at": "2018-03-16T14:34:31.615511Z", + "email": "developer@email.com", + "emailPrivate": true, + "emailVerified": true, + "featureLevel": "internal", + "fullName": "Osio Developer", + "identityID": "00000000-0000-4000-a000-000000000000", + "imageURL": "n/a", + "providerType": "kc", + "registrationCompleted": true, + "updated-at": "2018-05-30T11:05:23.513612Z", + "url": "n/a", + "userID": "5f41b66e-6f84-42b3-ab5f-8d9ef21149b1", + "username": "developer" + }, + "id": "00000000-0000-4000-a000-000000000000", + "links": { + "related": "http://localhost:8089/api/users/00000000-0000-4000-a000-000000000000", + "self": "http://localhost:8089/api/users/00000000-0000-4000-a000-000000000000" + }, + "type": "identities" + } + ] + }, + "matchingRules": { + "$.body.data": { + "min": 1 + }, + "$.body.data[*].*": { + "match": "type" + }, + "$.body.data[*].attributes.bio": { + "match": "regex", + "regex": "^[ a-zA-Z0-9,\\\\.\\/]*$" + }, + "$.body.data[*].attributes.cluster": { + "match": "type" + }, + "$.body.data[*].attributes.company": { + "match": "regex", + "regex": "^[ a-zA-Z0-9,\\\\.\\/]*$" + }, + "$.body.data[*].attributes.created-at": { + "match": "type" + }, + "$.body.data[*].attributes.email": { + "match": "type" + }, + "$.body.data[*].attributes.emailPrivate": { + "match": "type" + }, + "$.body.data[*].attributes.emailVerified": { + "match": "type" + }, + "$.body.data[*].attributes.featureLevel": { + "match": "type" + }, + "$.body.data[*].attributes.fullName": { + "match": "type" + }, + "$.body.data[*].attributes.identityID": { + "match": "type" + }, + "$.body.data[*].attributes.imageURL": { + "match": "type" + }, + "$.body.data[*].attributes.providerType": { + "match": "type" + }, + "$.body.data[*].attributes.registrationCompleted": { + "match": "type" + }, + "$.body.data[*].attributes.updated-at": { + "match": "type" + }, + "$.body.data[*].attributes.url": { + "match": "type" + }, + "$.body.data[*].attributes.userID": { + "match": "type" + }, + "$.body.data[*].attributes.username": { + "match": "type" + }, + "$.body.data[*].id": { + "match": "type" + }, + "$.body.data[*].links.related": { + "match": "type" + }, + "$.body.data[*].links.self": { + "match": "type" + }, + "$.body.data[*].type": { + "match": "type" + } + } + } + }, + { + "description": "A request to get user's information by ID", + "providerState": "User with a given ID exists.", + "request": { + "method": "GET", + "path": "/api/users/00000000-0000-4000-a000-000000000000", + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "$.path": { + "match": "regex", + "regex": "\\/api\\/users\\/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.api+json" + }, + "body": { + "data": { + "attributes": { + "bio": "n/a", + "cluster": "openshift.developer.osio/", + "company": "n/a", + "created-at": "2018-03-16T14:34:31.615511Z", + "email": "developer@email.com", + "emailPrivate": true, + "emailVerified": true, + "featureLevel": "internal", + "fullName": "Osio Developer", + "identityID": "00000000-0000-4000-a000-000000000000", + "imageURL": "n/a", + "providerType": "kc", + "registrationCompleted": true, + "updated-at": "2018-05-30T11:05:23.513612Z", + "url": "n/a", + "userID": "5f41b66e-6f84-42b3-ab5f-8d9ef21149b1", + "username": "developer" + }, + "id": "00000000-0000-4000-a000-000000000000", + "links": { + "related": "http://localhost:8089/api/users/00000000-0000-4000-a000-000000000000", + "self": "http://localhost:8089/api/users/00000000-0000-4000-a000-000000000000" + }, + "type": "identities" + } + }, + "matchingRules": { + "$.body.data.attributes.bio": { + "match": "regex", + "regex": "^[ a-zA-Z0-9,\\\\.\\/]*$" + }, + "$.body.data.attributes.cluster": { + "match": "type" + }, + "$.body.data.attributes.company": { + "match": "regex", + "regex": "^[ a-zA-Z0-9,\\\\.\\/]*$" + }, + "$.body.data.attributes.created-at": { + "match": "type" + }, + "$.body.data.attributes.email": { + "match": "type" + }, + "$.body.data.attributes.emailPrivate": { + "match": "type" + }, + "$.body.data.attributes.emailVerified": { + "match": "type" + }, + "$.body.data.attributes.featureLevel": { + "match": "type" + }, + "$.body.data.attributes.fullName": { + "match": "type" + }, + "$.body.data.attributes.identityID": { + "match": "type" + }, + "$.body.data.attributes.imageURL": { + "match": "type" + }, + "$.body.data.attributes.providerType": { + "match": "type" + }, + "$.body.data.attributes.registrationCompleted": { + "match": "type" + }, + "$.body.data.attributes.updated-at": { + "match": "type" + }, + "$.body.data.attributes.url": { + "match": "type" + }, + "$.body.data.attributes.userID": { + "match": "type" + }, + "$.body.data.attributes.username": { + "match": "type" + }, + "$.body.data.id": { + "match": "type" + }, + "$.body.data.links.related": { + "match": "type" + }, + "$.body.data.links.self": { + "match": "type" + }, + "$.body.data.type": { + "match": "type" + } + } + } + }, + { + "description": "A request to get user's information with valid auth token ", + "providerState": "A user exists with the given valid token.", + "request": { + "method": "GET", + "path": "/api/user", + "headers": { + "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJ0eXAiOiJKV1QifXNvbWVyYW5kb21ieXRlcw", + "Content-Type": "application/json" + }, + "matchingRules": { + "$.headers.Authorization": { + "match": "regex", + "regex": "^Bearer [a-zA-Z0-9\\-_]+?\\.?[a-zA-Z0-9\\-_]+?\\.?([a-zA-Z0-9\\-_]+)?$" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.api+json" + }, + "body": { + "data": { + "attributes": { + "bio": "n/a", + "cluster": "openshift.developer.osio/", + "company": "n/a", + "created-at": "2018-03-16T14:34:31.615511Z", + "email": "developer@email.com", + "emailPrivate": true, + "emailVerified": true, + "featureLevel": "internal", + "fullName": "Osio Developer", + "identityID": "00000000-0000-4000-a000-000000000000", + "imageURL": "n/a", + "providerType": "kc", + "registrationCompleted": true, + "updated-at": "2018-05-30T11:05:23.513612Z", + "url": "n/a", + "userID": "5f41b66e-6f84-42b3-ab5f-8d9ef21149b1", + "username": "developer" + }, + "id": "00000000-0000-4000-a000-000000000000", + "links": { + "related": "http://localhost:8089/api/users/00000000-0000-4000-a000-000000000000", + "self": "http://localhost:8089/api/users/00000000-0000-4000-a000-000000000000" + }, + "type": "identities" + } + }, + "matchingRules": { + "$.body.data.attributes.bio": { + "match": "regex", + "regex": "^[ a-zA-Z0-9,\\\\.\\/]*$" + }, + "$.body.data.attributes.cluster": { + "match": "type" + }, + "$.body.data.attributes.company": { + "match": "regex", + "regex": "^[ a-zA-Z0-9,\\\\.\\/]*$" + }, + "$.body.data.attributes.created-at": { + "match": "type" + }, + "$.body.data.attributes.email": { + "match": "type" + }, + "$.body.data.attributes.emailPrivate": { + "match": "type" + }, + "$.body.data.attributes.emailVerified": { + "match": "type" + }, + "$.body.data.attributes.featureLevel": { + "match": "type" + }, + "$.body.data.attributes.fullName": { + "match": "type" + }, + "$.body.data.attributes.identityID": { + "match": "type" + }, + "$.body.data.attributes.imageURL": { + "match": "type" + }, + "$.body.data.attributes.providerType": { + "match": "type" + }, + "$.body.data.attributes.registrationCompleted": { + "match": "type" + }, + "$.body.data.attributes.updated-at": { + "match": "type" + }, + "$.body.data.attributes.url": { + "match": "type" + }, + "$.body.data.attributes.userID": { + "match": "type" + }, + "$.body.data.attributes.username": { + "match": "type" + }, + "$.body.data.id": { + "match": "type" + }, + "$.body.data.links.related": { + "match": "type" + }, + "$.body.data.links.self": { + "match": "type" + }, + "$.body.data.type": { + "match": "type" + } + } + } + }, + { + "description": "A request to get public keys", + "providerState": "Auth service is up and running.", + "request": { + "method": "GET", + "path": "/api/token/keys", + "headers": { + "Accept": "application/json" + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.publickeys+json" + }, + "body": { + "keys": [ + { + "alg": "RS256", + "e": "AQAB", + "kid": "abcdefghijklmnopqrstuvwxyz-0123456789_ABCDE", + "kty": "RSA", + "n": "abcdefghijklmnopqrstuvwxyz-0123456789_ABCDE", + "use": "sig" + } + ] + }, + "matchingRules": { + "$.body.keys": { + "min": 1 + }, + "$.body.keys[*].*": { + "match": "type" + }, + "$.body.keys[*].alg": { + "match": "type" + }, + "$.body.keys[*].e": { + "match": "type" + }, + "$.body.keys[*].kid": { + "match": "regex", + "regex": "^[a-zA-Z0-9_-]{43}$" + }, + "$.body.keys[*].kty": { + "match": "type" + }, + "$.body.keys[*].n": { + "match": "regex", + "regex": "^[a-zA-Z0-9_-]+" + }, + "$.body.keys[*].use": { + "match": "type" + } + } + } + }, + { + "description": "A request to get user's information with invalid auth token ", + "providerState": "No user exists with the given token valid.", + "request": { + "method": "GET", + "path": "/api/user", + "headers": { + "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEiLCJ0eXAiOiJKV1QifXNvbWVyYW5kb21ieXRlcw", + "Content-Type": "application/json" + }, + "matchingRules": { + "$.headers.Authorization": { + "match": "regex", + "regex": "^Bearer [a-zA-Z0-9\\-_]+?\\.?[a-zA-Z0-9\\-_]+?\\.?([a-zA-Z0-9\\-_]+)?$" + } + } + }, + "response": { + "status": 401, + "headers": { + "Content-Type": "application/vnd.api+json" + }, + "body": { + "errors": [ + { + "code": "token_validation_failed", + "detail": "token is invalid", + "id": "76J0ww+6", + "status": "401", + "title": "Unauthorized" + } + ] + }, + "matchingRules": { + "$.body.errors": { + "min": 1 + }, + "$.body.errors[*].*": { + "match": "type" + }, + "$.body.errors[*].code": { + "match": "type" + }, + "$.body.errors[*].detail": { + "match": "type" + }, + "$.body.errors[*].id": { + "match": "type" + }, + "$.body.errors[*].status": { + "match": "type" + }, + "$.body.errors[*].title": { + "match": "type" + } + } + } + }, + { + "description": "A request to get user's information with no auth token ", + "providerState": "Any user exists but no auth token was provided.", + "request": { + "method": "GET", + "path": "/api/user", + "headers": { + "Content-Type": "application/json" + } + }, + "response": { + "status": 401, + "headers": { + "Content-Type": "application/vnd.api+json" + }, + "body": { + "errors": [ + { + "code": "jwt_security_error", + "detail": "missing header \"Authorization\"", + "id": "FRzHbogQ", + "status": "401", + "title": "Unauthorized" + } + ] + }, + "matchingRules": { + "$.body.errors": { + "min": 1 + }, + "$.body.errors[*].*": { + "match": "type" + }, + "$.body.errors[*].code": { + "match": "type" + }, + "$.body.errors[*].detail": { + "match": "type" + }, + "$.body.errors[*].id": { + "match": "type" + }, + "$.body.errors[*].status": { + "match": "type" + }, + "$.body.errors[*].title": { + "match": "type" + } + } + } + } + ], + "metadata": { + "pactSpecification": { + "version": "2.0.0" + } + } +} \ No newline at end of file diff --git a/test/contracts/provider/provider_test.go b/test/contracts/provider/provider_test.go new file mode 100644 index 00000000..146b32c1 --- /dev/null +++ b/test/contracts/provider/provider_test.go @@ -0,0 +1,142 @@ +package provider + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/fabric8-services/fabric8-auth/test/contracts/model" + "github.com/pact-foundation/pact-go/dsl" + "github.com/pact-foundation/pact-go/types" +) + +// TestAuthAPIProvider verifies the provider +func TestAuthAPIProvider(t *testing.T) { + + var pactDir = os.Getenv("PACT_DIR") + var pactProviderBaseURL = os.Getenv("PACT_PROVIDER_BASE_URL") + + var pactConsumer = os.Getenv("PACT_CONSUMER") + var pactProvider = os.Getenv("PACT_PROVIDER") + + var pactVersion = os.Getenv("PACT_VERSION") + + var pactBrokerUsername = os.Getenv("PACT_BROKER_USERNAME") + var pactBrokerPassword = os.Getenv("PACT_BROKER_PASSWORD") + var pactBrokerURL = os.Getenv("PACT_BROKER_URL") + + var userName = os.Getenv("OSIO_USERNAME") + var userPassword = os.Getenv("OSIO_PASSWORD") + + /* + log.Printf("pactDir=%s\n", pactDir) + log.Printf("pactProviderBaseURL=%s\n", pactProviderBaseURL) + log.Printf("pactConsumer=%s\n", pactConsumer) + log.Printf("pactProvider=%s\n", pactProvider) + log.Printf("pactVersion=%s\n", pactVersion) + log.Printf("pactBrokerUsername=%s\n", pactBrokerUsername) + log.Printf("pactBrokerPassword=%s\n", pactBrokerPassword) + log.Printf("pactBrokerURL=%s\n", pactBrokerURL) + log.Printf("userName=%s\n", userName) + log.Printf("userPassword=%s\n", userPassword) + */ + + // Create Pact connecting to local Daemon + pact := &dsl.Pact{ + Consumer: pactConsumer, + Provider: pactProvider, + PactDir: pactDir, + Host: "localhost", + LogLevel: "INFO", + SpecificationVersion: 2, + } + defer pact.Teardown() + + var providerSetupHost = "localhost" // this should ultimately be part of the provider api (developer mode: on) + var providerSetupPort = 8080 + + // Set provider into initial state + providerInfo := Setup(providerSetupHost, providerSetupPort, pactProviderBaseURL, userName, userPassword) + + if providerInfo == nil { + log.Fatalf("Error setting up provider initial state") + } + var pactContent string + + if pactBrokerURL != "" { + // Download pact file from pact broker + pactContent = pactFromBroker( + pactBrokerURL, pactBrokerUsername, pactBrokerPassword, + pactConsumer, pactProvider, pactVersion, + ) + } else { + // Load a pact file cached locally + pactFile := fmt.Sprintf("%s/%s-%s.json", pactDir, strings.ToLower(pactConsumer), strings.ToLower(pactProvider)) + pactContent = pactFromFile(pactFile) + } + + // Replace placeholders in pact file with real data (user name/id/token) + pactContent = strings.Replace(pactContent, model.TestUserName, providerInfo.User.Data.Attributes.Username, -1) + pactContent = strings.Replace(pactContent, model.TestUserID, providerInfo.User.Data.ID, -1) + pactContent = strings.Replace(pactContent, model.TestJWSToken, providerInfo.Tokens.AccessToken, -1) + + pactFilePath := fmt.Sprintf("%s/provider-%s-%s.json", pactDir, strings.ToLower(pactConsumer), strings.ToLower(pactProvider)) + pactFile, err := os.Create(pactFilePath) + if err != nil { + log.Fatal(err) + } + defer pactFile.Close() + + _, err = pactFile.WriteString(pactContent) + + // Verify the Provider with local Pact Files + pact.VerifyProvider(t, types.VerifyRequest{ + ProviderBaseURL: pactProviderBaseURL, + PactURLs: []string{pactFilePath}, + ProviderStatesSetupURL: fmt.Sprintf("http://%s:%d/pact/setup", providerSetupHost, providerSetupPort), + }) + + log.Println("Test Passed!") +} + +// pactFromFile reads a pact from a given file and returns as string +func pactFromFile(pactFile string) string { + f, err := ioutil.ReadFile(pactFile) + if err != nil { + log.Fatalf("Unable to read pact file: %s", pactFile) + } + return string(f) +} + +// pactFromBroker reads a pact from a given pact broker and returns as string +func pactFromBroker(pactBrokerURL string, pactBrokerUsername string, pactBrokerPassword string, pactConsumer string, pactProvider string, pactVersion string) string { + + var httpClient = &http.Client{ + Timeout: time.Second * 30, + } + pactURL := fmt.Sprintf("%s/pacts/provider/%s/consumer/%s/version/%s", pactBrokerURL, pactProvider, pactConsumer, pactVersion) + request, err := http.NewRequest("GET", pactURL, nil) + if err != nil { + log.Fatal(err) + } + request.Header.Set("Accept", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", pactBrokerUsername, pactBrokerPassword))))) + + log.Printf("Downloading a pact file from pact broker: %s", pactURL) + response, err := httpClient.Do(request) + if err != nil { + log.Fatal(err) + } + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + + // Replace placeholders in pact file with real data (user name/id/token) + return string(responseBody) +} diff --git a/test/contracts/provider/setup.go b/test/contracts/provider/setup.go new file mode 100644 index 00000000..1f84e5fd --- /dev/null +++ b/test/contracts/provider/setup.go @@ -0,0 +1,248 @@ +package provider + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "time" + + "github.com/pmacik/loginusers-go/config" + "github.com/pmacik/loginusers-go/loginusers" + + "github.com/fabric8-services/fabric8-auth/test/contracts/model" + "github.com/google/uuid" +) + +type providerStateInfo struct { + // Consumer name + Consumer string `json:"consumer"` + // State + State string `json:"state"` + // States + States []string `json:"states"` +} + +type ProviderInitialState struct { + User model.User + Tokens loginusers.Tokens +} + +type createUserAttributes struct { + Bio string `json:"bio"` + Cluster string `json:"cluster"` + Username string `json:"username"` + Email string `json:"email"` + RhdUserID string `json:"rhd_user_id"` +} + +type createUserData struct { + createUserAttributes `json:"attributes"` + Type string `json:"type" pact:"example=identities"` +} + +type createUserRequest struct { + createUserData `json:"data"` +} + +// Setup starts a setup service for a provider - should be replaced by a provider setup endpoint +func Setup(setupHost string, setupPort int, providerBaseURL string, userName string, userPassword string) *ProviderInitialState { + log.SetOutput(os.Stdout) + + // Create test user in Auth and retun user info (such as id) + log.Printf("Making sure user %s is created...", userName) + var user = createUser(providerBaseURL, userName) + if user == nil { + log.Fatalf("Error creating/getting user") + } + log.Printf("Provider setup with user ID: %s", user.Data.ID) + + loginUsersConfig := config.DefaultConfig() + loginUsersConfig.Auth.ServerAddress = providerBaseURL + // Log user in to get tokens + userTokens, err := loginusers.OAuth2(userName, userPassword, loginUsersConfig) + if err != nil { + log.Fatalf("Unable to login user: %s", err) + return nil + } + + go setupEndpoint(setupHost, setupPort) + + return &ProviderInitialState{ + User: *user, + Tokens: *userTokens, + } +} + +func setupEndpoint(setupHost string, setupPort int) { + http.HandleFunc("/pact/setup", func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Fatalf(">>> ERROR: Unable to read request body.\n %q", err) + return + } + + var providerState providerStateInfo + json.Unmarshal(body, &providerState) + + switch providerState.State { + case "User with a given username exists.", + "User with a given ID exists.", + "A user exists with the given valid token.", + "No user exists with the given token valid.", + "Any user exists but no auth token was provided.", + "Auth service is up and running.": + log.Printf(">>>> %s\n", providerState.State) + default: + errorMessage(w, fmt.Sprintf("State '%s' not impemented.", providerState.State)) + return + } + fmt.Fprintf(w, "Provider states has ben set up.\n") + }) + + var setupURL = fmt.Sprintf("%s:%d", setupHost, setupPort) + log.Printf(">>> Starting ProviderSetup and listening at %s\n", setupURL) + log.Fatal(http.ListenAndServe(setupURL, nil)) +} + +func errorMessage(w http.ResponseWriter, errorMessage string) { + w.WriteHeader(500) + fmt.Fprintf(w, `{"error": "%s"}`, errorMessage) +} + +func createUser(providerBaseURL string, userName string) *model.User { + + var httpClient = &http.Client{ + Timeout: time.Second * 10, + } + + log.Println("Getting the auth service account token") + authServiceAccountToken := serviceAccountToken(providerBaseURL) + + rhdUserUUID, _ := uuid.NewUUID() + message := &createUserRequest{ + createUserData: createUserData{ + createUserAttributes: createUserAttributes{ + Bio: "Contract testing user account", + Cluster: "localhost", + Email: fmt.Sprintf("%s@email.com", userName), + Username: userName, + RhdUserID: rhdUserUUID.String(), + }, + }, + } + + messageBytes, err := json.Marshal(message) + if err != nil { + log.Fatalf("createUser: Error marshalling JSON object:\n%q", err) + } + + request, err := http.NewRequest("POST", fmt.Sprintf("%s/api/users", providerBaseURL), bytes.NewBuffer(messageBytes)) + if err != nil { + log.Fatalf("createUser: Error creating HTTP request:\n%q", err) + } + request.Header.Add("Content-Type", "application/json") + request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authServiceAccountToken)) + + log.Println("Sending a request to create a user") + response, err := httpClient.Do(request) + if err != nil { + log.Fatalf("createUser: Error sending HTTP request:\n%q", err) + } + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + + if response.StatusCode != 200 { + if response.StatusCode == 409 { //user already exists + log.Printf("User %s already exists, getting user info.", userName) + response2, err := http.Get(fmt.Sprintf("%s/api/users?filter[username]=%s", providerBaseURL, userName)) + if err != nil { + log.Fatalf("userExists: Error creating HTTP request:\n%q", err) + } + defer response2.Body.Close() + + responseBody, err := ioutil.ReadAll(response2.Body) + // log.Printf("User info:\n%s\n", responseBody) + if response2.StatusCode != 200 { + log.Fatalf("userExists: Something went wrong with reading response body: %s", responseBody) + } + var users model.Users + err = json.Unmarshal(responseBody, &users) + if err != nil { + log.Fatalf("userExists: Unable to unmarshal response body: %s", err) + } + var user = &model.User{ + Data: users.Data[0], + } + log.Printf("User found with ID: %s", user.Data.ID) + return user + } + log.Fatalf("createUser: Something went wrong with reading response body: %s", responseBody) + } + + var user model.User + err = json.Unmarshal(responseBody, &user) + if err != nil { + log.Fatalf("createUser: Unable to unmarshal response body: %s", err) + } + log.Printf("User created with ID: %s", user.Data.ID) + return &user +} + +// ServiceAccountTokenRequest represents a request JSON body +type ServiceAccountTokenRequest struct { + GrantType string `json:"grant_type"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} + +// ServiceAccountTokenResponse represents a response JSON body +type ServiceAccountTokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` +} + +func serviceAccountToken(providerBaseURL string) string { + var httpClient = &http.Client{ + Timeout: time.Second * 10, + } + authClientID := os.Getenv("AUTH_SERVICE_ACCOUNT_CLIENT_ID") + authClienSecret := os.Getenv("AUTH_SERVICE_ACCOUNT_CLIENT_SECRET") + + message, err := json.Marshal(&ServiceAccountTokenRequest{ + GrantType: "client_credentials", + ClientID: authClientID, + ClientSecret: authClienSecret, + }) + + // log.Printf("Message: %s", string(message)) + + if err != nil { + log.Fatalf("serviceAccountToken: Error marshalling json object: %q\n", err) + } + request, err := http.NewRequest("POST", fmt.Sprintf("%s/api/token", providerBaseURL), bytes.NewBuffer(message)) + request.Header.Add("Content-Type", "application/json") + + response, err := httpClient.Do(request) + if err != nil { + log.Fatalf("serviceAccountToken: Error sending HTTP request: %q\n", err) + } + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + + if response.StatusCode != 200 { + log.Fatalf("serviceAccountToken: Something went wrong with reading response body: %s", responseBody) + } + + var tokenResponse ServiceAccountTokenResponse + err = json.Unmarshal(responseBody, &tokenResponse) + if err != nil { + log.Fatalf("serviceAccountToken: Unable to unmarshal response body: %s", err) + } + return tokenResponse.AccessToken +}