id | testField1 | created | updated | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
No records found. |
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3ec83c..de0f363 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@v3 - name: install deps run: | - apt update && apt install jq -y + apt-get update && apt-get install jq chromium -y make REVISION=$GITHUB_SHA install - name: make test run: | diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3518d14..7e59c18 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -39,7 +39,7 @@ jobs: - uses: actions/checkout@v3 - name: install deps run: | - apt update && apt install jq -y + apt-get update && apt-get install jq chromium -y make REVISION=$GITHUB_SHA install - name: make test run: | diff --git a/Makefile b/Makefile index 5d187d3..4ef32ee 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ btr: build tag release # TEST test: test_prereq - go test `go list ./... | grep -v */generated/` -v -mod=readonly -race -coverprofile=.coverage/out | go-junit-report > .coverage/report-junit.xml && \ + go test ./... -v -mod=readonly -race -coverprofile=.coverage/out | go-junit-report > .coverage/report-junit.xml && \ gocov convert .coverage/out | gocov-xml > .coverage/report-cobertura.xml test_ci: diff --git a/README.md b/README.md index dc6d0b0..a4622f3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=dnitsch_uistrategy&metric=bugs)](https://sonarcloud.io/summary/new_code?id=dnitsch_uistrategy) +[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=dnitsch_uistrategy&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=dnitsch_uistrategy) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=dnitsch_uistrategy&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=dnitsch_uistrategy) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=dnitsch_uistrategy&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=dnitsch_uistrategy) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=dnitsch_uistrategy&metric=coverage)](https://sonarcloud.io/summary/new_code?id=dnitsch_uistrategy) + # UI Strategy - Beta [![Go Report Card](https://goreportcard.com/badge/github.com/dnitsch/uistrategy)](https://goreportcard.com/report/github.com/dnitsch/uistrategy) diff --git a/auth.go b/auth.go index 9ce8dad..33169ab 100644 --- a/auth.go +++ b/auth.go @@ -1,6 +1,10 @@ package uistrategy -import "github.com/go-rod/rod" +import ( + "fmt" + + "github.com/go-rod/rod" +) type Auth struct { Username Element `yaml:"username" json:"username"` @@ -50,6 +54,9 @@ func (web *Web) doLocalAuth(auth Auth) (*LoggedInPage, error) { // SP initiated will be simpler as you can omit the idpUrl // the flow will follow redirects func (web *Web) doIdpAuth(auth Auth) (*LoggedInPage, error) { + if auth.IdpSelector == nil { + return nil, fmt.Errorf("idpSelector must be specified") + } page := web.browser.MustPage(web.config.BaseUrl + auth.Navigate).MustWaitLoad() lp := &LoggedInPage{web, page, UIStrategyError{}} @@ -58,7 +65,7 @@ func (web *Web) doIdpAuth(auth Auth) (*LoggedInPage, error) { idpSelect, err := determinActionElement(lp.log, page, *auth.IdpSelector) if err != nil { - web.log.Errorf("unable to find IdpSelector field, by selector: %v", *auth.IdpSelector.Selector) + web.log.Errorf("unable to find IdpSelector field, by selector: %v, error: %v", auth.IdpSelector.Selector, err.Error()) return nil, err } idpSelect.MustClick() diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 0000000..6cc0db1 --- /dev/null +++ b/auth_test.go @@ -0,0 +1,164 @@ +package uistrategy_test + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + log "github.com/dnitsch/simplelog" + "github.com/dnitsch/uistrategy" + "github.com/dnitsch/uistrategy/internal/util" +) + +func TestDoLoginBasic(t *testing.T) { + t.Parallel() + ttests := map[string]struct { + baseConf func(t *testing.T, url string) uistrategy.BaseConfig + auth func(t *testing.T, url string) *uistrategy.Auth + handler func(t *testing.T) http.Handler + // auth func(t *testing.T, url string) *uistrategy.Auth + }{ + "local login": { + func(t *testing.T, url string) uistrategy.BaseConfig { + return uistrategy.BaseConfig{ + BaseUrl: url, + LauncherConfig: &uistrategy.WebConfig{ + Headless: true, + }, + } + }, + func(t *testing.T, url string) *uistrategy.Auth { + return &uistrategy.Auth{ + Username: uistrategy.Element{ + Selector: util.Str("#username"), + Value: util.Str("test"), + }, + Password: uistrategy.Element{ + Selector: util.Str("#password"), + Value: util.Str("test"), + }, + Navigate: "/login", + Submit: uistrategy.Element{ + Selector: util.Str("#submit"), + }, + } + }, + func(t *testing.T) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(localLoginHtml) + }) + mux.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(`
Hello!`)) + }) + return mux + }, + }, + "idp auth": { + func(t *testing.T, url string) uistrategy.BaseConfig { + return uistrategy.BaseConfig{ + BaseUrl: url, + LauncherConfig: &uistrategy.WebConfig{ + Headless: true, + }, + } + }, + func(t *testing.T, url string) *uistrategy.Auth { + return &uistrategy.Auth{ + Username: uistrategy.Element{ + Selector: util.Str("#username"), + Value: util.Str("test"), + }, + Password: uistrategy.Element{ + Selector: util.Str("#password"), + Value: util.Str("test"), + }, + Navigate: "/login-idp", + IdpManaged: true, + IdpSelector: &uistrategy.Element{ + Selector: util.Str("#idp-login"), + }, + IdpUrl: url, + Submit: uistrategy.Element{ + Selector: util.Str("#submit"), + }, + } + }, + func(t *testing.T) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/login-idp", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(idpLoginHtml) + }) + mux.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(`Hello!`)) + }) + return mux + }, + }, + "mfa Login": { + func(t *testing.T, url string) uistrategy.BaseConfig { + return uistrategy.BaseConfig{ + BaseUrl: url, + LauncherConfig: &uistrategy.WebConfig{ + Headless: true, + }, + } + }, + func(t *testing.T, url string) *uistrategy.Auth { + return &uistrategy.Auth{ + Username: uistrategy.Element{ + Selector: util.Str("#username"), + Value: util.Str("test"), + }, + Password: uistrategy.Element{ + Selector: util.Str("#password"), + Value: util.Str("test"), + }, + Navigate: "/login-idp", + IdpManaged: true, + MfaSelector: &uistrategy.Element{ + Selector: util.Str("#mfa"), + }, + IdpSelector: &uistrategy.Element{ + Selector: util.Str("#idp-login"), + }, + IdpUrl: url, + Submit: uistrategy.Element{ + Selector: util.Str("#submit"), + }, + } + }, + func(t *testing.T) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/login-idp", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(mfaLogin) + }) + mux.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(`Hello!`)) + }) + return mux + }, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler(t)) + defer ts.Close() + uiWeb := uistrategy.New(tt.baseConf(t, ts.URL)).WithLogger(log.New(&bytes.Buffer{}, log.ErrorLvl)) + lp, err := uiWeb.DoAuth(tt.auth(t, ts.URL)) + if err != nil { + t.Errorf("failed to do auth: %v", err) + } + if lp == nil { + t.Errorf("logged in page - got nil expected not nil") + } + }) + } +} diff --git a/cmd/uistrategy/uistrategy.go b/cmd/uistrategy/uistrategy.go index 6b62411..78a1137 100644 --- a/cmd/uistrategy/uistrategy.go +++ b/cmd/uistrategy/uistrategy.go @@ -18,7 +18,7 @@ var ( Use: "uistrategy", RunE: runActions, Short: "executes a series of actions against a setup config", - Long: ``, + Long: `executes a series of instructions against a any number of paths under the same host. supports multiple login options - basic/Idp/MFA e.g. `, } ) diff --git a/cmd/uistrategy/uistrategy_test.go b/cmd/uistrategy/uistrategy_test.go index fff3829..ad5c011 100644 --- a/cmd/uistrategy/uistrategy_test.go +++ b/cmd/uistrategy/uistrategy_test.go @@ -1,32 +1,146 @@ package cmd import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" "testing" - "github.com/spf13/cobra" + "github.com/dnitsch/uistrategy" + "github.com/dnitsch/uistrategy/internal/util" + "gopkg.in/yaml.v2" ) -func Test_runActions_integration(t *testing.T) { - tests := []struct { - name string - path string +func helperTestSeed(conf *uistrategy.UiStrategyConf) string { + b, _ := yaml.Marshal(conf) + dir, _ := os.MkdirTemp("", "uiseeder-test") + file := filepath.Join(dir, "uiseeder.yml") + _ = os.WriteFile(file, b, 0777) + return file +} + +func Test_runActions(t *testing.T) { + tests := map[string]struct { + path func(t *testing.T, baseUrl string) string + handler func(t *testing.T) http.Handler + additionalArgs []string }{ - { - name: "integration without configmanager", - path: "../../test/integration.yml", + "simple test with 1 action": { + func(t *testing.T, baseUrl string) string { + conf := &uistrategy.UiStrategyConf{ + Setup: uistrategy.BaseConfig{ + BaseUrl: baseUrl, + LauncherConfig: &uistrategy.WebConfig{ + Headless: true, + }, + }, + Actions: []*uistrategy.ViewAction{ + { + Name: "test get", + Navigate: "/get/index.html", + ElementActions: []*uistrategy.ElementAction{ + { + Name: "get input id", + // SkipOnErrorMessage: , + CaptureOutput: false, + Element: uistrategy.Element{ + Selector: util.Str("//*/input"), + }, + }, + }, + }, + }, + } + return helperTestSeed(conf) + }, + func(t *testing.T) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/get/index.html", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(``)) + }) + return mux + }, + []string{}, + }, + "verbose with 1 action": { + func(t *testing.T, baseUrl string) string { + conf := &uistrategy.UiStrategyConf{ + Setup: uistrategy.BaseConfig{ + BaseUrl: baseUrl, + LauncherConfig: &uistrategy.WebConfig{ + Headless: true, + }, + }, + Actions: []*uistrategy.ViewAction{ + { + Name: "test get", + Navigate: "/get/index.html", + ElementActions: []*uistrategy.ElementAction{ + { + Name: "get input id", + // SkipOnErrorMessage: , + CaptureOutput: false, + Element: uistrategy.Element{ + Selector: func(input string) *string { return &input }("//*/input"), + }, + }, + }, + }, + }, + } + return helperTestSeed(conf) + }, + func(t *testing.T) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/get/index.html", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(``)) + }) + return mux + }, + []string{"-v"}, }, - // { - // name: "integration with configmanager", - // path: "../../test/integration-with-configmanager.yml", - // }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - path = tt.path - verbose = true - if e := runActions(&cobra.Command{}, []string{}); e != nil { - t.Errorf("%v", e) + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler(t)) + defer ts.Close() + + path = tt.path(t, ts.URL) + stdout, errout := &bytes.Buffer{}, &bytes.Buffer{} + cmd := rootCmd + args := []string{"-i", path} + args = append(args, tt.additionalArgs...) + cmd.SetArgs(args) + cmd.SetErr(errout) + cmd.SetOut(stdout) + if _, err := cmd.ExecuteC(); err != nil { + t.Errorf("uiseeder cmd failed: %v", err) } }) } } + +func TestVersion(t *testing.T) { + b := new(bytes.Buffer) + cmd := rootCmd + cmd.SetArgs([]string{"version"}) + cmd.SetErr(b) + cmd.SetOut(b) + _, err := cmd.ExecuteC() + if err != nil { + t.Errorf("...") + } + out, _ := io.ReadAll(b) + if !strings.Contains(string(out), "Version:") { + t.Errorf("version not shown correctly") + } + if !strings.Contains(string(out), "Revision:") { + t.Errorf("revision not shown correctly") + } +} diff --git a/cmd/uistrategy/version.go b/cmd/uistrategy/version.go index bfe4e3a..a70a56e 100644 --- a/cmd/uistrategy/version.go +++ b/cmd/uistrategy/version.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "os" "github.com/dnitsch/uistrategy/internal/config" "github.com/spf13/cobra" @@ -16,8 +15,8 @@ var ( Short: fmt.Sprintf("Get version number %s", config.SELF_NAME), Long: `Version and Revision number of the installed CLI`, Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Version: %s\nRevision: %s\n", Version, Revision) - os.Exit(0) + fmt.Fprint(cmd.OutOrStdout(), fmt.Sprintf("Version: %s\nRevision: %s\n", Version, Revision)) + // os.Exit(0) }, } ) diff --git a/go.mod b/go.mod index 963adcd..888ed04 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( golang.org/x/net v0.5.0 // indirect golang.org/x/sys v0.4.0 // indirect golang.org/x/text v0.6.0 // indirect + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 061348c..142107a 100644 --- a/go.sum +++ b/go.sum @@ -113,5 +113,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmdutil/cmdutil.go b/internal/cmdutil/cmdutil.go index afeba24..d23f67a 100644 --- a/internal/cmdutil/cmdutil.go +++ b/internal/cmdutil/cmdutil.go @@ -2,7 +2,6 @@ package cmdutil import ( "context" - "fmt" "io" "github.com/dnitsch/configmanager" @@ -15,10 +14,7 @@ type ConfManager interface { } func RunActions(ui *uistrategy.Web, conf *uistrategy.UiStrategyConf) error { - if err := ui.Drive(context.Background(), conf.Auth, conf.Actions); len(err.Error()) > 0 { - return fmt.Errorf(err.Error()) - } - return nil + return ui.Drive(context.Background(), conf.Auth, conf.Actions) } // YamlParseInput will return a filled pointer with Unmarshalled data diff --git a/test/idp.html b/test/idp.html new file mode 100644 index 0000000..a5a7300 --- /dev/null +++ b/test/idp.html @@ -0,0 +1,46 @@ + + + + + + id | testField1 | created | updated | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
No records found. |
id | testField1 | created | updated | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
No records found. |
id | testField1 | created | updated | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
No records found. |