Merge branch 'main' into jtc/add-importas-linter

This commit is contained in:
Joshua Casey
2024-06-11 09:39:48 -05:00
357 changed files with 23832 additions and 1310 deletions
+193
View File
@@ -5,7 +5,10 @@
package browsertest
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"fmt"
"log"
"regexp"
@@ -16,10 +19,12 @@ import (
"time"
chromedpbrowser "github.com/chromedp/cdproto/browser"
"github.com/chromedp/cdproto/dom"
chromedpruntime "github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"github.com/stretchr/testify/require"
"go.pinniped.dev/internal/testutil/totp"
"go.pinniped.dev/test/testlib"
)
@@ -162,12 +167,52 @@ func OpenBrowser(t *testing.T) *Browser {
for _, e := range b.exceptionEvents {
t.Logf("exception: %s", e)
}
// If the test failed, dump helpful debugging info from the browser's final page.
if t.Failed() {
b.dumpPage(t)
}
})
// Done. The browser is ready to be driven by the test.
return b
}
func (b *Browser) dumpPage(t *testing.T) {
// Log the URL of the current page.
var url string
b.runWithTimeout(t, b.timeout(), chromedp.Location(&url))
t.Logf("Browser URL from end of test %q: %s", t.Name(), url)
// Log the title of the current page.
t.Logf("Browser page title from end of test %q: %q", t.Name(), b.Title(t))
// Log a screenshot of the current page.
var screenBuf []byte
b.runWithTimeout(t, b.timeout(), chromedp.FullScreenshot(&screenBuf, 10)) // low quality to make it smaller
t.Logf("Browser screenshot (base64 encoded jpeg format) from end of test %q:\n%s\n",
t.Name(), base64.StdEncoding.EncodeToString(screenBuf))
// Log the HTML of the current page.
var html string
b.runWithTimeout(t, b.timeout(), chromedp.ActionFunc(func(ctx context.Context) error {
node, err := dom.GetDocument().Do(ctx)
if err != nil {
return err
}
html, err = dom.GetOuterHTML().WithNodeID(node.NodeID).Do(ctx)
return err
}))
var htmlBuf bytes.Buffer
gz := gzip.NewWriter(&htmlBuf)
_, err := gz.Write([]byte(html))
require.NoError(t, err)
err = gz.Close()
require.NoError(t, err)
t.Logf("Browser html (gzip and base64 encoded) from end of test %q:\n%s\n",
t.Name(), base64.StdEncoding.EncodeToString(htmlBuf.Bytes()))
}
func (b *Browser) timeout() time.Duration {
return 30 * time.Second
}
@@ -357,6 +402,154 @@ func LoginToUpstreamOIDC(t *testing.T, b *Browser, upstream testlib.TestOIDCUpst
b.ClickFirstMatch(t, cfg.LoginButtonSelector)
}
// LoginToUpstreamGitHub expects the page to be redirected to GitHub.
// It knows how to enter the test username/password and submit the upstream login form.
func LoginToUpstreamGitHub(t *testing.T, b *Browser, upstream testlib.TestGithubUpstream) {
t.Helper()
// Expect to be redirected to the login page.
t.Logf("waiting for redirect to GitHub login page")
b.WaitForURL(t, regexp.MustCompile(`\Ahttps://github\.com/login.+\z`))
usernameSelector := "input#login_field"
passwordSelector := "input#password"
loginButtonSelector := "input[type=submit]"
// Wait for the login page to be rendered.
b.WaitForVisibleElements(t, usernameSelector, passwordSelector, loginButtonSelector)
// Fill in the username and password and click "submit".
t.Logf("logging into GitHub")
b.SendKeysToFirstMatch(t, usernameSelector, upstream.TestUserUsername)
b.SendKeysToFirstMatch(t, passwordSelector, upstream.TestUserPassword)
b.ClickFirstMatch(t, loginButtonSelector)
handleGithubOTPLoginPage(t, b, upstream)
// Keep looping until we get to a page that we do not know how to handle. Then return to allow the test to move on.
for handleOccasionalGithubLoginPage(t, b, upstream) {
continue
}
}
func handleGithubOTPLoginPage(t *testing.T, b *Browser, upstream testlib.TestGithubUpstream) {
// Next, GitHub should go to a new page and prompt for the six digit MFA/OTP code.
otpSelector := "input#app_totp"
// Wait for the MFA page to be rendered.
t.Logf("waiting for GitHub MFA page")
b.WaitForVisibleElements(t, otpSelector)
// Sleep for a bit to make it less likely that we use the same OTP code twice when multiple tests are run in serial.
// GitHub gets upset when the same OTP code gets reused.
// GitHub seems to also get upset when any OTP codes are used often, like when all our GitHub tests run sequentially,
// because sometimes auth will go to a GitHub page that says: "We were unable to authenticate your request because too
// many codes have been submitted. Please wait a few minutes and contact support if you continue to have problems."
otpSleepSeconds := 60
t.Logf("sleeping %d seconds before generating a GitHub OTP code", otpSleepSeconds)
time.Sleep(time.Duration(otpSleepSeconds) * time.Second)
code, codeRemainingLifetimeSeconds := totp.GenerateOTPCode(t, upstream.TestUserOTPSecret, time.Now())
if codeRemainingLifetimeSeconds < 2 {
t.Log("sleeping for 2 seconds before generating another OTP code")
time.Sleep(2 * time.Second)
code, _ = totp.GenerateOTPCode(t, upstream.TestUserOTPSecret, time.Now())
}
// Fill in the OTP code. We do not need to click "verify" because entering the code automatically submits the page.
t.Logf("entering GitHub OTP code")
b.SendKeysToFirstMatch(t, otpSelector, code)
}
// handleOccasionalGithubLoginPage handles the interstitial pages which GitHub might show during a login flow.
// None of these will always happen.
func handleOccasionalGithubLoginPage(t *testing.T, b *Browser, upstream testlib.TestGithubUpstream) bool {
t.Helper()
t.Log("sleeping for 2 seconds before looking at page title")
time.Sleep(2 * time.Second)
pageTitle := b.Title(t)
t.Logf("saw page title %q", pageTitle)
lowercaseTitle := strings.ToLower(pageTitle)
switch {
case strings.HasPrefix(lowercaseTitle, "authorize "): // the title is "Authorize <App Name>"
// Next GitHub might go to another page asking if you authorize the GitHub App to act on your behalf,
// if this user has never authorized this app.
// Wait for the authorize app page to be rendered.
t.Logf("waiting for GitHub authorize button")
// There are unfortunately two very similar buttons on this page:
// <button name="authorize" value="0" type="submit" data-view-component="true" class="ws-normal btn width-full mr-2">Cancel
// <button name="authorize" value="1" type="submit" data-view-component="true" class="js-oauth-authorize-btn ws-normal btn-primary btn width-full">Authorize
submitAuthorizeAppButtonSelector := "button.btn-primary"
b.WaitForVisibleElements(t, submitAuthorizeAppButtonSelector)
t.Logf("clicking authorize button")
b.ClickFirstMatch(t, submitAuthorizeAppButtonSelector)
return true
case strings.HasPrefix(lowercaseTitle, "confirm your account recovery settings"):
// Next GitHub might occasionally as you to confirm your recovery settings.
// Wait for the page to be rendered.
t.Logf("waiting for GitHub confirm button")
// There are several buttons and links. We want to click this confirm button to confirm our settings:
// <button type="submit" name="type" value="confirmed" class="btn btn-block btn-primary ml-3">Confirm</button>
submitConfirmButtonSelector := "button.btn-primary"
b.WaitForVisibleElements(t, submitConfirmButtonSelector)
t.Logf("clicking confirm button")
b.ClickFirstMatch(t, submitConfirmButtonSelector)
return true
case strings.HasPrefix(lowercaseTitle, "verify two-factor authentication"):
// Next GitHub might occasionally as you to confirm your MFA settings.
// Wait for the page to be rendered.
t.Logf("waiting for GitHub skip link")
// There are several buttons and links. We want to click this link to "skip 2FA verification":
// <button type="submit" data-view-component="true" class="Button--link Button--medium Button">
submitSkipButtonSelector := "button.Button--link[type=submit]"
b.WaitForVisibleElements(t, submitSkipButtonSelector)
t.Logf("clicking skip link")
b.ClickFirstMatch(t, submitSkipButtonSelector)
return true
case strings.HasPrefix(lowercaseTitle, "configure passwordless authentication"):
// Next GitHub might occasionally ask if we want to configure a passkey for auth.
// The URL bar shows https://github.com/sessions/trusted-device for this page.
// The link that we want to click looks like this:
// <input class="btn-link" type="submit" value="Don't ask again for this browser">
dontAskAgainLinkSelector := `input[value="Don't ask again for this browser"]`
// Wait for the passkey page to be rendered.
t.Logf("waiting for GitHub's don't ask again button")
b.WaitForVisibleElements(t, dontAskAgainLinkSelector)
// Tell it that we do not want to use a passkey.
t.Logf("clicking don't ask again button")
b.ClickFirstMatch(t, dontAskAgainLinkSelector)
return true
case strings.HasPrefix(lowercaseTitle, "two-factor authentication"):
// Sometimes this happens after the OTP page when we try to use the same OTP code again too quickly.
// GitHub stays on the same page and shows an error banner saying that we used the same code again.
// Sleep for a long time to try to avoid this error from GitHub, which seems to be some type of rate limiting on OTP codes:
// "We were unable to authenticate your request because too many codes have been submitted".
otpSleepSeconds := 60
t.Logf("sleeping %d seconds before generating another GitHub OTP code after a previous code failed", otpSleepSeconds)
time.Sleep(time.Duration(otpSleepSeconds) * time.Second)
handleGithubOTPLoginPage(t, b, upstream)
return true
case strings.HasPrefix(lowercaseTitle, "server error"):
// Sometimes this happens after the OTP page. Not sure why. The page has a cute cartoon, but no helpful information.
// The URL bar shows https://github.com/sessions/trusted-device for this error page, which is the URL that usually
// asks if you want to configure passwordless authentication (aka passkey).
t.Fatal("Got GitHub server internal error page during login flow. This is not expected, but is unfortunately unrecoverable.")
return false // we recognized the title, but we don't know how to handle this page because it has no buttons or other way forward
default:
// We did not know how to handle the page given its title.
// Maybe we successfully got through all the interstitial pages and finished the login.
return false
}
}
// LoginToUpstreamLDAP expects the page to be redirected to the Supervisor's login UI for an LDAP/AD IDP.
// It knows how to enter the test username/password and submit the upstream login form.
func LoginToUpstreamLDAP(t *testing.T, b *Browser, issuer, username, password string) {