From 6f267c28d5fc7e966acfad4795896f08c6b8a898 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Fri, 6 Nov 2020 15:01:49 +0530 Subject: [PATCH 01/20] Update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 839a56d..81a8163 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Dependency directories (remove the comment below to include it) vendor/ + +.env \ No newline at end of file From 50ebf9b076ca5b7d87df9c396c7a1787ca446f22 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Fri, 13 Nov 2020 01:13:19 +0530 Subject: [PATCH 02/20] Add Parse function. --- atcoder/atcoder.go | 76 ++++++++++++++++++++++++++++++++++ atcoder/atcoder_test.go | 92 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 atcoder/atcoder.go create mode 100644 atcoder/atcoder_test.go diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go new file mode 100644 index 0000000..76633ee --- /dev/null +++ b/atcoder/atcoder.go @@ -0,0 +1,76 @@ +package atcoder + +import ( + "fmt" + "regexp" + "strings" +) + +type ( + // Args holds specifier details parsed by + // Parse() function. All methods use this + // at the core. + Args struct { + Contest string + Problem string + } +) + +// Errors returned by library. +var ( + ErrInvalidSpecifier = fmt.Errorf("invalid specifier data") + errInvalidCredentials = fmt.Errorf("invalid login credentials") +) + +var ( + hostURL = "https://atcoder.jp" +) + +// loginPage returns link to login page +func loginPage() string { + return fmt.Sprintf("%v/login", hostURL) +} + +// Parse passed in specifier string to new Args struct. +// Validates parsed args and returns error if any. +func Parse(str string) (Args, error) { + var ( + rxCont = `(?P[A-Za-z0-9-]+)` + rxProb = `(?P[A-Za-z0-9_]+)` + + valRx = []string{ + `atcoder.jp\/contests\/` + rxCont + `$`, + `atcoder.jp\/contests\/` + rxCont + `\/tasks\/` + rxProb + `$`, + + `^` + rxCont + `$`, + `^` + rxCont + `\s+` + rxProb + `$`, + } + ) + + str = strings.TrimSpace(str) + if str == "" { + return Args{}, nil + } + + for _, rgx := range valRx { + re := regexp.MustCompile(rgx) + if re.MatchString(str) { + // attrib : stackoverflow.com/a/9606036 + match := re.FindStringSubmatch(str) + result := map[string]string{} + for i, name := range re.SubexpNames() { + if i != 0 && name != "" { + result[name] = match[i] + } + } + // convert to lowercase (default config) + result["prob"] = strings.ToLower(result["prob"]) + arg := Args{ + Contest: result["cont"], + Problem: result["prob"], + } + return arg, nil + } + } + return Args{}, ErrInvalidSpecifier +} diff --git a/atcoder/atcoder_test.go b/atcoder/atcoder_test.go new file mode 100644 index 0000000..15f9e92 --- /dev/null +++ b/atcoder/atcoder_test.go @@ -0,0 +1,92 @@ +package atcoder + +import ( + "reflect" + "testing" +) + +func Test_loginPage(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "Login Page", + want: "https://atcoder.jp/login", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := loginPage(); got != tt.want { + t.Errorf("loginPage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParse(t *testing.T) { + type args struct { + str string + } + tests := []struct { + name string + args args + want Args + wantErr bool + }{ + { + name: "Test #1", + args: args{"https://atcoder.jp/contests/acl1"}, + want: Args{"acl1", ""}, + wantErr: false, + }, + { + name: "Test #2", + args: args{"https://atcoder.jp/contests/m-solutions2020/tasks/m_solutions2020_a"}, + want: Args{"m-solutions2020", "m_solutions2020_a"}, + wantErr: false, + }, + { + name: "Test #3", + args: args{"arc107"}, + want: Args{"arc107", ""}, + wantErr: false, + }, + { + name: "Test #4", // Problem id need not match contest id. + args: args{"arc9999 aproblem"}, + want: Args{"arc9999", "aproblem"}, + wantErr: false, + }, + { + name: "Test #5", + args: args{"in_valid"}, + want: Args{}, + wantErr: true, + }, + { + name: "Test #6", + args: args{"in-valid in-valid"}, + want: Args{}, + wantErr: true, + }, + { + name: "Test #7", + args: args{""}, + want: Args{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.args.str) + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Parse() = %v, want %v", got, tt.want) + } + }) + } +} From 8516fb1679e7ee2ffaaa80c9f8cdf7a500a166ad Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Fri, 13 Nov 2020 02:03:11 +0530 Subject: [PATCH 03/20] Move Browser launcher to util package. NewBrowser is common to all website start functionalities. --- util/browser.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 util/browser.go diff --git a/util/browser.go b/util/browser.go new file mode 100644 index 0000000..5516543 --- /dev/null +++ b/util/browser.go @@ -0,0 +1,65 @@ +package util + +import ( + "os" + "path/filepath" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/launcher" +) + +// NewBrowser initiates the automated browser to use. +func NewBrowser(headless bool, userDataDir, bin string) (*rod.Browser, error) { + // Launch browser. + launchBrowser := func(controlURL string) (*rod.Browser, error) { + b := rod.New().ControlURL(controlURL) + if err := b.Connect(); err != nil { + return nil, err + } + return b, nil + } + + // Store data in cache (to reduce time). + cacheDir, _ := os.UserCacheDir() + cacheUserDataDir := filepath.Join(cacheDir, "cp-tools", "cpt-lib", bin) + + // Initiate the browser to use. + l := launcher.New(). + UserDataDir(cacheUserDataDir). + Headless(headless). + Bin(bin) + + controlURL, err := l.Launch() + if err != nil { + return nil, err + } + + Browser, err := launchBrowser(controlURL) + if err != nil { + return nil, err + } + + // Load temporary browser to extract cookies only if path exists. + if file, err := os.Stat(userDataDir); err == nil && file.IsDir() { + // Initiate browser to extract cookies from. + cookiesl := launcher.NewUserMode(). + UserDataDir(userDataDir). + Headless(true). + Bin(bin) + + cookiesControlURL, err := cookiesl.Launch() + if err != nil { + return nil, err + } + + cookiesBrowser, err := launchBrowser(cookiesControlURL) + if err != nil { + return nil, err + } + defer cookiesBrowser.Close() + // Copy cookies of user. + Browser.MustSetCookies(cookiesBrowser.MustGetCookies()) + } + + return Browser, nil +} From 01ad8fe603af6922caa99d9f57e837f0c0f4f326 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Fri, 13 Nov 2020 02:05:57 +0530 Subject: [PATCH 04/20] Add Start function. --- atcoder/atcoder.go | 15 +++++++++++++++ atcoder/atcoder_test.go | 17 +++++++++++++++++ go.mod | 6 +++++- go.sum | 12 ++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go index 76633ee..e01eb7b 100644 --- a/atcoder/atcoder.go +++ b/atcoder/atcoder.go @@ -4,6 +4,10 @@ import ( "fmt" "regexp" "strings" + + "github.com/cp-tools/cpt-lib/util" + + "github.com/go-rod/rod" ) type ( @@ -24,8 +28,19 @@ var ( var ( hostURL = "https://atcoder.jp" + + // Browser is the headless browser to use. + Browser *rod.Browser ) +// Start initiates the automated browser to use. +func Start(headless bool, userDataDir, bin string) error { + bs, err := util.NewBrowser(headless, userDataDir, bin) + Browser = bs + + return err +} + // loginPage returns link to login page func loginPage() string { return fmt.Sprintf("%v/login", hostURL) diff --git a/atcoder/atcoder_test.go b/atcoder/atcoder_test.go index 15f9e92..3d1b64d 100644 --- a/atcoder/atcoder_test.go +++ b/atcoder/atcoder_test.go @@ -1,10 +1,27 @@ package atcoder import ( + "os" "reflect" "testing" + + "github.com/joho/godotenv" ) +func TestMain(m *testing.M) { + // Load local .env file. + godotenv.Load() + + _, browserHeadless := os.LookupEnv("BROWSER_HEADLESS") + browserBin := os.Getenv("BROWSER_BINARY") + Start(browserHeadless, "", browserBin) + + exitCode := m.Run() + + Browser.Close() + os.Exit(exitCode) +} + func Test_loginPage(t *testing.T) { tests := []struct { name string diff --git a/go.mod b/go.mod index fbd74d6..58c44fc 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module github.com/cp-tools/cpt-lib go 1.14 -require github.com/PuerkitoBio/goquery v1.5.1 +require ( + github.com/PuerkitoBio/goquery v1.5.1 + github.com/go-rod/rod v0.79.7 + github.com/joho/godotenv v1.3.0 +) diff --git a/go.sum b/go.sum index 6b8db65..640e3b0 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,21 @@ github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154Oa github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/go-rod/rod v0.79.7 h1:IPpHrKxLeze2kMU9+68QXGigfDh8kcDPknDM8BV3MTA= +github.com/go-rod/rod v0.79.7/go.mod h1:vgd1G+mTyCUVks90Uefu1nKcjCB+WCuWBXWgwFJ3Cqc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/ysmood/goob v0.3.0 h1:XZ51cZJ4W3WCoCiUktixzMIQF86W7G5VFL4QQ/Q2uS0= +github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= +github.com/ysmood/got v0.8.3/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= +github.com/ysmood/gotrace v0.1.1/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.6.3 h1:4cU+5oOdsyundXHy00t99H0rLXLthuseD3x6W+xmCiU= +github.com/ysmood/gson v0.6.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.6.7 h1:nJNIwECon6vskFI4fgPPlFHEXZKUqKfdKijiopUdReE= +github.com/ysmood/leakless v0.6.7/go.mod h1:eOWmPkwrQgyJ+JivIgpu5EoJXRN04Wf+wENtR9CUKyE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 1a3d703fce783abeb9efed5ffe58bfff9f9b5167 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Fri, 13 Nov 2020 16:36:48 +0530 Subject: [PATCH 05/20] Downgrade dependencies. --- go.mod | 2 +- go.sum | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 58c44fc..0a17204 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,6 @@ go 1.14 require ( github.com/PuerkitoBio/goquery v1.5.1 - github.com/go-rod/rod v0.79.7 + github.com/go-rod/rod v0.77.1 github.com/joho/godotenv v1.3.0 ) diff --git a/go.sum b/go.sum index 640e3b0..01e828a 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,7 @@ -github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= -github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/go-rod/rod v0.79.7 h1:IPpHrKxLeze2kMU9+68QXGigfDh8kcDPknDM8BV3MTA= -github.com/go-rod/rod v0.79.7/go.mod h1:vgd1G+mTyCUVks90Uefu1nKcjCB+WCuWBXWgwFJ3Cqc= +github.com/go-rod/rod v0.77.1 h1:e6QK8KyUaLRswDUDHxW526nDPaP9EvRaaiSce9qm55M= +github.com/go-rod/rod v0.77.1/go.mod h1:XEc4dRYDxlKw+SFG3ZpWTZ8k4vosgg5IDUHKYPMzVSI= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/ysmood/goob v0.3.0 h1:XZ51cZJ4W3WCoCiUktixzMIQF86W7G5VFL4QQ/Q2uS0= github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= @@ -11,11 +9,10 @@ github.com/ysmood/got v0.8.3/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhM github.com/ysmood/gotrace v0.1.1/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.6.3 h1:4cU+5oOdsyundXHy00t99H0rLXLthuseD3x6W+xmCiU= github.com/ysmood/gson v0.6.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= -github.com/ysmood/leakless v0.6.7 h1:nJNIwECon6vskFI4fgPPlFHEXZKUqKfdKijiopUdReE= -github.com/ysmood/leakless v0.6.7/go.mod h1:eOWmPkwrQgyJ+JivIgpu5EoJXRN04Wf+wENtR9CUKyE= +github.com/ysmood/leakless v0.6.3 h1:blW3g7NYSTkXtRhmV0vq+t4KzWWUJUdd2pB77O0uteQ= +github.com/ysmood/leakless v0.6.3/go.mod h1:eOWmPkwrQgyJ+JivIgpu5EoJXRN04Wf+wENtR9CUKyE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 2bfac5036c06b30c1ec506f9d5e13e294833509f Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Fri, 13 Nov 2020 16:37:22 +0530 Subject: [PATCH 06/20] Add NewPage functionality. --- util/browser.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/util/browser.go b/util/browser.go index 5516543..7bc1dec 100644 --- a/util/browser.go +++ b/util/browser.go @@ -6,6 +6,7 @@ import ( "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/proto" ) // NewBrowser initiates the automated browser to use. @@ -63,3 +64,25 @@ func NewBrowser(headless bool, userDataDir, bin string) (*rod.Browser, error) { return Browser, nil } + +// NewPage loads the given link in a new browser tab. +func NewPage(browser *rod.Browser, link string, block []proto.NetworkResourceType) (*rod.Page, error) { + page, err := browser.Page(proto.TargetCreateTarget{URL: link}) + if err != nil { + return nil, err + } + + router := page.HijackRequests() + router.MustAdd("*", func(h *rod.Hijack) { + for _, b := range block { + if h.Request.Type() == b { + h.Response.Fail(proto.NetworkErrorReasonBlockedByClient) + return + } + } + h.ContinueRequest(&proto.FetchContinueRequest{}) + }) + go router.Run() + + return page, nil +} From befaf2f6247db4145091dc9824a0a9366bb472e8 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Fri, 13 Nov 2020 16:37:56 +0530 Subject: [PATCH 07/20] Add login and logout functions. --- atcoder/atcoder.go | 57 ++++++++++++++++++++++++++++++++++++++++ atcoder/atcoder_test.go | 58 +++++++++++++++++++++++++++++++++++++++++ atcoder/utils.go | 32 +++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 atcoder/utils.go diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go index e01eb7b..71eb5c6 100644 --- a/atcoder/atcoder.go +++ b/atcoder/atcoder.go @@ -89,3 +89,60 @@ func Parse(str string) (Args, error) { } return Args{}, ErrInvalidSpecifier } + +func login(usr, passwd string) (string, error) { + link := loginPage() + page, msg, err := loadPage(link, selCSSFooter) + if err != nil { + return "", err + } + defer page.Close() + + if msg != "" { + // There shouldn't be any error. + return "", fmt.Errorf(msg) + } + + // Check if current user is logged in. + if !page.MustHasR(selCSSHandle, `Sign In`) { + handle := page.MustElement(selCSSHandle).MustText() + return handle, nil + } + + // check if username/password are valid + if usr == "" || passwd == "" { + return "", errInvalidCredentials + } + + // Otherwise, login. + page.MustElement("#username").Input(usr) + page.MustElement("#password").Input(passwd) + page.MustElement("#submit").MustClick().WaitInvisible() + + elm := page.MustElement(selCSSNotif+`.alert-danger`, selCSSHandle) + if elm.MustMatches(selCSSNotif) { + return "", errInvalidCredentials + } + return elm.MustText(), nil +} + +func logout() error { + page, msg, err := loadPage(hostURL, selCSSFooter) + if err != nil { + return err + } + defer page.Close() + + if msg != "" { + return fmt.Errorf(msg) + } + + if !page.MustHasR(selCSSHandle, `Sign In`) { + // Run the logout javascript function. + page.MustEval("form_logout.submit()") + // Wait till logout is completed. + page.ElementR(selCSSHandle, `Sign In`) + } + + return nil +} diff --git a/atcoder/atcoder_test.go b/atcoder/atcoder_test.go index 3d1b64d..3a45c78 100644 --- a/atcoder/atcoder_test.go +++ b/atcoder/atcoder_test.go @@ -1,6 +1,7 @@ package atcoder import ( + "fmt" "os" "reflect" "testing" @@ -8,6 +9,13 @@ import ( "github.com/joho/godotenv" ) +func getLoginCredentials() (string, string) { + // setup login access to use + usr := os.Getenv("ATCODER_USERNAME") + passwd := os.Getenv("ATCODER_PASSWORD") + return usr, passwd +} + func TestMain(m *testing.M) { // Load local .env file. godotenv.Load() @@ -16,6 +24,12 @@ func TestMain(m *testing.M) { browserBin := os.Getenv("BROWSER_BINARY") Start(browserHeadless, "", browserBin) + if _, err := login(getLoginCredentials()); err != nil { + fmt.Println("Login failed:", err) + Browser.Close() + os.Exit(1) + } + exitCode := m.Run() Browser.Close() @@ -107,3 +121,47 @@ func TestParse(t *testing.T) { }) } } + +func Test_login(t *testing.T) { + logout() + + type args struct { + usr string + passwd string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "Test #1", + args: args{"cptools", "PleaseTryAgain"}, + want: "", + wantErr: true, + }, + { + name: "Test #2", + args: args{"", ""}, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := login(tt.args.usr, tt.args.passwd) + if (err != nil) != tt.wantErr { + t.Errorf("login() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("login() = %v, want %v", got, tt.want) + } + }) + } + + // Hope nothing goes wrong here. + logout() + login(getLoginCredentials()) +} diff --git a/atcoder/utils.go b/atcoder/utils.go new file mode 100644 index 0000000..697d029 --- /dev/null +++ b/atcoder/utils.go @@ -0,0 +1,32 @@ +package atcoder + +import ( + "github.com/cp-tools/cpt-lib/util" + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/proto" +) + +var ( + selCSSHandle = `.navbar-right>li:last-child a` + selCSSNotif = `#main-container .alert` + selCSSFooter = `footer.footer` +) + +func loadPage(link string, selMatch ...string) (*rod.Page, string, error) { + page, err := util.NewPage(Browser, link, []proto.NetworkResourceType{ + proto.NetworkResourceTypeImage, proto.NetworkResourceTypeFont, + proto.NetworkResourceTypeStylesheet, proto.NetworkResourceTypeMedia, + }) + if err != nil { + return nil, "", err + } + + selMatch = append([]string{selCSSNotif}, selMatch...) + elm := page.MustElement(selMatch...) + + if elm.MustMatches(selCSSNotif) { + return page, elm.MustText(), nil + } + + return page, "", nil +} From 87e182a41e0c0ba44f235433fab7a88d58b57563 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Mon, 16 Nov 2020 02:41:06 +0530 Subject: [PATCH 08/20] Add strings utility functions. Added string cleaner and random string generator. --- util/strings.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 util/strings.go diff --git a/util/strings.go b/util/strings.go new file mode 100644 index 0000000..9468eef --- /dev/null +++ b/util/strings.go @@ -0,0 +1,29 @@ +package util + +import ( + "math/rand" + "regexp" +) + +// StrClean replaces all unicode space +// characters in the string with ascii space. +func StrClean(str string) string { + re := regexp.MustCompile(`\p{Z}`) + return re.ReplaceAllString(str, " ") +} + +// StrRandom generates a random string of length n. +// The returned string is strictly alpha-numeric. +// Attrib: https://stackoverflow.com/a/31832326/9606036. +func StrRandom(n int) string { + const charBytes = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789" + + b := make([]byte, n) + for i := range b { + b[i] = charBytes[rand.Intn(len(charBytes))] + } + + return string(b) +} From 89f8278b8e2575fc354da4c8fe7f7793c146f209 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Mon, 16 Nov 2020 02:41:27 +0530 Subject: [PATCH 09/20] Clean returned strings --- atcoder/atcoder.go | 11 +++++------ atcoder/utils.go | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go index 71eb5c6..507c2f9 100644 --- a/atcoder/atcoder.go +++ b/atcoder/atcoder.go @@ -62,7 +62,7 @@ func Parse(str string) (Args, error) { } ) - str = strings.TrimSpace(str) + str = strings.TrimSpace(util.StrClean(str)) if str == "" { return Args{}, nil } @@ -70,7 +70,7 @@ func Parse(str string) (Args, error) { for _, rgx := range valRx { re := regexp.MustCompile(rgx) if re.MatchString(str) { - // attrib : stackoverflow.com/a/9606036 + // https://stackoverflow.com/a/46202939/9606036 match := re.FindStringSubmatch(str) result := map[string]string{} for i, name := range re.SubexpNames() { @@ -78,8 +78,7 @@ func Parse(str string) (Args, error) { result[name] = match[i] } } - // convert to lowercase (default config) - result["prob"] = strings.ToLower(result["prob"]) + arg := Args{ Contest: result["cont"], Problem: result["prob"], @@ -106,7 +105,7 @@ func login(usr, passwd string) (string, error) { // Check if current user is logged in. if !page.MustHasR(selCSSHandle, `Sign In`) { handle := page.MustElement(selCSSHandle).MustText() - return handle, nil + return util.StrClean(handle), nil } // check if username/password are valid @@ -123,7 +122,7 @@ func login(usr, passwd string) (string, error) { if elm.MustMatches(selCSSNotif) { return "", errInvalidCredentials } - return elm.MustText(), nil + return util.StrClean(elm.MustText()), nil } func logout() error { diff --git a/atcoder/utils.go b/atcoder/utils.go index 697d029..62b1e57 100644 --- a/atcoder/utils.go +++ b/atcoder/utils.go @@ -25,7 +25,7 @@ func loadPage(link string, selMatch ...string) (*rod.Page, string, error) { elm := page.MustElement(selMatch...) if elm.MustMatches(selCSSNotif) { - return page, elm.MustText(), nil + return page, util.StrClean(elm.MustText()), nil } return page, "", nil From 21d20f7f3822213fbc69cfa1f86398dbf4606abf Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Fri, 11 Dec 2020 17:22:15 +0530 Subject: [PATCH 10/20] Update dependencies --- atcoder/atcoder.go | 6 +++++- atcoder/utils.go | 8 ++++++-- go.mod | 4 ++-- go.sum | 15 +++++++++------ 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go index 507c2f9..5784263 100644 --- a/atcoder/atcoder.go +++ b/atcoder/atcoder.go @@ -118,7 +118,11 @@ func login(usr, passwd string) (string, error) { page.MustElement("#password").Input(passwd) page.MustElement("#submit").MustClick().WaitInvisible() - elm := page.MustElement(selCSSNotif+`.alert-danger`, selCSSHandle) + elm := page.Race(). + Element(selCSSHandle). + Element(selCSSNotif + `.alert-danger`). + MustDo() + if elm.MustMatches(selCSSNotif) { return "", errInvalidCredentials } diff --git a/atcoder/utils.go b/atcoder/utils.go index 62b1e57..c16b488 100644 --- a/atcoder/utils.go +++ b/atcoder/utils.go @@ -21,9 +21,13 @@ func loadPage(link string, selMatch ...string) (*rod.Page, string, error) { return nil, "", err } - selMatch = append([]string{selCSSNotif}, selMatch...) - elm := page.MustElement(selMatch...) + rc := page.Race() + rc.Element(selCSSNotif) + for _, sel := range selMatch { + rc.Element(sel) + } + elm := rc.MustDo() if elm.MustMatches(selCSSNotif) { return page, util.StrClean(elm.MustText()), nil } diff --git a/go.mod b/go.mod index 0a17204..75e683d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/cp-tools/cpt-lib go 1.14 require ( - github.com/PuerkitoBio/goquery v1.5.1 - github.com/go-rod/rod v0.77.1 + github.com/PuerkitoBio/goquery v1.6.0 + github.com/go-rod/rod v0.84.3 github.com/joho/godotenv v1.3.0 ) diff --git a/go.sum b/go.sum index 01e828a..6ea7d2b 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,21 @@ -github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/goquery v1.6.0 h1:j7taAbelrdcsOlGeMenZxc2AWXD5fieT1/znArdnx94= +github.com/PuerkitoBio/goquery v1.6.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/go-rod/rod v0.77.1 h1:e6QK8KyUaLRswDUDHxW526nDPaP9EvRaaiSce9qm55M= -github.com/go-rod/rod v0.77.1/go.mod h1:XEc4dRYDxlKw+SFG3ZpWTZ8k4vosgg5IDUHKYPMzVSI= +github.com/go-rod/rod v0.84.3 h1:vXOejZALkJX+zpofH298td18WRqO4b7nf9g2wX0I7P0= +github.com/go-rod/rod v0.84.3/go.mod h1:A4Ad8yn2sX9npA+Vvp2q1X7RvSQT+t2BgAjBHSaIatU= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/ysmood/goob v0.3.0 h1:XZ51cZJ4W3WCoCiUktixzMIQF86W7G5VFL4QQ/Q2uS0= github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= -github.com/ysmood/got v0.8.3/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= +github.com/ysmood/got v0.8.7/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= github.com/ysmood/gotrace v0.1.1/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.6.3 h1:4cU+5oOdsyundXHy00t99H0rLXLthuseD3x6W+xmCiU= github.com/ysmood/gson v0.6.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= -github.com/ysmood/leakless v0.6.3 h1:blW3g7NYSTkXtRhmV0vq+t4KzWWUJUdd2pB77O0uteQ= -github.com/ysmood/leakless v0.6.3/go.mod h1:eOWmPkwrQgyJ+JivIgpu5EoJXRN04Wf+wENtR9CUKyE= +github.com/ysmood/leakless v0.6.7 h1:nJNIwECon6vskFI4fgPPlFHEXZKUqKfdKijiopUdReE= +github.com/ysmood/leakless v0.6.7/go.mod h1:eOWmPkwrQgyJ+JivIgpu5EoJXRN04Wf+wENtR9CUKyE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 7f63394d23ba2742f757ca0e5d41bf0d982e8114 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Fri, 1 Jan 2021 04:33:53 +0530 Subject: [PATCH 11/20] More changes. --- atcoder/atcoder.go | 4 +-- atcoder/atcoder_test.go | 5 +++- atcoder/contests.go | 56 ++++++++++++++++++++++++++++++++++++++++ atcoder/contests_test.go | 39 ++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 atcoder/contests.go create mode 100644 atcoder/contests_test.go diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go index 5784263..a36c6d7 100644 --- a/atcoder/atcoder.go +++ b/atcoder/atcoder.go @@ -120,10 +120,10 @@ func login(usr, passwd string) (string, error) { elm := page.Race(). Element(selCSSHandle). - Element(selCSSNotif + `.alert-danger`). + Element(`.alert.for__password`). MustDo() - if elm.MustMatches(selCSSNotif) { + if !elm.MustMatches(selCSSHandle) { return "", errInvalidCredentials } return util.StrClean(elm.MustText()), nil diff --git a/atcoder/atcoder_test.go b/atcoder/atcoder_test.go index 3a45c78..a6fa05d 100644 --- a/atcoder/atcoder_test.go +++ b/atcoder/atcoder_test.go @@ -22,7 +22,10 @@ func TestMain(m *testing.M) { _, browserHeadless := os.LookupEnv("BROWSER_HEADLESS") browserBin := os.Getenv("BROWSER_BINARY") - Start(browserHeadless, "", browserBin) + if err := Start(browserHeadless, "", browserBin); err != nil { + fmt.Println("Failed to start browser:", err) + os.Exit(1) + } if _, err := login(getLoginCredentials()); err != nil { fmt.Println("Login failed:", err) diff --git a/atcoder/contests.go b/atcoder/contests.go new file mode 100644 index 0000000..1d26d55 --- /dev/null +++ b/atcoder/contests.go @@ -0,0 +1,56 @@ +package atcoder + +import ( + "fmt" + "time" +) + +// DashboardPage returns link to dashboard of contest +func (arg Args) DashboardPage() (link string, err error) { + if arg.Contest == "" { + return "", ErrInvalidSpecifier + } + + link = fmt.Sprintf("%v/contests/%v", hostURL, arg.Contest) + return +} + +func (arg Args) CountdownPage(isVC bool) (link string, err error) { + if arg.Contest == "" { + return "", ErrInvalidSpecifier + } + + dashboardLink, _ := arg.DashboardPage() + if isVC == true { + link = fmt.Sprintf("%v/virtual", dashboardLink) + return + } + + link = dashboardLink + return +} + +func (arg Args) GetCountdown(isVC bool) (time.Duration, error) { + if arg.Contest == "" { + return 0, ErrInvalidSpecifier + } + + link, err := arg.CountdownPage(isVC) + if err != nil { + return 0, err + } + + page, msg, err := loadPage(link, selCSSFooter) + if err != nil { + return 0, err + } + defer page.Close() + + if msg != "" { + // there should be no notification + return 0, fmt.Errorf(msg) + } + + fmt.Println(page.MustEval("startTime._d.toISOString()").String()) + return 0, nil +} diff --git a/atcoder/contests_test.go b/atcoder/contests_test.go new file mode 100644 index 0000000..836ad06 --- /dev/null +++ b/atcoder/contests_test.go @@ -0,0 +1,39 @@ +package atcoder + +import ( + "testing" + "time" +) + +func TestArgs_GetCountdown(t *testing.T) { + type fields struct { + isVC bool + } + tests := []struct { + name string + arg Args + fields fields + want time.Duration + wantErr bool + }{ + { + name: "Test #1", + fields: fields{true}, + arg: Args{"abc180", ""}, + want: 0, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.arg.GetCountdown(tt.fields.isVC) + if (err != nil) != tt.wantErr { + t.Errorf("Args.GetCountdown() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Args.GetCountdown() = %v, want %v", got, tt.want) + } + }) + } +} From 07ecc33d6aca095bb5d0be38d41e93c1ab996913 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Sat, 2 Jan 2021 00:26:59 +0530 Subject: [PATCH 12/20] Fix logged in handle parsing issues. Change selCSSHandle to select only the username of logged in user. --- atcoder/atcoder.go | 16 ++++++++++------ atcoder/utils.go | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go index a36c6d7..43b9eda 100644 --- a/atcoder/atcoder.go +++ b/atcoder/atcoder.go @@ -18,6 +18,10 @@ type ( Contest string Problem string } + + page struct { + *rod.Page + } ) // Errors returned by library. @@ -103,7 +107,7 @@ func login(usr, passwd string) (string, error) { } // Check if current user is logged in. - if !page.MustHasR(selCSSHandle, `Sign In`) { + if page.MustHas(selCSSHandle) { handle := page.MustElement(selCSSHandle).MustText() return util.StrClean(handle), nil } @@ -120,7 +124,7 @@ func login(usr, passwd string) (string, error) { elm := page.Race(). Element(selCSSHandle). - Element(`.alert.for__password`). + ElementR(selCSSNotif, "Username or Password is incorrect"). MustDo() if !elm.MustMatches(selCSSHandle) { @@ -140,11 +144,11 @@ func logout() error { return fmt.Errorf(msg) } - if !page.MustHasR(selCSSHandle, `Sign In`) { - // Run the logout javascript function. - page.MustEval("form_logout.submit()") + // Run the logout javascript function. + page.MustEval("form_logout.submit()") + if page.MustHas(selCSSHandle) { // Wait till logout is completed. - page.ElementR(selCSSHandle, `Sign In`) + page.MustElement(selCSSHandle).WaitInvisible() } return nil diff --git a/atcoder/utils.go b/atcoder/utils.go index c16b488..b06ae9a 100644 --- a/atcoder/utils.go +++ b/atcoder/utils.go @@ -7,7 +7,7 @@ import ( ) var ( - selCSSHandle = `.navbar-right>li:last-child a` + selCSSHandle = `.navbar-right>li:last-child>a[class]` selCSSNotif = `#main-container .alert` selCSSFooter = `footer.footer` ) From 33655637ccbc3df2bd8f194f3b2bdcf1238de1e9 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Sun, 3 Jan 2021 18:20:08 +0530 Subject: [PATCH 13/20] Create page sub-struct. --- atcoder/utils.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/atcoder/utils.go b/atcoder/utils.go index b06ae9a..c64b749 100644 --- a/atcoder/utils.go +++ b/atcoder/utils.go @@ -2,7 +2,6 @@ package atcoder import ( "github.com/cp-tools/cpt-lib/util" - "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) @@ -12,8 +11,8 @@ var ( selCSSFooter = `footer.footer` ) -func loadPage(link string, selMatch ...string) (*rod.Page, string, error) { - page, err := util.NewPage(Browser, link, []proto.NetworkResourceType{ +func loadPage(link string, selMatch ...string) (*page, string, error) { + rp, err := util.NewPage(Browser, link, []proto.NetworkResourceType{ proto.NetworkResourceTypeImage, proto.NetworkResourceTypeFont, proto.NetworkResourceTypeStylesheet, proto.NetworkResourceTypeMedia, }) @@ -21,7 +20,9 @@ func loadPage(link string, selMatch ...string) (*rod.Page, string, error) { return nil, "", err } - rc := page.Race() + p := &page{rp} + + rc := p.Race() rc.Element(selCSSNotif) for _, sel := range selMatch { rc.Element(sel) @@ -29,8 +30,8 @@ func loadPage(link string, selMatch ...string) (*rod.Page, string, error) { elm := rc.MustDo() if elm.MustMatches(selCSSNotif) { - return page, util.StrClean(elm.MustText()), nil + return p, util.StrClean(elm.MustText()), nil } - return page, "", nil + return p, "", nil } From 1f44304b6983cbf74ed0a952eaeaf1ce65c368d1 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Sun, 3 Jan 2021 18:21:15 +0530 Subject: [PATCH 14/20] Add GetCountdown method. --- atcoder/contests.go | 53 ++++++++++++++++++-------- atcoder/contests_test.go | 81 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 21 deletions(-) diff --git a/atcoder/contests.go b/atcoder/contests.go index 1d26d55..1f01461 100644 --- a/atcoder/contests.go +++ b/atcoder/contests.go @@ -2,6 +2,7 @@ package atcoder import ( "fmt" + "regexp" "time" ) @@ -15,42 +16,64 @@ func (arg Args) DashboardPage() (link string, err error) { return } -func (arg Args) CountdownPage(isVC bool) (link string, err error) { +// VirtualPage returns link to virtual contest tab. +func (arg Args) VirtualPage() (link string, err error) { if arg.Contest == "" { return "", ErrInvalidSpecifier } dashboardLink, _ := arg.DashboardPage() - if isVC == true { - link = fmt.Sprintf("%v/virtual", dashboardLink) - return - } - - link = dashboardLink + link = fmt.Sprintf("%v/virtual", dashboardLink) return } -func (arg Args) GetCountdown(isVC bool) (time.Duration, error) { - if arg.Contest == "" { - return 0, ErrInvalidSpecifier +func (p *page) getCountdown() (time.Duration, error) { + // First check for virtual countdown. + if match := regexp.MustCompile(`var virtualStartTime = moment\("(.*)"\)`). + FindStringSubmatch(p.MustElement("html").MustHTML()); len(match) == 2 { + startTime, err := time.Parse(time.RFC3339Nano, match[1]) + if err != nil { + return 0, err + } + + dur := time.Until(startTime).Truncate(time.Second) + if dur < 0 { // Virtual already started; No countdown + dur = 0 + } + return dur, nil } - link, err := arg.CountdownPage(isVC) + // Parse actual contest start time. + startTimeStr := p.MustEval("startTime._d.toISOString()").String() + startTime, err := time.Parse(time.RFC3339Nano, startTimeStr) + if err != nil { + return 0, err + } + + dur := time.Until(startTime).Truncate(time.Second) + if dur < 0 { // Contest already started; No countdown + dur = 0 + } + return dur, nil +} + +// GetCountdown ... +func (arg Args) GetCountdown() (time.Duration, error) { + link, err := arg.VirtualPage() if err != nil { return 0, err } - page, msg, err := loadPage(link, selCSSFooter) + p, msg, err := loadPage(link, selCSSFooter) if err != nil { return 0, err } - defer page.Close() + defer p.Close() if msg != "" { // there should be no notification return 0, fmt.Errorf(msg) } - fmt.Println(page.MustEval("startTime._d.toISOString()").String()) - return 0, nil + return p.getCountdown() } diff --git a/atcoder/contests_test.go b/atcoder/contests_test.go index 836ad06..24a9a3a 100644 --- a/atcoder/contests_test.go +++ b/atcoder/contests_test.go @@ -5,28 +5,97 @@ import ( "time" ) -func TestArgs_GetCountdown(t *testing.T) { - type fields struct { - isVC bool +func TestArgs_DashboardPage(t *testing.T) { + tests := []struct { + name string + arg Args + wantLink string + wantErr bool + }{ + { + name: "Test #1", + arg: Args{"hhkb2020", ""}, + wantLink: "https://atcoder.jp/contests/hhkb2020", + wantErr: false, + }, + { + name: "Test #2", + arg: Args{}, + wantLink: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotLink, err := tt.arg.DashboardPage() + if (err != nil) != tt.wantErr { + t.Errorf("Args.DashboardPage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotLink != tt.wantLink { + t.Errorf("Args.DashboardPage() = %v, want %v", gotLink, tt.wantLink) + } + }) + } +} + +func TestArgs_VirtualPage(t *testing.T) { + tests := []struct { + name string + arg Args + wantLink string + wantErr bool + }{ + { + name: "Test #1", + arg: Args{"tokiomarine2020", ""}, + wantLink: "https://atcoder.jp/contests/tokiomarine2020/virtual", + wantErr: false, + }, + { + name: "Test #2", + arg: Args{}, + wantLink: "", + wantErr: true, + }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotLink, err := tt.arg.VirtualPage() + if (err != nil) != tt.wantErr { + t.Errorf("Args.DashboardPage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotLink != tt.wantLink { + t.Errorf("Args.DashboardPage() = %v, want %v", gotLink, tt.wantLink) + } + }) + } +} + +func TestArgs_GetCountdown(t *testing.T) { tests := []struct { name string arg Args - fields fields want time.Duration wantErr bool }{ { name: "Test #1", - fields: fields{true}, arg: Args{"abc180", ""}, want: 0, wantErr: false, }, + { + name: "Test #2", + arg: Args{"InVaLiD123", ""}, + want: 0, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.arg.GetCountdown(tt.fields.isVC) + got, err := tt.arg.GetCountdown() if (err != nil) != tt.wantErr { t.Errorf("Args.GetCountdown() error = %v, wantErr %v", err, tt.wantErr) return From 415049966006e4a2206d0e9acbcd82c92392d356 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Sun, 3 Jan 2021 18:21:38 +0530 Subject: [PATCH 15/20] Upgrade dependencies. --- go.mod | 2 +- go.sum | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 75e683d..9ebd4d9 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,6 @@ go 1.14 require ( github.com/PuerkitoBio/goquery v1.6.0 - github.com/go-rod/rod v0.84.3 + github.com/go-rod/rod v0.87.2 github.com/joho/godotenv v1.3.0 ) diff --git a/go.sum b/go.sum index 6ea7d2b..84cedfb 100644 --- a/go.sum +++ b/go.sum @@ -2,21 +2,21 @@ github.com/PuerkitoBio/goquery v1.6.0 h1:j7taAbelrdcsOlGeMenZxc2AWXD5fieT1/znArd github.com/PuerkitoBio/goquery v1.6.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/go-rod/rod v0.84.3 h1:vXOejZALkJX+zpofH298td18WRqO4b7nf9g2wX0I7P0= -github.com/go-rod/rod v0.84.3/go.mod h1:A4Ad8yn2sX9npA+Vvp2q1X7RvSQT+t2BgAjBHSaIatU= +github.com/go-rod/rod v0.87.2 h1:zlos09t7oV1kDxgz26oRvcYX0mQkjeJSNJugq9MUCno= +github.com/go-rod/rod v0.87.2/go.mod h1:UEYVhPXlrtQIL1Nlnq/fPY7Q5pB708UIEy5IKtNVuFg= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/ysmood/goob v0.3.0 h1:XZ51cZJ4W3WCoCiUktixzMIQF86W7G5VFL4QQ/Q2uS0= github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= -github.com/ysmood/got v0.8.7/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= +github.com/ysmood/got v0.8.9/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= github.com/ysmood/gotrace v0.1.1/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.6.3 h1:4cU+5oOdsyundXHy00t99H0rLXLthuseD3x6W+xmCiU= github.com/ysmood/gson v0.6.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= -github.com/ysmood/leakless v0.6.7 h1:nJNIwECon6vskFI4fgPPlFHEXZKUqKfdKijiopUdReE= -github.com/ysmood/leakless v0.6.7/go.mod h1:eOWmPkwrQgyJ+JivIgpu5EoJXRN04Wf+wENtR9CUKyE= +github.com/ysmood/leakless v0.6.11 h1:9y8/5v9FjHFXo81vZyh+VQTQCLs+aFjuJJqbpn33TLg= +github.com/ysmood/leakless v0.6.11/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 505b96d6a3324c247bb3ba749048cf47040f1374 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Sun, 3 Jan 2021 18:25:50 +0530 Subject: [PATCH 16/20] Add workflow. --- .github/workflows/atcoder.yaml | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/atcoder.yaml diff --git a/.github/workflows/atcoder.yaml b/.github/workflows/atcoder.yaml new file mode 100644 index 0000000..570e761 --- /dev/null +++ b/.github/workflows/atcoder.yaml @@ -0,0 +1,45 @@ +name: Test (atcoder) + +on: + push: + branches: ['**'] + tags-ignore: ['*'] + paths: ['atcoder/*'] + + workflow_dispatch: + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - name: Set up Go 1.15 + uses: actions/setup-go@v2 + with: + go-version: 1.15 + + - name: Checkout Project + uses: actions/checkout@v2 + + - name: Cache Dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-go- + + - name: Run Tests + run: go test -v -coverprofile=c.out ./atcoder + env: + BROWSER_HEADLESS: + BROWSER_BINARY: google-chrome + + ATCODER_USERNAME: cptools + ATCODER_PASSWORD: ${{ secrets.ATCODER_PASSWORD }} + + - name: Upload Coverage + uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: c.out + From d4bc99a91a5479a3d40bb38e1426645297cfdce8 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Mon, 4 Jan 2021 03:23:58 +0530 Subject: [PATCH 17/20] Restructure code. Handle element waiting inside the method, rather than loadPage. This is to accomodate the various selectors available. Do parsing in page method. This is a refractor to allow cross page usage. --- atcoder/atcoder.go | 85 +++++++++++++++++++++++++-------------------- atcoder/contests.go | 10 +++--- atcoder/utils.go | 42 +++++++++------------- 3 files changed, 69 insertions(+), 68 deletions(-) diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go index 43b9eda..74a15a7 100644 --- a/atcoder/atcoder.go +++ b/atcoder/atcoder.go @@ -93,63 +93,72 @@ func Parse(str string) (Args, error) { return Args{}, ErrInvalidSpecifier } -func login(usr, passwd string) (string, error) { - link := loginPage() - page, msg, err := loadPage(link, selCSSFooter) - if err != nil { - return "", err - } - defer page.Close() - - if msg != "" { - // There shouldn't be any error. - return "", fmt.Errorf(msg) - } - +func (p *page) login(usr, passwd string) (string, error) { // Check if current user is logged in. - if page.MustHas(selCSSHandle) { - handle := page.MustElement(selCSSHandle).MustText() - return util.StrClean(handle), nil + if handle := p.MustEval(`userScreenName`).String(); handle != "" { + return handle, nil } - // check if username/password are valid + // Check if username/password are valid. if usr == "" || passwd == "" { return "", errInvalidCredentials } // Otherwise, login. - page.MustElement("#username").Input(usr) - page.MustElement("#password").Input(passwd) - page.MustElement("#submit").MustClick().WaitInvisible() + p.MustElement("#username").Input(usr) + p.MustElement("#password").Input(passwd) + p.MustElement("#submit").MustClick().WaitInvisible() - elm := page.Race(). - Element(selCSSHandle). - ElementR(selCSSNotif, "Username or Password is incorrect"). - MustDo() + _, err := p.Race().ElementR(`.alert`, `Username or Password is incorrect`). + Handle(func(e *rod.Element) error { return errInvalidCredentials }). + Element(`.navbar-right>li:last-child>a[class]`).Do() - if !elm.MustMatches(selCSSHandle) { - return "", errInvalidCredentials + if err != nil { + return "", err } - return util.StrClean(elm.MustText()), nil + + handle := p.MustEval(`userScreenName`).String() + return handle, nil } -func logout() error { - page, msg, err := loadPage(hostURL, selCSSFooter) +func login(usr, passwd string) (string, error) { + link := loginPage() + p, err := loadPage(link) if err != nil { - return err + return "", err } - defer page.Close() + defer p.Close() - if msg != "" { - return fmt.Errorf(msg) + _, err = p.Race().Element(`alert`).Handle(handleErrMsg). + Element(`footer.footer`).Do() + + if err != nil { + return "", err } + return p.login(usr, passwd) +} + +func (p *page) logout() error { // Run the logout javascript function. - page.MustEval("form_logout.submit()") - if page.MustHas(selCSSHandle) { - // Wait till logout is completed. - page.MustElement(selCSSHandle).WaitInvisible() + p.MustEval("form_logout.submit()") + p.MustWait(`userScreenName == ""`) + return nil +} + +func logout() error { + p, err := loadPage(hostURL) + if err != nil { + return err } + defer p.Close() - return nil + _, err = p.Race().Element(`.alert`).Handle(handleErrMsg). + Element(`footer.footer`).Do() + + if err != nil { + return err + } + + return p.logout() } diff --git a/atcoder/contests.go b/atcoder/contests.go index 1f01461..2cad4af 100644 --- a/atcoder/contests.go +++ b/atcoder/contests.go @@ -64,15 +64,17 @@ func (arg Args) GetCountdown() (time.Duration, error) { return 0, err } - p, msg, err := loadPage(link, selCSSFooter) + p, err := loadPage(link) if err != nil { return 0, err } defer p.Close() - if msg != "" { - // there should be no notification - return 0, fmt.Errorf(msg) + _, err = p.Race().Element(`.alert`).Handle(handleErrMsg). + Element(`footer.footer`).Do() + + if err != nil { + return 0, err } return p.getCountdown() diff --git a/atcoder/utils.go b/atcoder/utils.go index c64b749..53d8988 100644 --- a/atcoder/utils.go +++ b/atcoder/utils.go @@ -1,37 +1,27 @@ package atcoder import ( + "fmt" + "github.com/cp-tools/cpt-lib/util" + "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) -var ( - selCSSHandle = `.navbar-right>li:last-child>a[class]` - selCSSNotif = `#main-container .alert` - selCSSFooter = `footer.footer` -) - -func loadPage(link string, selMatch ...string) (*page, string, error) { - rp, err := util.NewPage(Browser, link, []proto.NetworkResourceType{ - proto.NetworkResourceTypeImage, proto.NetworkResourceTypeFont, - proto.NetworkResourceTypeStylesheet, proto.NetworkResourceTypeMedia, - }) - if err != nil { - return nil, "", err - } - - p := &page{rp} - - rc := p.Race() - rc.Element(selCSSNotif) - for _, sel := range selMatch { - rc.Element(sel) +func loadPage(link string) (*page, error) { + // Blocking these files results in faster page loading. + resourcesToBlock := []proto.NetworkResourceType{ + proto.NetworkResourceTypeFont, + proto.NetworkResourceTypeMedia, + proto.NetworkResourceTypeImage, + proto.NetworkResourceTypeStylesheet, } - elm := rc.MustDo() - if elm.MustMatches(selCSSNotif) { - return p, util.StrClean(elm.MustText()), nil - } + p, err := util.NewPage(Browser, link, resourcesToBlock) + return &page{p}, err +} - return p, "", nil +func handleErrMsg(e *rod.Element) error { + // There should be no notification. + return fmt.Errorf(e.MustText()) } From 423adfc16d1fda03f40fe49a5989e2bb79acd9f6 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Mon, 4 Jan 2021 14:33:07 +0530 Subject: [PATCH 18/20] 'go mod tidy' changes. --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index 84cedfb..b2c07e0 100644 --- a/go.sum +++ b/go.sum @@ -8,7 +8,9 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/ysmood/goob v0.3.0 h1:XZ51cZJ4W3WCoCiUktixzMIQF86W7G5VFL4QQ/Q2uS0= github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= +github.com/ysmood/got v0.8.9 h1:fUbi0c03DSNdo82DWd4m6l63WswtJDqwBPTcrrKMUHU= github.com/ysmood/got v0.8.9/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= +github.com/ysmood/gotrace v0.1.1 h1:1H9L1Rc0o/80+2Vtm3vn85rNVpNOo94/wf+G5qW0JY8= github.com/ysmood/gotrace v0.1.1/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.6.3 h1:4cU+5oOdsyundXHy00t99H0rLXLthuseD3x6W+xmCiU= github.com/ysmood/gson v0.6.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= From 933ffaee8dff7598629494c033d0b033b9404f99 Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Mon, 4 Jan 2021 16:05:05 +0530 Subject: [PATCH 19/20] Move Page functions to separate file. --- atcoder/atcoder.go | 5 --- atcoder/atcoder_test.go | 19 --------- atcoder/contests.go | 22 ---------- atcoder/contests_test.go | 68 ------------------------------ atcoder/pages.go | 29 +++++++++++++ atcoder/pages_test.go | 90 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 114 deletions(-) create mode 100644 atcoder/pages.go create mode 100644 atcoder/pages_test.go diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go index 74a15a7..bf6eb50 100644 --- a/atcoder/atcoder.go +++ b/atcoder/atcoder.go @@ -45,11 +45,6 @@ func Start(headless bool, userDataDir, bin string) error { return err } -// loginPage returns link to login page -func loginPage() string { - return fmt.Sprintf("%v/login", hostURL) -} - // Parse passed in specifier string to new Args struct. // Validates parsed args and returns error if any. func Parse(str string) (Args, error) { diff --git a/atcoder/atcoder_test.go b/atcoder/atcoder_test.go index a6fa05d..af97a9e 100644 --- a/atcoder/atcoder_test.go +++ b/atcoder/atcoder_test.go @@ -39,25 +39,6 @@ func TestMain(m *testing.M) { os.Exit(exitCode) } -func Test_loginPage(t *testing.T) { - tests := []struct { - name string - want string - }{ - { - name: "Login Page", - want: "https://atcoder.jp/login", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := loginPage(); got != tt.want { - t.Errorf("loginPage() = %v, want %v", got, tt.want) - } - }) - } -} - func TestParse(t *testing.T) { type args struct { str string diff --git a/atcoder/contests.go b/atcoder/contests.go index 2cad4af..ff8c344 100644 --- a/atcoder/contests.go +++ b/atcoder/contests.go @@ -1,32 +1,10 @@ package atcoder import ( - "fmt" "regexp" "time" ) -// DashboardPage returns link to dashboard of contest -func (arg Args) DashboardPage() (link string, err error) { - if arg.Contest == "" { - return "", ErrInvalidSpecifier - } - - link = fmt.Sprintf("%v/contests/%v", hostURL, arg.Contest) - return -} - -// VirtualPage returns link to virtual contest tab. -func (arg Args) VirtualPage() (link string, err error) { - if arg.Contest == "" { - return "", ErrInvalidSpecifier - } - - dashboardLink, _ := arg.DashboardPage() - link = fmt.Sprintf("%v/virtual", dashboardLink) - return -} - func (p *page) getCountdown() (time.Duration, error) { // First check for virtual countdown. if match := regexp.MustCompile(`var virtualStartTime = moment\("(.*)"\)`). diff --git a/atcoder/contests_test.go b/atcoder/contests_test.go index 24a9a3a..ec7266a 100644 --- a/atcoder/contests_test.go +++ b/atcoder/contests_test.go @@ -5,74 +5,6 @@ import ( "time" ) -func TestArgs_DashboardPage(t *testing.T) { - tests := []struct { - name string - arg Args - wantLink string - wantErr bool - }{ - { - name: "Test #1", - arg: Args{"hhkb2020", ""}, - wantLink: "https://atcoder.jp/contests/hhkb2020", - wantErr: false, - }, - { - name: "Test #2", - arg: Args{}, - wantLink: "", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotLink, err := tt.arg.DashboardPage() - if (err != nil) != tt.wantErr { - t.Errorf("Args.DashboardPage() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotLink != tt.wantLink { - t.Errorf("Args.DashboardPage() = %v, want %v", gotLink, tt.wantLink) - } - }) - } -} - -func TestArgs_VirtualPage(t *testing.T) { - tests := []struct { - name string - arg Args - wantLink string - wantErr bool - }{ - { - name: "Test #1", - arg: Args{"tokiomarine2020", ""}, - wantLink: "https://atcoder.jp/contests/tokiomarine2020/virtual", - wantErr: false, - }, - { - name: "Test #2", - arg: Args{}, - wantLink: "", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotLink, err := tt.arg.VirtualPage() - if (err != nil) != tt.wantErr { - t.Errorf("Args.DashboardPage() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotLink != tt.wantLink { - t.Errorf("Args.DashboardPage() = %v, want %v", gotLink, tt.wantLink) - } - }) - } -} - func TestArgs_GetCountdown(t *testing.T) { tests := []struct { name string diff --git a/atcoder/pages.go b/atcoder/pages.go new file mode 100644 index 0000000..8865cc6 --- /dev/null +++ b/atcoder/pages.go @@ -0,0 +1,29 @@ +package atcoder + +import "fmt" + +// loginPage returns link to login page +func loginPage() string { + return fmt.Sprintf("%v/login", hostURL) +} + +// DashboardPage returns link to dashboard of contest +func (arg Args) DashboardPage() (link string, err error) { + if arg.Contest == "" { + return "", ErrInvalidSpecifier + } + + link = fmt.Sprintf("%v/contests/%v", hostURL, arg.Contest) + return +} + +// VirtualPage returns link to virtual contest tab. +func (arg Args) VirtualPage() (link string, err error) { + if arg.Contest == "" { + return "", ErrInvalidSpecifier + } + + dashboardLink, _ := arg.DashboardPage() + link = fmt.Sprintf("%v/virtual", dashboardLink) + return +} diff --git a/atcoder/pages_test.go b/atcoder/pages_test.go new file mode 100644 index 0000000..d744aee --- /dev/null +++ b/atcoder/pages_test.go @@ -0,0 +1,90 @@ +package atcoder + +import "testing" + +func Test_loginPage(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "Login Page", + want: "https://atcoder.jp/login", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := loginPage(); got != tt.want { + t.Errorf("loginPage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestArgs_DashboardPage(t *testing.T) { + tests := []struct { + name string + arg Args + wantLink string + wantErr bool + }{ + { + name: "Test #1", + arg: Args{"hhkb2020", ""}, + wantLink: "https://atcoder.jp/contests/hhkb2020", + wantErr: false, + }, + { + name: "Test #2", + arg: Args{}, + wantLink: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotLink, err := tt.arg.DashboardPage() + if (err != nil) != tt.wantErr { + t.Errorf("Args.DashboardPage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotLink != tt.wantLink { + t.Errorf("Args.DashboardPage() = %v, want %v", gotLink, tt.wantLink) + } + }) + } +} + +func TestArgs_VirtualPage(t *testing.T) { + tests := []struct { + name string + arg Args + wantLink string + wantErr bool + }{ + { + name: "Test #1", + arg: Args{"tokiomarine2020", ""}, + wantLink: "https://atcoder.jp/contests/tokiomarine2020/virtual", + wantErr: false, + }, + { + name: "Test #2", + arg: Args{}, + wantLink: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotLink, err := tt.arg.VirtualPage() + if (err != nil) != tt.wantErr { + t.Errorf("Args.DashboardPage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotLink != tt.wantLink { + t.Errorf("Args.DashboardPage() = %v, want %v", gotLink, tt.wantLink) + } + }) + } +} From 161416751060e8026258e8d647cc697b977ca6cb Mon Sep 17 00:00:00 2001 From: Adithya Joshua Dsilva Date: Mon, 4 Jan 2021 16:37:16 +0530 Subject: [PATCH 20/20] Move login, logout functions to test package. --- atcoder/atcoder.go | 73 +-------------------------------------- atcoder/atcoder_test.go | 76 +++++++++++++++++------------------------ atcoder/pages.go | 5 --- atcoder/pages_test.go | 19 ----------- 4 files changed, 33 insertions(+), 140 deletions(-) diff --git a/atcoder/atcoder.go b/atcoder/atcoder.go index bf6eb50..7b931e3 100644 --- a/atcoder/atcoder.go +++ b/atcoder/atcoder.go @@ -26,8 +26,7 @@ type ( // Errors returned by library. var ( - ErrInvalidSpecifier = fmt.Errorf("invalid specifier data") - errInvalidCredentials = fmt.Errorf("invalid login credentials") + ErrInvalidSpecifier = fmt.Errorf("invalid specifier data") ) var ( @@ -87,73 +86,3 @@ func Parse(str string) (Args, error) { } return Args{}, ErrInvalidSpecifier } - -func (p *page) login(usr, passwd string) (string, error) { - // Check if current user is logged in. - if handle := p.MustEval(`userScreenName`).String(); handle != "" { - return handle, nil - } - - // Check if username/password are valid. - if usr == "" || passwd == "" { - return "", errInvalidCredentials - } - - // Otherwise, login. - p.MustElement("#username").Input(usr) - p.MustElement("#password").Input(passwd) - p.MustElement("#submit").MustClick().WaitInvisible() - - _, err := p.Race().ElementR(`.alert`, `Username or Password is incorrect`). - Handle(func(e *rod.Element) error { return errInvalidCredentials }). - Element(`.navbar-right>li:last-child>a[class]`).Do() - - if err != nil { - return "", err - } - - handle := p.MustEval(`userScreenName`).String() - return handle, nil -} - -func login(usr, passwd string) (string, error) { - link := loginPage() - p, err := loadPage(link) - if err != nil { - return "", err - } - defer p.Close() - - _, err = p.Race().Element(`alert`).Handle(handleErrMsg). - Element(`footer.footer`).Do() - - if err != nil { - return "", err - } - - return p.login(usr, passwd) -} - -func (p *page) logout() error { - // Run the logout javascript function. - p.MustEval("form_logout.submit()") - p.MustWait(`userScreenName == ""`) - return nil -} - -func logout() error { - p, err := loadPage(hostURL) - if err != nil { - return err - } - defer p.Close() - - _, err = p.Race().Element(`.alert`).Handle(handleErrMsg). - Element(`footer.footer`).Do() - - if err != nil { - return err - } - - return p.logout() -} diff --git a/atcoder/atcoder_test.go b/atcoder/atcoder_test.go index af97a9e..dd8a787 100644 --- a/atcoder/atcoder_test.go +++ b/atcoder/atcoder_test.go @@ -6,9 +6,41 @@ import ( "reflect" "testing" + "github.com/go-rod/rod" "github.com/joho/godotenv" ) +func login(usr, passwd string) (string, error) { + p, err := loadPage(fmt.Sprintf("%v/login", hostURL)) + if err != nil { + return "", err + } + defer p.Close() + + if _, err := p.Race().Element(`alert`).Handle(handleErrMsg). + Element(`footer.footer`).Do(); err != nil { + return "", err + } + + // Check if current user is logged in. + if handle := p.MustEval(`userScreenName`).String(); handle != "" { + return handle, nil + } + // Otherwise, login. + p.MustElement("#username").Input(usr) + p.MustElement("#password").Input(passwd) + p.MustElement("#submit").MustClick().WaitInvisible() + + if _, err := p.Race().ElementR(`.alert`, `Username or Password is incorrect`). + Handle(func(e *rod.Element) error { return fmt.Errorf(e.MustText()) }). + Element(`.navbar-right>li:last-child>a[class]`).Do(); err != nil { + return "", err + } + + handle := p.MustEval(`userScreenName`).String() + return handle, nil +} + func getLoginCredentials() (string, string) { // setup login access to use usr := os.Getenv("ATCODER_USERNAME") @@ -105,47 +137,3 @@ func TestParse(t *testing.T) { }) } } - -func Test_login(t *testing.T) { - logout() - - type args struct { - usr string - passwd string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "Test #1", - args: args{"cptools", "PleaseTryAgain"}, - want: "", - wantErr: true, - }, - { - name: "Test #2", - args: args{"", ""}, - want: "", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := login(tt.args.usr, tt.args.passwd) - if (err != nil) != tt.wantErr { - t.Errorf("login() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("login() = %v, want %v", got, tt.want) - } - }) - } - - // Hope nothing goes wrong here. - logout() - login(getLoginCredentials()) -} diff --git a/atcoder/pages.go b/atcoder/pages.go index 8865cc6..4c9e5c4 100644 --- a/atcoder/pages.go +++ b/atcoder/pages.go @@ -2,11 +2,6 @@ package atcoder import "fmt" -// loginPage returns link to login page -func loginPage() string { - return fmt.Sprintf("%v/login", hostURL) -} - // DashboardPage returns link to dashboard of contest func (arg Args) DashboardPage() (link string, err error) { if arg.Contest == "" { diff --git a/atcoder/pages_test.go b/atcoder/pages_test.go index d744aee..af35abe 100644 --- a/atcoder/pages_test.go +++ b/atcoder/pages_test.go @@ -2,25 +2,6 @@ package atcoder import "testing" -func Test_loginPage(t *testing.T) { - tests := []struct { - name string - want string - }{ - { - name: "Login Page", - want: "https://atcoder.jp/login", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := loginPage(); got != tt.want { - t.Errorf("loginPage() = %v, want %v", got, tt.want) - } - }) - } -} - func TestArgs_DashboardPage(t *testing.T) { tests := []struct { name string