package emulatorsvc import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "reflect" "regexp" "testing" "time" "github.com/stretchr/testify/assert" "golang.org/x/oauth2" "edge-infra.dev/pkg/lib/fog" "edge-infra.dev/pkg/sds/emergencyaccess/eaconst" "edge-infra.dev/pkg/sds/emergencyaccess/msgdata" "edge-infra.dev/pkg/sds/emergencyaccess/types" ) const apiV2 = "/api/v2" const https = "https://" var ( defaultAccessToken = "accessToken" defaultSessionTarget = types.Target{Projectid: "", Bannerid: "bannerID", Storeid: "storeID", Terminalid: "terminalID"} defaultSessionResolvedTarget = types.Target{ Projectid: "project-UUID", Bannerid: "banner-UUID", Storeid: "store-UUID", Terminalid: "terminal-UUID", } testSessionID string defaultResponseDataJSON = []byte(` { "type": "Output", "exitCode": 0, "output": "hello\n", "timestamp": "01-01-2023 00:00:00", "duration": 0.1 }`) defaultAttrMap = map[string]string{ "bannerId": "banner", "storeId": "store", "terminalId": "terminal", "sessionId": "orderingKey", "identity": "identity", "version": "1.0", "signature": "signature", "request-message-uuid": "test", "commandId": "testID", } ) type tHelper interface { Helper() } func EqualError(message string) assert.ErrorAssertionFunc { return func(t assert.TestingT, err error, i ...interface{}) bool { if tt, ok := t.(tHelper); ok { tt.Helper() } return assert.EqualError(t, err, message, i...) } } // MatchesError does a regex assertion against the error message func MatchesError(message string) assert.ErrorAssertionFunc { return func(t assert.TestingT, err error, i ...interface{}) bool { if tt, ok := t.(tHelper); ok { tt.Helper() } return assert.Regexp(t, regexp.MustCompile(message), err.Error(), i...) } } func TestMain(m *testing.M) { // Create temp dir where files get created on initialisation dir, err := os.MkdirTemp("", "") if err != nil { panic(err) } os.Setenv(envFilePathDir, dir) m.Run() // Remove temporary directory os.Unsetenv(envFilePathDir) err = os.RemoveAll(dir) if err != nil { panic(err) } } // Test Servers TODO: move these to separate file? // Tests the startSession query. asserts the payload target is the same as the defaultSessionTarget // Returns the resolved target UUID's in the expected headers func startSessionServer(t *testing.T) *httptest.Server { mux := http.NewServeMux() mux.HandleFunc("/startSession", func(w http.ResponseWriter, r *http.Request) { data, err := io.ReadAll(r.Body) assert.NoError(t, err) var payload types.StartSessionPayload err = json.Unmarshal(data, &payload) assert.NoError(t, err) assert.EqualValues(t, defaultSessionTarget, payload.Target) w.Header().Add("X-EA-ProjectID", defaultSessionResolvedTarget.Projectid) w.Header().Add("X-EA-BannerID", defaultSessionResolvedTarget.Bannerid) w.Header().Add("X-EA-StoreID", defaultSessionResolvedTarget.Storeid) w.Header().Add("X-EA-TerminalID", defaultSessionResolvedTarget.Terminalid) }) server := httptest.NewServer(mux) return server } // checks no error is returned on connection and the target in the payload matches func TestConnect(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // tests are in the server endpoint server := startSessionServer(t) defer server.Close() t.Setenv("RCLI_GATEWAY_HOST", server.URL) es, err := New(ctx, Config{}) assert.NoError(t, err) es.idToken = &oauth2.Token{ AccessToken: defaultAccessToken, Expiry: time.Now().Add(24 * time.Hour), } err = es.Connect(ctx, "", "bannerID", "storeID", "terminalID") assert.NoError(t, err) assert.Equal(t, defaultSessionResolvedTarget, es.session.target) } // TODO: This should write back an error that apierrorhandler.ParseJSONAPIError() can parse func connectionFailureServer(status int) *httptest.Server { mux := http.NewServeMux() mux.HandleFunc("/empty/startSession", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(status) }) mux.HandleFunc("/json/startSession", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(status) _, _ = w.Write([]byte(` { "errorCode": 61111, "errorMessage": "Bad things happen", "details": ["lots", "of", "bad", "things"] } `)) }) server := httptest.NewServer(mux) return server } func TestConnectFail(t *testing.T) { cases := map[string]struct { path string errAssert assert.ErrorAssertionFunc }{ "Error on non-OK": { path: "/empty/", errAssert: MatchesError(`error calling startSession API \(http://127.0.0.1:.*/empty/startSession\), status \(403 Forbidden\)`), }, "APIError response": { path: "/json/", errAssert: EqualError("61111: Bad things happen"), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() server := connectionFailureServer(403) defer server.Close() host := server.URL if tc.path != "" { host += tc.path } t.Setenv("RCLI_GATEWAY_HOST", host) es, err := New(ctx, Config{}) assert.NoError(t, err) es.idToken = &oauth2.Token{ AccessToken: defaultAccessToken, } err = es.Connect(ctx, "", "bannerID", "storeID", "terminalID") tc.errAssert(t, err) }) } } // checks whether send delivers a correctly parsed send payload to // a mock endpoint func TestSend(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() server := sendCommandServer(t) defer server.Close() t.Setenv("RCLI_GATEWAY_HOST", server.URL) es, err := New(ctx, Config{}) assert.NoError(t, err) es.idToken = &oauth2.Token{ AccessToken: defaultAccessToken, } err = es.Connect(ctx, "", "bannerID", "storeID", "terminalID") // testSessionID is checked by sendCommandServer testSessionID = es.session.ID assert.NoError(t, err) commandID, err := es.Send("test") assert.NoError(t, err) assert.Equal(t, "abcd", commandID) } func TestDarkmode(t *testing.T) { tests := map[string]struct { darkmode bool }{ "Darkmode True": { darkmode: true, }, "Darkmode False": { darkmode: false, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // set up the test server server := sendCommandServer(t, assertDarkmode(tc.darkmode)) defer server.Close() t.Setenv("RCLI_GATEWAY_HOST", server.URL) es, err := New(ctx, Config{}) assert.NoError(t, err) es.idToken = &oauth2.Token{ AccessToken: defaultAccessToken, Expiry: time.Now().Add(24 * time.Hour), } // set darkmode es.SetDarkmode(tc.darkmode) err = es.Connect(ctx, "", "bannerID", "storeID", "terminalID") // testSessionID is checked by sendCommandServer testSessionID = es.session.ID assert.NoError(t, err) _, err = es.Send("test") assert.NoError(t, err) }) } } // tests whether the context gets cancelled and the displaychannel is closed func TestEnd(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() server := endSessionServer(t) defer server.Close() t.Setenv("RCLI_GATEWAY_HOST", server.URL) es, err := New(ctx, Config{}) es.idToken = &oauth2.Token{ AccessToken: defaultAccessToken, } assert.NoError(t, err) err = es.Connect(ctx, "projectID", "bannerID", "storeID", "terminalID") assert.NoError(t, err) testSessionID = es.session.ID err = es.End() assert.NoError(t, err) done := <-es.session.context.Done() assert.NotNil(t, done) } func TestSetGatewayURLs(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) defer cancel() log := fog.New(fog.To(io.Discard)) ctx = fog.IntoContext(ctx, log) // Precondition, env var behaviour is tested in TestSetGatewayURLsEnvVar _, ok := os.LookupEnv(envGatewayHost) assert.False(t, ok, "Test Requires %s to not be set", envGatewayHost) host := "dev1.edge-preprod.dev" tests := map[string]struct { uri string }{ "Simple": { uri: https + host + apiV2, }, "Trailing Slash": { uri: https + host + "/api/v2/", }, "Just host": { uri: https + host, }, "Different path": { uri: https + host + "/different/path", }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() es := EmulatorService{ config: &Config{ Profile: Profile{ API: tc.uri, }, }, } // Test err := es.setGatewayURLs(ctx) assert.NoError(t, err) expectedValue := gatewayURLs{ send: &url.URL{Host: host, Path: "/api/ea/sendCommand", Scheme: "https"}, start: &url.URL{Host: host, Path: "/api/ea/startSession", Scheme: "https"}, end: &url.URL{Host: host, Path: "/api/ea/endSession", Scheme: "https"}, } assert.EqualValues(t, &expectedValue, es.gatewayURLs) }) } } // checks the gatewayURLs struct is set correctly func TestSetGatewayURLsEnvVar(t *testing.T) { tests := map[string]struct { envVar string expVal gatewayURLs }{ "Simple": { envVar: "https://testhostURL", expVal: gatewayURLs{ start: &url.URL{Host: "testhostURL", Path: "/startSession", Scheme: "https"}, end: &url.URL{Host: "testhostURL", Path: "/endSession", Scheme: "https"}, send: &url.URL{Host: "testhostURL", Path: "/sendCommand", Scheme: "https"}, }, }, "Simple with port": { envVar: "https://testhostURL:8080", expVal: gatewayURLs{ start: &url.URL{Host: "testhostURL:8080", Path: "/startSession", Scheme: "https"}, end: &url.URL{Host: "testhostURL:8080", Path: "/endSession", Scheme: "https"}, send: &url.URL{Host: "testhostURL:8080", Path: "/sendCommand", Scheme: "https"}, }, }, "With Path": { envVar: "https://testhostURL/abcd/", // Note the trailing slash is required for correct path setting expVal: gatewayURLs{ start: &url.URL{Host: "testhostURL", Path: "/abcd/startSession", Scheme: "https"}, end: &url.URL{Host: "testhostURL", Path: "/abcd/endSession", Scheme: "https"}, send: &url.URL{Host: "testhostURL", Path: "/abcd/sendCommand", Scheme: "https"}, }, }, "With Port and Path": { envVar: "https://testhostURL:8080/abcd/", expVal: gatewayURLs{ start: &url.URL{Host: "testhostURL:8080", Path: "/abcd/startSession", Scheme: "https"}, end: &url.URL{Host: "testhostURL:8080", Path: "/abcd/endSession", Scheme: "https"}, send: &url.URL{Host: "testhostURL:8080", Path: "/abcd/sendCommand", Scheme: "https"}, }, }, // The following tests are just used to document the current behaviour of // different forms of the RCLI_GATEWAY_HOST env var. There are no requirements // that the behaviour remains the same "Missing scheme": { envVar: "testhostURL:8080", // Obviously these are invalid, but having the test makes it explicit // that this form of the env var is not supported. expVal: gatewayURLs{ start: &url.URL{Host: "", Path: "/startSession", Scheme: "testhosturl"}, end: &url.URL{Host: "", Path: "/endSession", Scheme: "testhosturl"}, send: &url.URL{Host: "", Path: "/sendCommand", Scheme: "testhosturl"}, }, }, "Just host": { envVar: "testhostURL", // This uses the value from the configured API endpoint expVal: gatewayURLs{ send: &url.URL{Host: "dev1.edge-preprod.dev", Path: "/api/ea/sendCommand", Scheme: "https"}, start: &url.URL{Host: "dev1.edge-preprod.dev", Path: "/api/ea/startSession", Scheme: "https"}, end: &url.URL{Host: "dev1.edge-preprod.dev", Path: "/api/ea/endSession", Scheme: "https"}, }, }, "Missing Trailing slash": { // Test documents a trailing slash is required for correctly setting path segment envVar: "https://testhostURL/abcd", expVal: gatewayURLs{ start: &url.URL{Host: "testhostURL", Path: "/startSession", Scheme: "https"}, end: &url.URL{Host: "testhostURL", Path: "/endSession", Scheme: "https"}, send: &url.URL{Host: "testhostURL", Path: "/sendCommand", Scheme: "https"}, }, }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() t.Setenv(envGatewayHost, tc.envVar) es := EmulatorService{config: &Config{Profile: Profile{API: "https://dev1.edge-preprod.dev/api/v2/"}}} err := es.setGatewayURLs(ctx) assert.NoError(t, err) assert.EqualValues(t, &tc.expVal, es.gatewayURLs) }) } } // checks any incoming data on the request gets posted to the display channel func TestPostToDisplayChan(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() server := postToDisplayChannelServer(t) defer server.Close() t.Setenv("RCLI_GATEWAY_HOST", server.URL) es, err := New(ctx, Config{}) assert.NoError(t, err) es.idToken = &oauth2.Token{ AccessToken: defaultAccessToken, } assert.NoError(t, err) // need to run Connect to initialise the display channel and post to the request buffer err = es.Connect(ctx, "projectID", "bannerID", "storeID", "terminalID") assert.NoError(t, err) msg := <-es.dispChan // create the expected output for the test expectedMsg, err := msgdata.NewCommandResponse(defaultResponseDataJSON, defaultAttrMap) assert.NoError(t, err) assert.EqualValues(t, expectedMsg, msg) } func TestGetSessionContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() server := startSessionServer(t) defer server.Close() t.Setenv("RCLI_GATEWAY_HOST", server.URL) es, err := New(ctx, Config{}) assert.NoError(t, err) es.idToken = &oauth2.Token{ AccessToken: defaultAccessToken, } assert.NoError(t, err) err = es.Connect(ctx, "", "bannerID", "storeID", "terminalID") assert.NoError(t, err) expectedSessionID := es.session.ID assert.Equal(t, expectedSessionID, es.GetSessionContext().Value(sessionID)) } func assertDarkmode(val bool) payloadAssertions { if val { return func(t *testing.T, sp types.SendPayload) { assert.True(t, sp.AuthDetails.DarkMode) } } return func(t *testing.T, sp types.SendPayload) { assert.False(t, sp.AuthDetails.DarkMode) } } type payloadAssertions func(*testing.T, types.SendPayload) // Tests the sendCommand query. Returns resolved target UUID's in StartSession's // return headers. Assert the payload target is the same as defaultSessionResolvedTarget, // command is test and sessionID is the same as defaultSessionID. Any additional asserts on the SendPayload can // be passed as options via the payloadAssertions type if required. func sendCommandServer(t *testing.T, asserts ...payloadAssertions) *httptest.Server { mux := http.NewServeMux() mux.HandleFunc("/startSession", func(w http.ResponseWriter, _ *http.Request) { w.Header().Add("X-EA-ProjectID", defaultSessionResolvedTarget.Projectid) w.Header().Add("X-EA-BannerID", defaultSessionResolvedTarget.Bannerid) w.Header().Add("X-EA-StoreID", defaultSessionResolvedTarget.Storeid) w.Header().Add("X-EA-TerminalID", defaultSessionResolvedTarget.Terminalid) }) mux.HandleFunc("/sendCommand", func(w http.ResponseWriter, r *http.Request) { data, err := io.ReadAll(r.Body) assert.NoError(t, err) var payload types.SendPayload err = json.Unmarshal(data, &payload) assert.NoError(t, err) assert.EqualValues(t, defaultSessionResolvedTarget, payload.Target) assert.Equal(t, "test", payload.Command) assert.Equal(t, payload.SessionID, testSessionID) for _, assert := range asserts { assert(t, payload) } w.Header().Add(`X-Correlation-ID`, "abcd") }) server := httptest.NewServer(mux) return server } // adds a connectionpayload with the defaultResponseDataJSON and defaultAttrMap to a response buffer one second after connection func postToDisplayChannelServer(t *testing.T) *httptest.Server { // channel := make(chan string) mux := http.NewServeMux() mux.HandleFunc("/startSession", func(w http.ResponseWriter, _ *http.Request) { w.(http.Flusher).Flush() // wait for 1 second so the post to display channel is up and running. time.Sleep(1 * time.Second) msg, err := msgdata.NewCommandResponse(defaultResponseDataJSON, defaultAttrMap) assert.NoError(t, err) resp := types.ConnectionPayload{Message: msg} bytes, err := json.Marshal(resp) assert.NoError(t, err) _, err = w.Write(bytes) assert.NoError(t, err) }) server := httptest.NewServer(mux) return server } // tests the payload is correctly formed and compares the sessionid in the payload to testSessionID func endSessionServer(t *testing.T) *httptest.Server { mux := http.NewServeMux() mux.HandleFunc("/startSession", func(_ http.ResponseWriter, _ *http.Request) { }) mux.HandleFunc("/sendCommand", func(_ http.ResponseWriter, _ *http.Request) { }) mux.HandleFunc("/endSession", func(_ http.ResponseWriter, r *http.Request) { data, err := io.ReadAll(r.Body) assert.NoError(t, err) var payload types.EndSessionPayload err = json.Unmarshal(data, &payload) assert.NoError(t, err) assert.Equal(t, testSessionID, payload.SessionID) }) server := httptest.NewServer(mux) return server } const ( validQueryF = "{\"query\":\"mutation($organization:String!$password:String!$username:String!){login(username: $username, password: $password, organization: $organization){token,banners{bannerEdgeId}}}\",\"variables\":{\"organization\":\"%s\",\"password\":\"%s\",\"username\":\"%s\"}}\n" validResponseF = `{"login": {"token": "%s"}}` validToken = "ewogICJhbGciOiAiSFM1MTIiLAogICJ0eXAiOiAiSldUIgp9.ewogICJhdXRoUHJvdmlkZXIiOiAiYnNsIiwKICAiZW1haWwiOiAiYWNjb3VudCIsCiAgIm9yZ2FuaXphdGlvbiI6ICJvcmdhbml6YXRpb24iLAogICJyZWZyZXNoVG9rZW4iOiAiIiwKICAicm9sZXMiOiBbCiAgICAiUk9MRSIKICBdLAogICJ0b2tlbiI6ICJld29nSUNKMGVYQWlPaUFpU2xkVUlpd0tJQ0FpWVd4bklqb2dJa1ZUTWpVMklncDkuZXdvZ0lDSnRkR2dpT2lCYkNpQWdJQ0FpY0dGemMzZHZjbVFpQ2lBZ1hTd0tJQ0FpYzNWaUlqb2dJbUZqWTI5MWJuUWlMQW9nSUNKdVltWWlPaUF4TnpFeE1UQXlOamcyTEFvZ0lDSnZjbWNpT2lBaWIzSm5ZVzVwZW1GMGFXOXVJaXdLSUNBaWFYTnpJam9nSW1semMzVmxjaUlzQ2lBZ0luSnNjeUk2SUNKbFNuaE9hVGhyVG1kRVFVMUNUamt5VG1KU1oydFdWM2RyU25wSmJVeFFMMUZxUVdZMFJGZGhNV1Y2UVdoek5tdENVbGxoU25oT2RHbG9NMDlHUnpKSGNHVk5XVW8zU0RaVVRtNUJkU3QxUTFnd1psWjRVMHcxVEhNeWNtMUdORk15ZFhkTVJqUXlPR3BSTlRWdFEyaE5ZWGs1Y0U0M2JWbElaMEU5SWl3S0lDQWlaWGh3SWpvZ01UY3hNVEV3TXpVNE5pd0tJQ0FpYVdGMElqb2dNVGN4TVRFd01qWTROaXdLSUNBaWFuUnBJam9nSWpNeE1XRXdaamcwTFRsa05USXROREU0TUMxaVpHUXdMV1psWlRoak5UaGpZemRsTUNJS2ZRLlUybG5ibUYwZFhKbE9pQmhkRE5PZWtNM2RXbDNTelE0V1VVNWJTMVBRMUV6VFZGcFRVMVVabDlxUkY4d2EzUmFWaTAxV0ZKSWNHaGpPWFJTVURSSlJGVk1WVWhDUlhSMmVVMDRPRTQwV1RadE1rRTNXVXAzYlY5MVpYQTFWM0JEWnciLAogICJ1c2VybmFtZSI6ICJ1c2VyIgp9.signature" //nolint:gosec // it's not that interesting invalidToken = "invalid" ) type graphQLErr struct { Message string Locations []struct { Line int Column int } } func startSessionReadCookieServer(t *testing.T, expected *http.Cookie) *httptest.Server { mux := http.NewServeMux() mux.HandleFunc("/startSession", func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("edge-session") assert.NoError(t, err) if reflect.DeepEqual(cookie, expected) { w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusForbidden) } }) server := httptest.NewServer(mux) return server } func edgeAPIMockServer(t *testing.T, expQuery string, token string) *httptest.Server { type output struct { Data *json.RawMessage `json:"data"` Errors []graphQLErr `json:"errors,omitempty"` } generateErr := func(key string) graphQLErr { message := fmt.Sprintf("Field \"login\" argument \"%s\" of type \"String!\" is required but not provided.", key) return graphQLErr{ Message: message, Locations: []struct { Line int Column int }{ {Line: 2, Column: 3}, }, } } mux := http.NewServeMux() mux.HandleFunc(apiV2, func(w http.ResponseWriter, r *http.Request) { var err error // Read body and ensure it's expected data, err := io.ReadAll(r.Body) assert.NoError(t, err) assert.Equal(t, expQuery, string(data)) // Parse variables map variables := bytes.SplitAfter(bytes.Split(data, []byte(`"variables":`))[1], []byte("}"))[0] var m map[string]string err = json.Unmarshal(variables, &m) assert.NoError(t, err) // Check for errors var errs []graphQLErr if m["username"] == "" { errs = append(errs, generateErr("username")) } if m["password"] == "" { errs = append(errs, generateErr("password")) } if m["organization"] == "" { errs = append(errs, generateErr("organization")) } // Prepare output var outputData *json.RawMessage if len(errs) == 0 { d := json.RawMessage(fmt.Sprintf(validResponseF, token)) outputData = &d } out := output{ Data: outputData, Errors: errs, } res, err := json.Marshal(out) assert.NoError(t, err) // Write response http.SetCookie(w, &http.Cookie{ Name: "edge-session", Value: "MTcxMjIyMzUzOHxOd3dBTkVkWFRVOVhOelpDVEV4UVdqTmFXRlpOTkZoRU4xa3pTMUJLVkVJelQwMUJNa2hTUzA1UVRWaFJSbFJPVVZGR1VWRTNRa0U9fIUdYK4-Qw-mlW3KIOInztkT-v8RpJyoTiMNVUsiA9Qx", Path: "/", // Set to "/" for local test }) w.Header().Add("content-type", "application/json") w.WriteHeader(http.StatusOK) _, err = w.Write(res) assert.NoError(t, err) }) server := httptest.NewServer(mux) return server } func TestRetrieveIdentityJWT(t *testing.T) { server := edgeAPIMockServer(t, fmt.Sprintf(validQueryF, "c", "b", "a"), validToken) defer server.Close() es, err := New(context.Background(), Config{ Profile: Profile{ Username: "a", Password: "b", Organization: "c", API: server.URL + apiV2, }, }) assert.NoError(t, err) err = es.RetrieveIdentity(context.Background()) assert.NoError(t, err) assert.Equal(t, validToken, es.idToken.AccessToken) assert.NotEmpty(t, es.userID) assert.Equal(t, "Bearer", es.idToken.TokenType) assert.NotNil(t, es.idToken.Expiry) } func TestProfileCookie(t *testing.T) { // This test is similar to TestRetrieveIdentityCookie, however specifically // applies for remotecli exec invocation rather than interactive remotecli // setup ctx, cancel := context.WithCancel(context.Background()) defer cancel() expectedCookie := &http.Cookie{ Name: "edge-session", Value: "MTcxMjIyMzUzOHxOd3dBTkVkWFRVOVhOelpDVEV4UVdqTmFXRlpOTkZoRU4xa3pTMUJLVkVJelQwMUJNa2hTUzA1UVRWaFJSbFJPVVZGR1VWRTNRa0U9fIUdYK4-Qw-mlW3KIOInztkT-v8RpJyoTiMNVUsiA9Qx", Path: "", // When path is set to "/", it is returned to Jar as an empty field } startSeshServer := startSessionReadCookieServer(t, expectedCookie) defer startSeshServer.Close() t.Setenv("RCLI_GATEWAY_HOST", startSeshServer.URL) api, err := url.Parse(startSeshServer.URL) assert.NoError(t, err) es, err := New(context.Background(), Config{ Profile: Profile{ Username: "a", Password: "b", Organization: "c", API: api.String(), SessionCookie: expectedCookie.String(), }, }) assert.NoError(t, err) // test // Ensure cookie set in Profile Config was stored in Jar u, err := url.Parse(startSeshServer.URL) assert.NoError(t, err) cookie := es.client.Jar.Cookies(u)[0] assert.Equal(t, expectedCookie, cookie) // Ensure cookie is passed to different request err = es.Connect(ctx, "", "bannerID", "storeID", "terminalID") assert.NoError(t, err) } func TestRetrieveIdentityCookie(t *testing.T) { // setup ctx, cancel := context.WithCancel(context.Background()) defer cancel() expectedCookie := &http.Cookie{ Name: "edge-session", Value: "MTcxMjIyMzUzOHxOd3dBTkVkWFRVOVhOelpDVEV4UVdqTmFXRlpOTkZoRU4xa3pTMUJLVkVJelQwMUJNa2hTUzA1UVRWaFJSbFJPVVZGR1VWRTNRa0U9fIUdYK4-Qw-mlW3KIOInztkT-v8RpJyoTiMNVUsiA9Qx", Path: "", // When path is set to "/", it is returned to Jar as an empty field } startSeshServer := startSessionReadCookieServer(t, expectedCookie) defer startSeshServer.Close() apiServer := edgeAPIMockServer(t, fmt.Sprintf(validQueryF, "c", "b", "a"), validToken) defer apiServer.Close() t.Setenv("RCLI_GATEWAY_HOST", startSeshServer.URL) api, err := url.Parse(apiServer.URL + "/api/v2") assert.NoError(t, err) es, err := New(context.Background(), Config{ Profile: Profile{ Username: "a", Password: "b", Organization: "c", API: api.String(), }, }) assert.NoError(t, err) // test err = es.RetrieveIdentity(context.Background()) assert.NoError(t, err) // Ensure cookie set in /api/v2 was stored in Jar u, err := url.Parse(apiServer.URL) assert.NoError(t, err) cookie := es.client.Jar.Cookies(u)[0] assert.Equal(t, expectedCookie, cookie) // Ensure cookie is saved to the profile assert.Equal(t, expectedCookie.String(), es.config.Profile.SessionCookie) // Ensure cookie is passed to different request err = es.Connect(ctx, "", "bannerID", "storeID", "terminalID") assert.NoError(t, err) // Ensure cookie is returned by env for remotecli exec expEnv := []string{ fmt.Sprintf("RCLI_API_ENDPOINT=%s/api/v2", apiServer.URL), "RCLI_COOKIE=edge-session=MTcxMjIyMzUzOHxOd3dBTkVkWFRVOVhOelpDVEV4UVdqTmFXRlpOTkZoRU4xa3pTMUJLVkVJelQwMUJNa2hTUzA1UVRWaFJSbFJPVVZGR1VWRTNRa0U9fIUdYK4-Qw-mlW3KIOInztkT-v8RpJyoTiMNVUsiA9Qx", } assert.Equal(t, expEnv, es.Env()) } func TestRetrieveIdentityInvalid(t *testing.T) { cases := map[string]struct { config Config expQuery string errAssert assert.ErrorAssertionFunc }{ "Missing Username Field": { config: Config{ Profile: Profile{ Password: "b", Organization: "c", }, }, expQuery: fmt.Sprintf(validQueryF, "c", "b", ""), errAssert: EqualError("error calling Edge API: Field \"login\" argument \"username\" of type \"String!\" is required but not provided."), }, "Missing Password Field": { config: Config{ Profile: Profile{ Username: "a", Organization: "c", }, }, expQuery: fmt.Sprintf(validQueryF, "c", "", "a"), errAssert: EqualError("error calling Edge API: Field \"login\" argument \"password\" of type \"String!\" is required but not provided."), }, "Missing Organization Field": { config: Config{ Profile: Profile{ Username: "a", Password: "b", }, }, expQuery: fmt.Sprintf(validQueryF, "", "b", "a"), errAssert: EqualError("error calling Edge API: Field \"login\" argument \"organization\" of type \"String!\" is required but not provided."), }, "Missing All Fields": { config: Config{}, expQuery: fmt.Sprintf(validQueryF, "", "", ""), errAssert: EqualError("error calling Edge API: Field \"login\" argument \"username\" of type \"String!\" is required but not provided."), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { server := edgeAPIMockServer(t, tc.expQuery, invalidToken) defer server.Close() tc.config.Profile.API = server.URL + apiV2 es, err := New(context.Background(), tc.config) assert.NoError(t, err) err = es.RetrieveIdentity(context.Background()) tc.errAssert(t, err) assert.Nil(t, es.idToken) }) } } func checkVersionServer(t *testing.T, expectedVer string) *httptest.Server { mux := http.NewServeMux() mux.HandleFunc("/", func(_ http.ResponseWriter, r *http.Request) { ver := r.Header.Get(eaconst.APIVersionKey) assert.Equal(t, expectedVer, ver) }) server := httptest.NewServer(mux) return server } func TestVersionHeader(t *testing.T) { ver := eaconst.APIVersion es, err := New(context.Background(), Config{}) assert.NoError(t, err) server := checkVersionServer(t, ver) defer server.Close() req, err := http.NewRequest(http.MethodGet, server.URL, nil) assert.NoError(t, err) _, err = es.client.Do(req) assert.NoError(t, err) }