mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-19 22:44:18 +00:00
first draft of an e2e integration test for GitHub login (skip while WIP)
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/testutil/totp"
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
|
||||
@@ -357,6 +358,85 @@ 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)
|
||||
|
||||
// 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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
// 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.
|
||||
if strings.HasPrefix(pageTitle, "Authorize ") { // the title is "Authorize <App Name>"
|
||||
// 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)
|
||||
|
||||
t.Log("sleeping for 2 seconds before looking at page title again")
|
||||
time.Sleep(2 * time.Second)
|
||||
pageTitle = b.Title(t)
|
||||
t.Logf("saw page title %q", pageTitle)
|
||||
}
|
||||
|
||||
// TODO I only saw this happen once, so I did not get a chance to finish this code. Not sure if it will happen again?
|
||||
// Next GitHub might ask if we want to configure a passkey for auth.
|
||||
if strings.HasPrefix(pageTitle, "Passkey TODO GET THIS PAGE TITLE") {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
+56
-2
@@ -497,13 +497,23 @@ func CreateTestSecretBytes(t *testing.T, namespace string, baseName string, secr
|
||||
return created
|
||||
}
|
||||
|
||||
func CreateClientCredsSecret(t *testing.T, clientID string, clientSecret string) *corev1.Secret {
|
||||
func CreateOIDCClientCredentialsSecret(t *testing.T, clientID string, clientSecret string) *corev1.Secret {
|
||||
t.Helper()
|
||||
return createClientCredentialsSecret(t, clientID, clientSecret, "secrets.pinniped.dev/oidc-client")
|
||||
}
|
||||
|
||||
func CreateGitHubClientCredentialsSecret(t *testing.T, clientID string, clientSecret string) *corev1.Secret {
|
||||
t.Helper()
|
||||
return createClientCredentialsSecret(t, clientID, clientSecret, "secrets.pinniped.dev/github-client")
|
||||
}
|
||||
|
||||
func createClientCredentialsSecret(t *testing.T, clientID string, clientSecret string, secretType string) *corev1.Secret {
|
||||
t.Helper()
|
||||
env := IntegrationEnv(t)
|
||||
return CreateTestSecret(t,
|
||||
env.SupervisorNamespace,
|
||||
"client-creds",
|
||||
"secrets.pinniped.dev/oidc-client",
|
||||
corev1.SecretType(secretType),
|
||||
map[string]string{
|
||||
"clientID": clientID,
|
||||
"clientSecret": clientSecret,
|
||||
@@ -585,6 +595,50 @@ func createOIDCClientSecret(t *testing.T, forOIDCClient *configv1alpha1.OIDCClie
|
||||
return generatedSecret
|
||||
}
|
||||
|
||||
func CreateTestGitHubIdentityProvider(t *testing.T, spec idpv1alpha1.GitHubIdentityProviderSpec, expectedPhase idpv1alpha1.GitHubIdentityProviderPhase) *idpv1alpha1.GitHubIdentityProvider {
|
||||
t.Helper()
|
||||
return CreateTestGitHubIdentityProviderWithObjectMeta(t, spec, TestObjectMeta(t, "upstream-github-idp"), expectedPhase)
|
||||
}
|
||||
|
||||
func CreateTestGitHubIdentityProviderWithObjectMeta(t *testing.T, spec idpv1alpha1.GitHubIdentityProviderSpec, objectMeta metav1.ObjectMeta, expectedPhase idpv1alpha1.GitHubIdentityProviderPhase) *idpv1alpha1.GitHubIdentityProvider {
|
||||
t.Helper()
|
||||
env := IntegrationEnv(t)
|
||||
client := NewSupervisorClientset(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
upstreams := client.IDPV1alpha1().GitHubIdentityProviders(env.SupervisorNamespace)
|
||||
|
||||
// Create the GitHubIdentityProvider using GenerateName to get a random name.
|
||||
created, err := upstreams.Create(ctx, &idpv1alpha1.GitHubIdentityProvider{
|
||||
ObjectMeta: objectMeta,
|
||||
Spec: spec,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Always clean this up after this point.
|
||||
t.Cleanup(func() {
|
||||
t.Logf("cleaning up test GitHubIdentityProvider %s/%s", created.Namespace, created.Name)
|
||||
err := upstreams.Delete(context.Background(), created.Name, metav1.DeleteOptions{})
|
||||
notFound := k8serrors.IsNotFound(err)
|
||||
// It's okay if it is not found, because it might have been deleted by another part of this test.
|
||||
if !notFound {
|
||||
require.NoErrorf(t, err, "could not cleanup test GitHubIdentityProvider %s/%s", created.Namespace, created.Name)
|
||||
}
|
||||
})
|
||||
t.Logf("created test GitHubIdentityProvider %s", created.Name)
|
||||
|
||||
// Wait for the GitHubIdentityProvider to enter the expected phase (or time out).
|
||||
var result *idpv1alpha1.GitHubIdentityProvider
|
||||
RequireEventuallyf(t, func(requireEventually *require.Assertions) {
|
||||
var err error
|
||||
result, err = upstreams.Get(ctx, created.Name, metav1.GetOptions{})
|
||||
requireEventually.NoErrorf(err, "error while getting GitHubIdentityProvider %s/%s", created.Namespace, created.Name)
|
||||
requireEventually.Equal(expectedPhase, result.Status.Phase)
|
||||
}, 60*time.Second, 1*time.Second, "expected the GitHubIdentityProvider to go into phase %s, GitHubIdentityProvider was: %s", expectedPhase, Sdump(result))
|
||||
return result
|
||||
}
|
||||
|
||||
func CreateTestOIDCIdentityProvider(t *testing.T, spec idpv1alpha1.OIDCIdentityProviderSpec, expectedPhase idpv1alpha1.OIDCIdentityProviderPhase) *idpv1alpha1.OIDCIdentityProvider {
|
||||
t.Helper()
|
||||
return CreateTestOIDCIdentityProviderWithObjectMeta(t, spec, TestObjectMeta(t, "upstream-oidc-idp"), expectedPhase)
|
||||
|
||||
+22
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testlib
|
||||
@@ -62,10 +62,11 @@ type TestEnv struct {
|
||||
ExpectedGroups []string `json:"expectedGroups"`
|
||||
} `json:"testUser"`
|
||||
|
||||
CLIUpstreamOIDC TestOIDCUpstream `json:"cliOIDCUpstream"`
|
||||
SupervisorUpstreamOIDC TestOIDCUpstream `json:"supervisorOIDCUpstream"`
|
||||
SupervisorUpstreamLDAP TestLDAPUpstream `json:"supervisorLDAPUpstream"`
|
||||
SupervisorUpstreamActiveDirectory TestLDAPUpstream `json:"supervisorActiveDirectoryUpstream"`
|
||||
CLIUpstreamOIDC TestOIDCUpstream `json:"cliOIDCUpstream"`
|
||||
SupervisorUpstreamOIDC TestOIDCUpstream `json:"supervisorOIDCUpstream"`
|
||||
SupervisorUpstreamLDAP TestLDAPUpstream `json:"supervisorLDAPUpstream"`
|
||||
SupervisorUpstreamActiveDirectory TestLDAPUpstream `json:"supervisorActiveDirectoryUpstream"`
|
||||
SupervisorUpstreamGithub TestGithubUpstream `json:"supervisorGithubUpstream"`
|
||||
}
|
||||
|
||||
type TestOIDCUpstream struct {
|
||||
@@ -110,6 +111,14 @@ type TestLDAPUpstream struct {
|
||||
TestDeactivatedUserPassword string `json:"TestDeactivatedUserPassword"`
|
||||
}
|
||||
|
||||
type TestGithubUpstream struct {
|
||||
GithubAppClientID string `json:"githubAppClientId"`
|
||||
GithubAppClientSecret string `json:"githubAppClientSecret"`
|
||||
TestUserUsername string `json:"testUserUsername"`
|
||||
TestUserPassword string `json:"testUserPassword"`
|
||||
TestUserOTPSecret string `json:"testUserOTPSecret"`
|
||||
}
|
||||
|
||||
// ProxyEnv returns a set of environment variable strings (e.g., to combine with os.Environ()) which set up the configured test HTTP proxy.
|
||||
func (e *TestEnv) ProxyEnv() []string {
|
||||
if e.Proxy == "" {
|
||||
@@ -319,6 +328,14 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
|
||||
GroupSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""),
|
||||
}
|
||||
|
||||
result.SupervisorUpstreamGithub = TestGithubUpstream{
|
||||
GithubAppClientID: wantEnv("PINNIPED_TEST_GITHUB_APP_CLIENT_ID", ""),
|
||||
GithubAppClientSecret: wantEnv("PINNIPED_TEST_GITHUB_APP_CLIENT_SECRET", ""),
|
||||
TestUserUsername: wantEnv("PINNIPED_TEST_GITHUB_USER_USERNAME", ""),
|
||||
TestUserPassword: wantEnv("PINNIPED_TEST_GITHUB_USER_PASSWORD", ""),
|
||||
TestUserOTPSecret: wantEnv("PINNIPED_TEST_GITHUB_USER_OTP_SECRET", ""),
|
||||
}
|
||||
|
||||
sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs)
|
||||
sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs)
|
||||
sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsCNs)
|
||||
|
||||
Reference in New Issue
Block a user