mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-01 05:37:20 +00:00
Merge branch 'main' into deflake-serving-certificate-rotation-test
This commit is contained in:
@@ -307,16 +307,15 @@ func runPinnipedLoginOIDC(
|
||||
reader := bufio.NewReader(testlib.NewLoggerReader(t, "stderr", stderr))
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
const prompt = "Please log in: "
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, prompt) {
|
||||
loginURLChan <- strings.TrimPrefix(line, prompt)
|
||||
loginURL, err := url.Parse(strings.TrimSpace(scanner.Text()))
|
||||
if err == nil && loginURL.Scheme == "https" {
|
||||
loginURLChan <- loginURL.String()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("expected stderr to contain %s", prompt)
|
||||
return fmt.Errorf("expected stderr to contain login URL")
|
||||
})
|
||||
|
||||
// Start a background goroutine to read stdout from the CLI and parse out an ExecCredential.
|
||||
@@ -356,7 +355,7 @@ func runPinnipedLoginOIDC(
|
||||
|
||||
// Expect to be redirected to the localhost callback.
|
||||
t.Logf("waiting for redirect to callback")
|
||||
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.CLIUpstreamOIDC.CallbackURL) + `\?.+\z`)
|
||||
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.CLIUpstreamOIDC.CallbackURL) + `(\?.+)?\z`)
|
||||
browsertest.WaitForURL(t, page, callbackURLPattern)
|
||||
|
||||
// Wait for the "pre" element that gets rendered for a `text/plain` page, and
|
||||
|
||||
@@ -1088,17 +1088,22 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
||||
return proxyURL, nil
|
||||
}
|
||||
}
|
||||
c, r, err := dialer.Dial(dest.String(), http.Header{"Origin": {dest.String()}})
|
||||
if r != nil {
|
||||
defer func() {
|
||||
require.NoError(t, r.Body.Close())
|
||||
}()
|
||||
}
|
||||
if err != nil && r != nil {
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
t.Logf("websocket dial failed: %d:%s", r.StatusCode, body)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
var (
|
||||
resp *http.Response
|
||||
conn *websocket.Conn
|
||||
)
|
||||
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
||||
var err error
|
||||
conn, resp, err = dialer.Dial(dest.String(), http.Header{"Origin": {dest.String()}})
|
||||
if resp != nil {
|
||||
defer func() { requireEventually.NoError(resp.Body.Close()) }()
|
||||
}
|
||||
if err != nil && resp != nil {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
t.Logf("websocket dial failed: %d:%s", resp.StatusCode, body)
|
||||
}
|
||||
requireEventually.NoError(err)
|
||||
}, time.Minute, time.Second)
|
||||
|
||||
// perform a create through the admin client
|
||||
wantConfigMap := &corev1.ConfigMap{
|
||||
@@ -1115,7 +1120,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
||||
})
|
||||
|
||||
// see if the websocket client received an event for the create
|
||||
_, message, err := c.ReadMessage()
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
+124
-21
@@ -109,7 +109,7 @@ func TestE2EFullIntegration(t *testing.T) {
|
||||
})
|
||||
|
||||
// Add an OIDC upstream IDP and try using it to authenticate during kubectl commands.
|
||||
t.Run("with Supervisor OIDC upstream IDP", func(t *testing.T) {
|
||||
t.Run("with Supervisor OIDC upstream IDP and automatic flow", func(t *testing.T) {
|
||||
expectedUsername := env.SupervisorUpstreamOIDC.Username
|
||||
expectedGroups := env.SupervisorUpstreamOIDC.ExpectedGroups
|
||||
|
||||
@@ -195,16 +195,15 @@ func TestE2EFullIntegration(t *testing.T) {
|
||||
}()
|
||||
|
||||
reader := bufio.NewReader(testlib.NewLoggerReader(t, "stderr", stderrPipe))
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read login URL line from stderr: %w", err)
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
loginURL, err := url.Parse(strings.TrimSpace(scanner.Text()))
|
||||
if err == nil && loginURL.Scheme == "https" {
|
||||
loginURLChan <- loginURL.String()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
const prompt = "Please log in: "
|
||||
if !strings.HasPrefix(line, prompt) {
|
||||
return fmt.Errorf("expected %q to have prefix %q", line, prompt)
|
||||
}
|
||||
loginURLChan <- strings.TrimPrefix(line, prompt)
|
||||
return readAndExpectEmpty(reader)
|
||||
return fmt.Errorf("expected stderr to contain login URL")
|
||||
})
|
||||
|
||||
// Start a background goroutine to read stdout from kubectl and return the result as a string.
|
||||
@@ -242,17 +241,13 @@ func TestE2EFullIntegration(t *testing.T) {
|
||||
// Expect to be redirected to the upstream provider and log in.
|
||||
browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC)
|
||||
|
||||
// Expect to be redirected to the localhost callback.
|
||||
t.Logf("waiting for redirect to callback")
|
||||
browsertest.WaitForURL(t, page, regexp.MustCompile(`\Ahttp://127\.0\.0\.1:[0-9]+/callback\?.+\z`))
|
||||
// Expect to be redirected to the downstream callback which is serving the form_post HTML.
|
||||
t.Logf("waiting for response page %s", downstream.Spec.Issuer)
|
||||
browsertest.WaitForURL(t, page, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer)))
|
||||
|
||||
// Wait for the "pre" element that gets rendered for a `text/plain` page, and
|
||||
// assert that it contains the success message.
|
||||
t.Logf("verifying success page")
|
||||
browsertest.WaitForVisibleElements(t, page, "pre")
|
||||
msg, err := page.First("pre").Text()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "you have been logged in and may now close this tab", msg)
|
||||
// The response page should have done the background fetch() and POST'ed to the CLI's callback.
|
||||
// It should now be in the "success" state.
|
||||
formpostExpectSuccessState(t, page)
|
||||
|
||||
// Expect the CLI to output a list of namespaces in JSON format.
|
||||
t.Logf("waiting for kubectl to output namespace list JSON")
|
||||
@@ -275,6 +270,113 @@ func TestE2EFullIntegration(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("with Supervisor OIDC upstream IDP and manual flow", func(t *testing.T) {
|
||||
expectedUsername := env.SupervisorUpstreamOIDC.Username
|
||||
expectedGroups := env.SupervisorUpstreamOIDC.ExpectedGroups
|
||||
|
||||
// Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster.
|
||||
testlib.CreateTestClusterRoleBinding(t,
|
||||
rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: expectedUsername},
|
||||
rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"},
|
||||
)
|
||||
testlib.WaitForUserToHaveAccess(t, expectedUsername, []string{}, &authorizationv1.ResourceAttributes{
|
||||
Verb: "get",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Resource: "namespaces",
|
||||
})
|
||||
|
||||
// Create upstream OIDC provider and wait for it to become ready.
|
||||
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||
},
|
||||
AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{
|
||||
AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes,
|
||||
},
|
||||
Claims: idpv1alpha1.OIDCClaims{
|
||||
Username: env.SupervisorUpstreamOIDC.UsernameClaim,
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/oidc-test-sessions-manual.yaml"
|
||||
kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{
|
||||
"get", "kubeconfig",
|
||||
"--concierge-api-group-suffix", env.APIGroupSuffix,
|
||||
"--concierge-authenticator-type", "jwt",
|
||||
"--concierge-authenticator-name", authenticator.Name,
|
||||
"--oidc-skip-browser",
|
||||
"--oidc-skip-listen",
|
||||
"--oidc-ca-bundle", testCABundlePath,
|
||||
"--oidc-session-cache", sessionCachePath,
|
||||
})
|
||||
|
||||
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
||||
start := time.Now()
|
||||
kubectlCmd := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath)
|
||||
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
|
||||
|
||||
ptyFile, err := pty.Start(kubectlCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the subprocess to print the login prompt.
|
||||
t.Logf("waiting for CLI to output login URL and manual prompt")
|
||||
output := readFromFileUntilStringIsSeen(t, ptyFile, "If automatic login fails, paste your authorization code to login manually: ")
|
||||
require.Contains(t, output, "Log in by visiting this link:")
|
||||
require.Contains(t, output, "If automatic login fails, paste your authorization code to login manually: ")
|
||||
|
||||
// Find the line with the login URL.
|
||||
var loginURL string
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "https://") {
|
||||
loginURL = trimmed
|
||||
}
|
||||
}
|
||||
require.NotEmptyf(t, loginURL, "didn't find login URL in output: %s", output)
|
||||
|
||||
t.Logf("navigating to login page")
|
||||
require.NoError(t, page.Navigate(loginURL))
|
||||
|
||||
// Expect to be redirected to the upstream provider and log in.
|
||||
browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC)
|
||||
|
||||
// Expect to be redirected to the downstream callback which is serving the form_post HTML.
|
||||
t.Logf("waiting for response page %s", downstream.Spec.Issuer)
|
||||
browsertest.WaitForURL(t, page, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer)))
|
||||
|
||||
// The response page should have failed to automatically post, and should now be showing the manual instructions.
|
||||
authCode := formpostExpectManualState(t, page)
|
||||
|
||||
// Enter the auth code in the waiting prompt, followed by a newline.
|
||||
t.Logf("'manually' pasting authorization code %q to waiting prompt", authCode)
|
||||
_, err = ptyFile.WriteString(authCode + "\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read all of the remaining output from the subprocess until EOF.
|
||||
t.Logf("waiting for kubectl to output namespace list")
|
||||
remainingOutput, _ := ioutil.ReadAll(ptyFile)
|
||||
// Ignore any errors returned because there is always an error on linux.
|
||||
require.Greaterf(t, len(remainingOutput), 0, "expected to get some more output from the kubectl subcommand, but did not")
|
||||
require.Greaterf(t, len(strings.Split(string(remainingOutput), "\n")), 2, "expected some namespaces to be returned, got %q", string(remainingOutput))
|
||||
t.Logf("first kubectl command took %s", time.Since(start).String())
|
||||
|
||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env,
|
||||
downstream,
|
||||
kubeconfigPath,
|
||||
sessionCachePath,
|
||||
pinnipedExe,
|
||||
expectedUsername,
|
||||
expectedGroups,
|
||||
)
|
||||
})
|
||||
|
||||
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands.
|
||||
t.Run("with Supervisor LDAP upstream IDP", func(t *testing.T) {
|
||||
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||
@@ -376,7 +478,7 @@ func TestE2EFullIntegration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func readFromFileUntilStringIsSeen(t *testing.T, f *os.File, until string) {
|
||||
func readFromFileUntilStringIsSeen(t *testing.T, f *os.File, until string) string {
|
||||
readFromFile := ""
|
||||
|
||||
testlib.RequireEventuallyWithoutError(t, func() (bool, error) {
|
||||
@@ -390,6 +492,7 @@ func readFromFileUntilStringIsSeen(t *testing.T, f *os.File, until string) {
|
||||
}
|
||||
return false, nil // keep waiting and reading
|
||||
}, 1*time.Minute, 1*time.Second)
|
||||
return readFromFile
|
||||
}
|
||||
|
||||
func readAvailableOutput(t *testing.T, r io.Reader) (string, bool) {
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/token/hmac"
|
||||
"github.com/sclevine/agouti"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/httputil/securityheader"
|
||||
"go.pinniped.dev/internal/oidc/provider/formposthtml"
|
||||
"go.pinniped.dev/test/testlib"
|
||||
"go.pinniped.dev/test/testlib/browsertest"
|
||||
)
|
||||
|
||||
func TestFormPostHTML(t *testing.T) {
|
||||
// Run a mock callback handler, simulating the one running in the CLI.
|
||||
callbackURL, expectCallback := formpostCallbackServer(t)
|
||||
|
||||
// Open a single browser for all subtests to use (in sequence).
|
||||
page := browsertest.Open(t)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
// Serve the form_post template with successful parameters.
|
||||
responseParams := formpostRandomParams(t)
|
||||
formpostInitiate(t, page, formpostTemplateServer(t, callbackURL, responseParams))
|
||||
|
||||
// Now we handle the callback and assert that we got what we expected. This should transition
|
||||
// the UI into the success state.
|
||||
expectCallback(t, responseParams)
|
||||
formpostExpectSuccessState(t, page)
|
||||
})
|
||||
|
||||
t.Run("callback server error", func(t *testing.T) {
|
||||
// Serve the form_post template with a redirect URI that will return an HTTP 500 response.
|
||||
responseParams := formpostRandomParams(t)
|
||||
formpostInitiate(t, page, formpostTemplateServer(t, callbackURL+"?fail=500", responseParams))
|
||||
|
||||
// Now we handle the callback and assert that we got what we expected.
|
||||
expectCallback(t, responseParams)
|
||||
|
||||
// This is not 100% the behavior we'd like, but because our JS is making
|
||||
// a cross-origin fetch() without CORS, we don't get to know anything
|
||||
// about the response (even whether it is 200 vs. 500), so this case
|
||||
// is the same as the success case.
|
||||
//
|
||||
// This case is fairly unlikely in practice, and if the CLI encounters
|
||||
// an error it can also expose it via stderr anyway.
|
||||
formpostExpectSuccessState(t, page)
|
||||
})
|
||||
|
||||
t.Run("network failure", func(t *testing.T) {
|
||||
// Serve the form_post template with a redirect URI that will return a network error.
|
||||
responseParams := formpostRandomParams(t)
|
||||
formpostInitiate(t, page, formpostTemplateServer(t, callbackURL+"?fail=close", responseParams))
|
||||
|
||||
// Now we handle the callback and assert that we got what we expected.
|
||||
// This will trigger the callback server to close the client connection abruptly because
|
||||
// of the `?fail=close` parameter above.
|
||||
expectCallback(t, responseParams)
|
||||
|
||||
// This failure should cause the UI to enter the "manual" state.
|
||||
actualCode := formpostExpectManualState(t, page)
|
||||
require.Equal(t, responseParams.Get("code"), actualCode)
|
||||
})
|
||||
|
||||
t.Run("timeout", func(t *testing.T) {
|
||||
// Serve the form_post template with successful parameters.
|
||||
responseParams := formpostRandomParams(t)
|
||||
formpostInitiate(t, page, formpostTemplateServer(t, callbackURL, responseParams))
|
||||
|
||||
// Sleep for longer than the two second timeout.
|
||||
// During this sleep we are blocking the callback from returning.
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Assert that the timeout fires and we see the manual instructions.
|
||||
actualCode := formpostExpectManualState(t, page)
|
||||
require.Equal(t, responseParams.Get("code"), actualCode)
|
||||
|
||||
// Now simulate the callback finally succeeding, in which case
|
||||
// the manual instructions should disappear and we should see the success
|
||||
// div instead.
|
||||
expectCallback(t, responseParams)
|
||||
formpostExpectSuccessState(t, page)
|
||||
})
|
||||
}
|
||||
|
||||
// formpostCallbackServer runs a test server that simulates the CLI's callback handler.
|
||||
// It returns the URL of the running test server and a function for fetching the next
|
||||
// received form POST parameters.
|
||||
//
|
||||
// The test server supports special `?fail=close` and `?fail=500` to force error cases.
|
||||
func formpostCallbackServer(t *testing.T) (string, func(*testing.T, url.Values)) {
|
||||
results := make(chan url.Values)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.NoError(t, r.ParseForm())
|
||||
|
||||
// Extract only the POST parameters (r.Form also contains URL query parameters).
|
||||
postParams := url.Values{}
|
||||
for k := range r.Form {
|
||||
if v := r.PostFormValue(k); v != "" {
|
||||
postParams.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Send the form parameters back on the results channel, giving up if the
|
||||
// request context is cancelled (such as if the client disconnects).
|
||||
select {
|
||||
case results <- postParams:
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
}
|
||||
|
||||
switch r.URL.Query().Get("fail") {
|
||||
case "close": // If "fail=close" is passed, close the connection immediately.
|
||||
if conn, _, err := w.(http.Hijacker).Hijack(); err == nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
return
|
||||
case "500": // If "fail=500" is passed, return a 500 error.
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}))
|
||||
t.Cleanup(func() {
|
||||
close(results)
|
||||
server.Close()
|
||||
})
|
||||
return server.URL, func(t *testing.T, expected url.Values) {
|
||||
t.Logf("expecting to get a POST callback...")
|
||||
select {
|
||||
case actual := <-results:
|
||||
require.Equal(t, expected, actual, "did not receive expected callback")
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Errorf("failed to receive expected callback %v", expected)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// formpostTemplateServer runs a test server that serves formposthtml.Template() rendered with test parameters.
|
||||
func formpostTemplateServer(t *testing.T, redirectURI string, responseParams url.Values) string {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fosite.WriteAuthorizeFormPostResponse(redirectURI, responseParams, formposthtml.Template(), w)
|
||||
})
|
||||
server := httptest.NewServer(securityheader.WrapWithCustomCSP(
|
||||
handler,
|
||||
formposthtml.ContentSecurityPolicy(),
|
||||
))
|
||||
t.Cleanup(server.Close)
|
||||
return server.URL
|
||||
}
|
||||
|
||||
// formpostRandomParams is a helper to generate random OAuth2 response parameters for testing.
|
||||
func formpostRandomParams(t *testing.T) url.Values {
|
||||
generator := &hmac.HMACStrategy{GlobalSecret: testlib.RandBytes(t, 32), TokenEntropy: 32}
|
||||
authCode, _, err := generator.Generate()
|
||||
require.NoError(t, err)
|
||||
return url.Values{
|
||||
"code": []string{authCode},
|
||||
"scope": []string{"openid offline_access pinniped:request-audience"},
|
||||
"state": []string{testlib.RandHex(t, 16)},
|
||||
}
|
||||
}
|
||||
|
||||
// formpostExpectTitle asserts that the page has the expected title.
|
||||
func formpostExpectTitle(t *testing.T, page *agouti.Page, expected string) {
|
||||
actual, err := page.Title()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
// formpostExpectTitle asserts that the page has the expected SVG/emoji favicon.
|
||||
func formpostExpectFavicon(t *testing.T, page *agouti.Page, expected string) {
|
||||
iconURL, err := page.First("#favicon").Attribute("href")
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasPrefix(iconURL, "data:image/svg+xml,<svg"))
|
||||
|
||||
// For some reason chromedriver on Linux returns this attribute urlencoded, but on macOS it contains the
|
||||
// original emoji bytes (unescaped). To check correctly in both cases we allow either version here.
|
||||
expectedEscaped := url.QueryEscape(expected)
|
||||
require.Truef(t,
|
||||
strings.Contains(iconURL, expected) || strings.Contains(iconURL, expectedEscaped),
|
||||
"expected %q to contain %q or %q", iconURL, expected, expectedEscaped,
|
||||
)
|
||||
}
|
||||
|
||||
// formpostInitiate navigates to the template server endpoint and expects the
|
||||
// loading animation to be shown.
|
||||
func formpostInitiate(t *testing.T, page *agouti.Page, url string) {
|
||||
require.NoError(t, page.Reset())
|
||||
t.Logf("navigating to mock form_post template URL %s...", url)
|
||||
require.NoError(t, page.Navigate(url))
|
||||
|
||||
t.Logf("expecting to see loading animation...")
|
||||
browsertest.WaitForVisibleElements(t, page, "#loading")
|
||||
formpostExpectTitle(t, page, "Logging in...")
|
||||
formpostExpectFavicon(t, page, "⏳")
|
||||
}
|
||||
|
||||
// formpostExpectSuccessState asserts that the page is in the "success" state.
|
||||
func formpostExpectSuccessState(t *testing.T, page *agouti.Page) {
|
||||
t.Logf("expecting to see success message become visible...")
|
||||
browsertest.WaitForVisibleElements(t, page, "#success")
|
||||
successDivText, err := page.First("#success").Text()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, successDivText, "Login succeeded")
|
||||
require.Contains(t, successDivText, "You have successfully logged in. You may now close this tab.")
|
||||
formpostExpectTitle(t, page, "Login succeeded")
|
||||
formpostExpectFavicon(t, page, "✅")
|
||||
}
|
||||
|
||||
// formpostExpectManualState asserts that the page is in the "manual" state and returns the auth code.
|
||||
func formpostExpectManualState(t *testing.T, page *agouti.Page) string {
|
||||
t.Logf("expecting to see manual message become visible...")
|
||||
browsertest.WaitForVisibleElements(t, page, "#manual")
|
||||
manualDivText, err := page.First("#manual").Text()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, manualDivText, "Finish your login")
|
||||
require.Contains(t, manualDivText, "To finish logging in, paste this authorization code into your command-line session:")
|
||||
formpostExpectTitle(t, page, "Finish your login")
|
||||
formpostExpectFavicon(t, page, "⌛")
|
||||
|
||||
// Click the copy button and expect that the code is copied to the clipboard. Unfortunately,
|
||||
// headless Chrome does not have a real clipboard we can check, so we rely on checking a
|
||||
// console.log() statement that happens at the same time.
|
||||
t.Logf("clicking the 'copy' button and expecting the clipboard event to fire...")
|
||||
require.NoError(t, page.First("#manual-copy-button").Click())
|
||||
|
||||
var authCode string
|
||||
consoleLogPattern := regexp.MustCompile(`code (.+) to clipboard`)
|
||||
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
||||
logs, err := page.ReadNewLogs("browser")
|
||||
requireEventually.NoError(err)
|
||||
|
||||
for _, log := range logs {
|
||||
if match := consoleLogPattern.FindStringSubmatch(log.Message); match != nil {
|
||||
authCode = match[1]
|
||||
return
|
||||
}
|
||||
}
|
||||
requireEventually.FailNow("expected console log was not found")
|
||||
}, 3*time.Second, 100*time.Millisecond)
|
||||
return authCode
|
||||
}
|
||||
@@ -479,6 +479,7 @@ func requireWellKnownEndpointIsWorking(t *testing.T, supervisorScheme, superviso
|
||||
"jwks_uri": "%s/jwks.json",
|
||||
"scopes_supported": ["openid", "offline"],
|
||||
"response_types_supported": ["code"],
|
||||
"response_modes_supported": ["query", "form_post"],
|
||||
"claims_supported": ["groups"],
|
||||
"discovery.supervisor.pinniped.dev/v1alpha1": {"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"},
|
||||
"subject_types_supported": ["public"],
|
||||
|
||||
@@ -65,6 +65,7 @@ func RequireEventuallyf(
|
||||
msg string,
|
||||
args ...interface{},
|
||||
) {
|
||||
t.Helper()
|
||||
RequireEventually(t, f, waitFor, tick, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ func Open(t *testing.T) *agouti.Page {
|
||||
t.Logf("opening browser driver")
|
||||
env := testlib.IntegrationEnv(t)
|
||||
caps := agouti.NewCapabilities()
|
||||
|
||||
// Capture console.log(), not just console.error().
|
||||
caps["loggingPrefs"] = map[string]string{"browser": "INFO"}
|
||||
|
||||
if env.Proxy != "" {
|
||||
t.Logf("configuring Chrome to use proxy %q", env.Proxy)
|
||||
caps = caps.Proxy(agouti.ProxyConfig{
|
||||
|
||||
@@ -311,11 +311,15 @@ func CreateTestFederationDomain(ctx context.Context, t *testing.T, issuer string
|
||||
return federationDomain
|
||||
}
|
||||
|
||||
func RandHex(t *testing.T, numBytes int) string {
|
||||
func RandBytes(t *testing.T, numBytes int) []byte {
|
||||
buf := make([]byte, numBytes)
|
||||
_, err := io.ReadFull(rand.Reader, buf)
|
||||
require.NoError(t, err)
|
||||
return hex.EncodeToString(buf)
|
||||
return buf
|
||||
}
|
||||
|
||||
func RandHex(t *testing.T, numBytes int) string {
|
||||
return hex.EncodeToString(RandBytes(t, numBytes))
|
||||
}
|
||||
|
||||
func CreateTestSecret(t *testing.T, namespace string, baseName string, secretType corev1.SecretType, stringData map[string]string) *corev1.Secret {
|
||||
|
||||
Reference in New Issue
Block a user