Refactor browser-related test functions to a ./test/library/browsertest
package.
Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
parent
22953cdb78
commit
545c26e5fe
@ -20,7 +20,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sclevine/agouti"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
@ -29,6 +28,7 @@ import (
|
|||||||
"go.pinniped.dev/pkg/oidcclient"
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
"go.pinniped.dev/pkg/oidcclient/filesession"
|
"go.pinniped.dev/pkg/oidcclient/filesession"
|
||||||
"go.pinniped.dev/test/library"
|
"go.pinniped.dev/test/library"
|
||||||
|
"go.pinniped.dev/test/library/browsertest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCLIGetKubeconfig(t *testing.T) {
|
func TestCLIGetKubeconfig(t *testing.T) {
|
||||||
@ -107,80 +107,14 @@ func runPinnipedCLIGetKubeconfig(t *testing.T, pinnipedExe, token, namespaceName
|
|||||||
return string(output)
|
return string(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
type loginProviderPatterns struct {
|
|
||||||
Name string
|
|
||||||
IssuerPattern *regexp.Regexp
|
|
||||||
LoginPagePattern *regexp.Regexp
|
|
||||||
UsernameSelector string
|
|
||||||
PasswordSelector string
|
|
||||||
LoginButtonSelector string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLoginProvider(t *testing.T) *loginProviderPatterns {
|
|
||||||
t.Helper()
|
|
||||||
issuer := library.IntegrationEnv(t).CLITestUpstream.Issuer
|
|
||||||
for _, p := range []loginProviderPatterns{
|
|
||||||
{
|
|
||||||
Name: "Okta",
|
|
||||||
IssuerPattern: regexp.MustCompile(`\Ahttps://.+\.okta\.com/.+\z`),
|
|
||||||
LoginPagePattern: regexp.MustCompile(`\Ahttps://.+\.okta\.com/.+\z`),
|
|
||||||
UsernameSelector: "input#okta-signin-username",
|
|
||||||
PasswordSelector: "input#okta-signin-password",
|
|
||||||
LoginButtonSelector: "input#okta-signin-submit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Dex",
|
|
||||||
IssuerPattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex.*\z`),
|
|
||||||
LoginPagePattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex/auth/local.+\z`),
|
|
||||||
UsernameSelector: "input#login",
|
|
||||||
PasswordSelector: "input#password",
|
|
||||||
LoginButtonSelector: "button#submit-login",
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
if p.IssuerPattern.MatchString(issuer) {
|
|
||||||
return &p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
require.Failf(t, "could not find login provider for issuer %q", issuer)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLILoginOIDC(t *testing.T) {
|
func TestCLILoginOIDC(t *testing.T) {
|
||||||
env := library.IntegrationEnv(t)
|
env := library.IntegrationEnv(t)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Find the login CSS selectors for the test issuer, or fail fast.
|
|
||||||
loginProvider := getLoginProvider(t)
|
|
||||||
|
|
||||||
// Start the browser driver.
|
// Start the browser driver.
|
||||||
t.Logf("opening browser driver")
|
page := browsertest.Open(t)
|
||||||
caps := agouti.NewCapabilities()
|
|
||||||
if env.Proxy != "" {
|
|
||||||
t.Logf("configuring Chrome to use proxy %q", env.Proxy)
|
|
||||||
caps = caps.Proxy(agouti.ProxyConfig{
|
|
||||||
ProxyType: "manual",
|
|
||||||
HTTPProxy: env.Proxy,
|
|
||||||
SSLProxy: env.Proxy,
|
|
||||||
NoProxy: "127.0.0.1",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
agoutiDriver := agouti.ChromeDriver(
|
|
||||||
agouti.Desired(caps),
|
|
||||||
agouti.ChromeOptions("args", []string{
|
|
||||||
"--no-sandbox",
|
|
||||||
"--ignore-certificate-errors",
|
|
||||||
"--headless", // Comment out this line to see the tests happen in a visible browser window.
|
|
||||||
}),
|
|
||||||
// Uncomment this to see stdout/stderr from chromedriver.
|
|
||||||
// agouti.Debug,
|
|
||||||
)
|
|
||||||
require.NoError(t, agoutiDriver.Start())
|
|
||||||
t.Cleanup(func() { require.NoError(t, agoutiDriver.Stop()) })
|
|
||||||
page, err := agoutiDriver.NewPage(agouti.Browser("chrome"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, page.Reset())
|
|
||||||
|
|
||||||
// Build pinniped CLI.
|
// Build pinniped CLI.
|
||||||
t.Logf("building CLI binary")
|
t.Logf("building CLI binary")
|
||||||
@ -261,28 +195,18 @@ func TestCLILoginOIDC(t *testing.T) {
|
|||||||
t.Logf("navigating to login page")
|
t.Logf("navigating to login page")
|
||||||
require.NoError(t, page.Navigate(loginURL))
|
require.NoError(t, page.Navigate(loginURL))
|
||||||
|
|
||||||
// Expect to be redirected to the login page.
|
// Expect to be redirected to the upstream provider and log in.
|
||||||
t.Logf("waiting for redirect to %s login page", loginProvider.Name)
|
browsertest.LoginToUpstream(t, page, env.CLITestUpstream)
|
||||||
waitForURL(t, page, loginProvider.LoginPagePattern)
|
|
||||||
|
|
||||||
// Wait for the login page to be rendered.
|
// Expect to be redirected to the localhost callback.
|
||||||
waitForVisibleElements(t, page, loginProvider.UsernameSelector, loginProvider.PasswordSelector, loginProvider.LoginButtonSelector)
|
t.Logf("waiting for redirect to callback")
|
||||||
|
|
||||||
// Fill in the username and password and click "submit".
|
|
||||||
t.Logf("logging into %s", loginProvider.Name)
|
|
||||||
require.NoError(t, page.First(loginProvider.UsernameSelector).Fill(env.CLITestUpstream.Username))
|
|
||||||
require.NoError(t, page.First(loginProvider.PasswordSelector).Fill(env.CLITestUpstream.Password))
|
|
||||||
require.NoError(t, page.First(loginProvider.LoginButtonSelector).Click())
|
|
||||||
|
|
||||||
// Wait for the login to happen and us be redirected back to a localhost callback.
|
|
||||||
t.Logf("waiting for redirect to localhost callback")
|
|
||||||
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.CLITestUpstream.CallbackURL) + `\?.+\z`)
|
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.CLITestUpstream.CallbackURL) + `\?.+\z`)
|
||||||
waitForURL(t, page, callbackURLPattern)
|
browsertest.WaitForURL(t, page, callbackURLPattern)
|
||||||
|
|
||||||
// Wait for the "pre" element that gets rendered for a `text/plain` page, and
|
// Wait for the "pre" element that gets rendered for a `text/plain` page, and
|
||||||
// assert that it contains the success message.
|
// assert that it contains the success message.
|
||||||
t.Logf("verifying success page")
|
t.Logf("verifying success page")
|
||||||
waitForVisibleElements(t, page, "pre")
|
browsertest.WaitForVisibleElements(t, page, "pre")
|
||||||
msg, err := page.First("pre").Text()
|
msg, err := page.First("pre").Text()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "you have been logged in and may now close this tab", msg)
|
require.Equal(t, "you have been logged in and may now close this tab", msg)
|
||||||
@ -360,44 +284,6 @@ func TestCLILoginOIDC(t *testing.T) {
|
|||||||
require.NotEqual(t, credOutput2.Status.Token, credOutput3.Status.Token)
|
require.NotEqual(t, credOutput2.Status.Token, credOutput3.Status.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForVisibleElements(t *testing.T, page *agouti.Page, selectors ...string) {
|
|
||||||
t.Helper()
|
|
||||||
require.Eventually(t,
|
|
||||||
func() bool {
|
|
||||||
for _, sel := range selectors {
|
|
||||||
vis, err := page.First(sel).Visible()
|
|
||||||
if !(err == nil && vis) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
10*time.Second,
|
|
||||||
100*time.Millisecond,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForURL(t *testing.T, page *agouti.Page, pat *regexp.Regexp) {
|
|
||||||
var lastURL string
|
|
||||||
require.Eventuallyf(t,
|
|
||||||
func() bool {
|
|
||||||
url, err := page.URL()
|
|
||||||
if err == nil && pat.MatchString(url) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if url != lastURL {
|
|
||||||
t.Logf("saw URL %s", url)
|
|
||||||
lastURL = url
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
10*time.Second,
|
|
||||||
100*time.Millisecond,
|
|
||||||
"expected to browse to %s, but never got there",
|
|
||||||
pat,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readAndExpectEmpty(r io.Reader) (err error) {
|
func readAndExpectEmpty(r io.Reader) (err error) {
|
||||||
var remainder bytes.Buffer
|
var remainder bytes.Buffer
|
||||||
_, err = io.Copy(&remainder, r)
|
_, err = io.Copy(&remainder, r)
|
||||||
|
150
test/library/browsertest/browsertest.go
Normal file
150
test/library/browsertest/browsertest.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package browsertest provides integration test helpers for our browser-based tests.
|
||||||
|
package browsertest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sclevine/agouti"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.pinniped.dev/test/library"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open a webdriver-driven browser and returns an *agouti.Page to control it. The browser will be automatically
|
||||||
|
// closed at the end of the current test. It is configured for test purposes with the correct HTTP proxy and
|
||||||
|
// in a mode that ignore certificate errors.
|
||||||
|
func Open(t *testing.T) *agouti.Page {
|
||||||
|
t.Logf("opening browser driver")
|
||||||
|
env := library.IntegrationEnv(t)
|
||||||
|
caps := agouti.NewCapabilities()
|
||||||
|
if env.Proxy != "" {
|
||||||
|
t.Logf("configuring Chrome to use proxy %q", env.Proxy)
|
||||||
|
caps = caps.Proxy(agouti.ProxyConfig{
|
||||||
|
ProxyType: "manual",
|
||||||
|
HTTPProxy: env.Proxy,
|
||||||
|
SSLProxy: env.Proxy,
|
||||||
|
NoProxy: "127.0.0.1",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
agoutiDriver := agouti.ChromeDriver(
|
||||||
|
agouti.Desired(caps),
|
||||||
|
agouti.ChromeOptions("args", []string{
|
||||||
|
"--no-sandbox",
|
||||||
|
"--ignore-certificate-errors",
|
||||||
|
"--headless", // Comment out this line to see the tests happen in a visible browser window.
|
||||||
|
}),
|
||||||
|
// Uncomment this to see stdout/stderr from chromedriver.
|
||||||
|
// agouti.Debug,
|
||||||
|
)
|
||||||
|
require.NoError(t, agoutiDriver.Start())
|
||||||
|
t.Cleanup(func() { require.NoError(t, agoutiDriver.Stop()) })
|
||||||
|
page, err := agoutiDriver.NewPage(agouti.Browser("chrome"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, page.Reset())
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForVisibleElements expects the page to contain all the the elements specified by the selectors. It waits for this
|
||||||
|
// to occur and times out, failing the test, if they never appear.
|
||||||
|
func WaitForVisibleElements(t *testing.T, page *agouti.Page, selectors ...string) {
|
||||||
|
t.Helper()
|
||||||
|
require.Eventually(t,
|
||||||
|
func() bool {
|
||||||
|
for _, sel := range selectors {
|
||||||
|
vis, err := page.First(sel).Visible()
|
||||||
|
if !(err == nil && vis) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
10*time.Second,
|
||||||
|
100*time.Millisecond,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForURL expects the page to eventually navigate to a URL matching the specified pattern. It waits for this
|
||||||
|
// to occur and times out, failing the test, if it never does.
|
||||||
|
func WaitForURL(t *testing.T, page *agouti.Page, pat *regexp.Regexp) {
|
||||||
|
var lastURL string
|
||||||
|
require.Eventuallyf(t,
|
||||||
|
func() bool {
|
||||||
|
url, err := page.URL()
|
||||||
|
if err == nil && pat.MatchString(url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if url != lastURL {
|
||||||
|
t.Logf("saw URL %s", url)
|
||||||
|
lastURL = url
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
10*time.Second,
|
||||||
|
100*time.Millisecond,
|
||||||
|
"expected to browse to %s, but never got there",
|
||||||
|
pat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginToUpstream expects the page to be redirected to one of several known upstream IDPs.
|
||||||
|
// It knows how to enter the test username/password and submit the upstream login form.
|
||||||
|
func LoginToUpstream(t *testing.T, page *agouti.Page, upstream library.TestOIDCUpstream) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Name string
|
||||||
|
IssuerPattern *regexp.Regexp
|
||||||
|
LoginPagePattern *regexp.Regexp
|
||||||
|
UsernameSelector string
|
||||||
|
PasswordSelector string
|
||||||
|
LoginButtonSelector string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the provider by matching on the issuer URL.
|
||||||
|
var cfg *config
|
||||||
|
for _, p := range []*config{
|
||||||
|
{
|
||||||
|
Name: "Okta",
|
||||||
|
IssuerPattern: regexp.MustCompile(`\Ahttps://.+\.okta\.com/.+\z`),
|
||||||
|
LoginPagePattern: regexp.MustCompile(`\Ahttps://.+\.okta\.com/.+\z`),
|
||||||
|
UsernameSelector: "input#okta-signin-username",
|
||||||
|
PasswordSelector: "input#okta-signin-password",
|
||||||
|
LoginButtonSelector: "input#okta-signin-submit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Dex",
|
||||||
|
IssuerPattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex.*\z`),
|
||||||
|
LoginPagePattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex/auth/local.+\z`),
|
||||||
|
UsernameSelector: "input#login",
|
||||||
|
PasswordSelector: "input#password",
|
||||||
|
LoginButtonSelector: "button#submit-login",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if p.IssuerPattern.MatchString(upstream.Issuer) {
|
||||||
|
cfg = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
require.Failf(t, "could not find login provider for issuer %q", upstream.Issuer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect to be redirected to the login page.
|
||||||
|
t.Logf("waiting for redirect to %s login page", cfg.Name)
|
||||||
|
WaitForURL(t, page, cfg.LoginPagePattern)
|
||||||
|
|
||||||
|
// Wait for the login page to be rendered.
|
||||||
|
WaitForVisibleElements(t, page, cfg.UsernameSelector, cfg.PasswordSelector, cfg.LoginButtonSelector)
|
||||||
|
|
||||||
|
// Fill in the username and password and click "submit".
|
||||||
|
t.Logf("logging into %s", cfg.Name)
|
||||||
|
require.NoError(t, page.First(cfg.UsernameSelector).Fill(upstream.Username))
|
||||||
|
require.NoError(t, page.First(cfg.PasswordSelector).Fill(upstream.Password))
|
||||||
|
require.NoError(t, page.First(cfg.LoginButtonSelector).Click())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user