Replace agouti and chromedriver with chromedp across the whole project

This commit is contained in:
Ryan Richard 2023-08-01 09:04:21 -07:00
parent 2c27db0c85
commit 4512eeca9a
10 changed files with 389 additions and 279 deletions

View File

@ -114,7 +114,6 @@ go build -o pinniped ./cmd/pinniped
1. Install dependencies:
- [`chromedriver`](https://chromedriver.chromium.org/) (and [Chrome](https://www.google.com/chrome/))
- [`docker`](https://www.docker.com/)
- `htpasswd` (installed by default on MacOS, usually found in `apache2-utils` package for linux)
- [`kapp`](https://carvel.dev/#getting-started)
@ -122,11 +121,13 @@ go build -o pinniped ./cmd/pinniped
- [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [`ytt`](https://carvel.dev/#getting-started)
- [`nmap`](https://nmap.org/download.html)
- [`openssl`](https://www.openssl.org) (installed by default on MacOS)
- [Chrome](https://www.google.com/chrome/)
On macOS, these tools can be installed with [Homebrew](https://brew.sh/) (assuming you have Chrome installed already):
```bash
brew install kind vmware-tanzu/carvel/ytt vmware-tanzu/carvel/kapp kubectl chromedriver nmap && brew cask install docker
brew install kind vmware-tanzu/carvel/ytt vmware-tanzu/carvel/kapp kubectl nmap && brew cask install docker
```
1. Create a kind cluster, compile, create container images, and install Pinniped and supporting test dependencies using:

8
go.mod
View File

@ -6,6 +6,8 @@ replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b
require (
github.com/MakeNowJust/heredoc/v2 v2.0.1
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9
github.com/chromedp/chromedp v0.9.1
github.com/coreos/go-oidc/v3 v3.6.0
github.com/creack/pty v1.1.18
github.com/davecgh/go-spew v1.1.1
@ -26,7 +28,6 @@ require (
github.com/ory/fosite v0.44.0
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/pkg/errors v0.9.1
github.com/sclevine/agouti v3.0.0+incompatible
github.com/sclevine/spec v1.4.0
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
@ -63,6 +64,7 @@ require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/coreos/go-oidc v2.2.1+incompatible // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.4.0 // indirect
@ -80,6 +82,9 @@ require (
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@ -105,7 +110,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/ory/go-acc v0.2.8 // indirect
github.com/ory/go-convenience v0.1.0 // indirect
github.com/ory/viper v1.7.5 // indirect

40
go.sum
View File

@ -80,6 +80,12 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY=
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA=
github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -187,7 +193,12 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
@ -307,7 +318,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
@ -349,6 +359,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
@ -380,19 +392,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/ory/fosite v0.44.0 h1:Z3UjyO11/wlIoa3BotOqcTkfm7kUNA8F7dd8mOMfx0o=
github.com/ory/fosite v0.44.0/go.mod h1:o/G4kAeNn65l6MCod2+KmFfU6JQBSojS7eXys6lKGzM=
github.com/ory/go-acc v0.2.8 h1:rOHHAPQjf0u7eHFGWpiXK+gIu/e0GRSJNr9pDukdNC4=
@ -459,8 +464,6 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sclevine/agouti v3.0.0+incompatible h1:8IBJS6PWz3uTlMP3YBIR5f+KAldcGuOeFkFbUWfBgK4=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@ -638,7 +641,6 @@ golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -663,7 +665,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@ -714,7 +715,6 @@ golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -726,10 +726,8 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -753,8 +751,8 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -848,7 +846,6 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@ -980,7 +977,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@ -992,8 +988,6 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -50,7 +50,6 @@ skip_build=no
clean_kind=no
api_group_suffix="pinniped.dev" # same default as in the values.yaml ytt file
dockerfile_path=""
skip_chromedriver_check=no
get_active_directory_vars="" # specify a filename for a script to get AD related env variables
alternate_deploy="undefined"
@ -78,10 +77,6 @@ while (("$#")); do
api_group_suffix=$1
shift
;;
--live-dangerously)
skip_chromedriver_check=yes
shift
;;
--get-active-directory-vars)
shift
# If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error
@ -153,28 +148,8 @@ check_dependency kapp "Please install kapp. e.g. 'brew tap vmware-tanzu/carvel &
check_dependency kubectl "Please install kubectl. e.g. 'brew install kubectl' for MacOS"
check_dependency htpasswd "Please install htpasswd. Should be pre-installed on MacOS. Usually found in 'apache2-utils' package for linux."
check_dependency openssl "Please install openssl. Should be pre-installed on MacOS."
check_dependency chromedriver "Please install chromedriver. e.g. 'brew install chromedriver' for MacOS"
check_dependency nmap "Please install nmap. e.g. 'brew install nmap' for MacOS"
# Check that Chrome and chromedriver versions match. If chromedriver falls a couple versions behind
# then usually tests start to fail with strange error messages.
if [[ "$skip_chromedriver_check" == "no" ]]; then
if [[ "$OSTYPE" == "darwin"* ]]; then
chrome_version=$(/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version | cut -d ' ' -f3 | cut -d '.' -f1)
else
chrome_version=$(google-chrome --version | cut -d ' ' -f3 | cut -d '.' -f1)
fi
chromedriver_version=$(chromedriver --version | cut -d ' ' -f2 | cut -d '.' -f1)
if [[ "$chrome_version" != "$chromedriver_version" ]]; then
log_error "It appears that you are using Chrome $chrome_version with chromedriver $chromedriver_version."
log_error "Please use the same version of chromedriver as Chrome."
log_error "If you are using the latest version of Chrome, then you can upgrade"
log_error "to the latest chromedriver, e.g. 'brew upgrade chromedriver' on MacOS."
log_error "Feeling lucky? Add --live-dangerously to skip this check."
exit 1
fi
fi
# Require kubectl >= 1.18.x.
if [ "$(kubectl version --client=true -o=json | grep gitVersion | cut -d '.' -f 2)" -lt 18 ]; then
log_error "kubectl >= 1.18.x is required, you have $(kubectl version --client=true --short | cut -d ':' -f2)"

View File

@ -1,4 +1,4 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
@ -280,7 +280,7 @@ func runPinnipedLoginOIDC(
sessionCachePath := testutil.TempDir(t) + "/sessions.yaml"
// Start the browser driver.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
// Start the CLI running the "login oidc [...]" command with stdout/stderr connected to pipes.
cmd := oidcLoginCommand(ctx, t, pinnipedExe, sessionCachePath)
@ -334,22 +334,21 @@ func runPinnipedLoginOIDC(
case loginURL = <-loginURLChan:
}
t.Logf("navigating to login page")
require.NoError(t, page.Navigate(loginURL))
browser.Navigate(t, loginURL)
// Expect to be redirected to the upstream provider and log in.
browsertest.LoginToUpstreamOIDC(t, page, env.CLIUpstreamOIDC)
browsertest.LoginToUpstreamOIDC(t, browser, env.CLIUpstreamOIDC)
// 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`)
browsertest.WaitForURL(t, page, callbackURLPattern)
browser.WaitForURL(t, callbackURLPattern)
// 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)
browser.WaitForVisibleElements(t, "pre")
msg := browser.TextOfFirstMatch(t, "pre")
require.Equal(t, "you have been logged in and may now close this tab", msg)
// Expect the CLI to output an ExecCredential in JSON format.

View File

@ -1,4 +1,4 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
@ -24,7 +24,6 @@ import (
"time"
"github.com/creack/pty"
"github.com/sclevine/agouti"
"github.com/stretchr/testify/require"
authorizationv1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1"
@ -123,7 +122,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
expectedUsername := env.SupervisorUpstreamOIDC.Username
expectedGroups := env.SupervisorUpstreamOIDC.ExpectedGroups
@ -177,18 +176,18 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
// Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser.
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page)
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser)
// Confirm that we got to the upstream IDP's login page, fill out the form, and submit the form.
browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC)
browsertest.LoginToUpstreamOIDC(t, browser, 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)))
browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer)))
// 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)
formpostExpectSuccessState(t, browser)
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
@ -204,7 +203,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
expectedUsername := env.SupervisorUpstreamOIDC.Username
expectedGroups := env.SupervisorUpstreamOIDC.ExpectedGroups
@ -258,18 +257,18 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
// Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser.
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page)
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser)
// Confirm that we got to the upstream IDP's login page, fill out the form, and submit the form.
browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC)
browsertest.LoginToUpstreamOIDC(t, browser, 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)))
browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(downstream.Spec.Issuer)))
// 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)
formpostExpectSuccessState(t, browser)
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
@ -288,7 +287,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
expectedUsername := env.SupervisorUpstreamOIDC.Username
expectedGroups := env.SupervisorUpstreamOIDC.ExpectedGroups
@ -363,20 +362,20 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
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))
browser.Navigate(t, loginURL)
// Expect to be redirected to the upstream provider and log in.
browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC)
browsertest.LoginToUpstreamOIDC(t, browser, 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)))
browser.WaitForURL(t, 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)
authCode := formpostExpectManualState(t, browser)
// Enter the auth code in the waiting prompt, followed by a newline.
t.Logf("'manually' pasting authorization code %q to waiting prompt", authCode)
t.Logf("'manually' pasting authorization code with length %d to waiting prompt", len(authCode))
_, err = ptyFile.WriteString(authCode + "\n")
require.NoError(t, err)
@ -399,7 +398,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
expectedUsername := env.SupervisorUpstreamOIDC.Username
expectedGroups := env.SupervisorUpstreamOIDC.ExpectedGroups
@ -488,20 +487,20 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
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))
browser.Navigate(t, loginURL)
// Expect to be redirected to the upstream provider and log in.
browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC)
browsertest.LoginToUpstreamOIDC(t, browser, 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)))
browser.WaitForURL(t, 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)
authCode := formpostExpectManualState(t, browser)
// Enter the auth code in the waiting prompt, followed by a newline.
t.Logf("'manually' pasting authorization code %q to waiting prompt", authCode)
t.Logf("'manually' pasting authorization code with length %d to waiting prompt", len(authCode))
_, err = ptyFile.WriteString(authCode + "\n")
require.NoError(t, err)
@ -1002,7 +1001,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue
expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs
@ -1029,13 +1028,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
// Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser.
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page)
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser)
// Confirm that we got to the Supervisor's login page, fill out the form, and submit the form.
browsertest.LoginToUpstreamLDAP(t, page, downstream.Spec.Issuer,
browsertest.LoginToUpstreamLDAP(t, browser, downstream.Spec.Issuer,
expectedUsername, env.SupervisorUpstreamLDAP.TestUserPassword)
formpostExpectSuccessState(t, page)
formpostExpectSuccessState(t, browser)
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
@ -1052,7 +1051,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue
expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames
@ -1079,13 +1078,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
// Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser.
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page)
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser)
// Confirm that we got to the Supervisor's login page, fill out the form, and submit the form.
browsertest.LoginToUpstreamLDAP(t, page, downstream.Spec.Issuer,
browsertest.LoginToUpstreamLDAP(t, browser, downstream.Spec.Issuer,
expectedUsername, env.SupervisorUpstreamActiveDirectory.TestUserPassword)
formpostExpectSuccessState(t, page)
formpostExpectSuccessState(t, browser)
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
@ -1102,7 +1101,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue
expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs
@ -1135,13 +1134,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
// Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser.
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page)
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser)
// Confirm that we got to the Supervisor's login page, fill out the form, and submit the form.
browsertest.LoginToUpstreamLDAP(t, page, downstream.Spec.Issuer,
browsertest.LoginToUpstreamLDAP(t, browser, downstream.Spec.Issuer,
expectedUsername, env.SupervisorUpstreamLDAP.TestUserPassword)
formpostExpectSuccessState(t, page)
formpostExpectSuccessState(t, browser)
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
@ -1149,7 +1148,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
})
}
func startKubectlAndOpenAuthorizationURLInBrowser(testCtx context.Context, t *testing.T, kubectlCmd *exec.Cmd, page *agouti.Page) chan string {
func startKubectlAndOpenAuthorizationURLInBrowser(testCtx context.Context, t *testing.T, kubectlCmd *exec.Cmd, b *browsertest.Browser) chan string {
// Wrap the stdout and stderr pipes with TeeReaders which will copy each incremental read to an
// in-memory buffer, so we can have the full output available to us at the end.
originalStderrPipe, err := kubectlCmd.StderrPipe()
@ -1226,7 +1225,7 @@ func startKubectlAndOpenAuthorizationURLInBrowser(testCtx context.Context, t *te
case loginURL = <-loginURLChan:
}
t.Logf("navigating to login page: %q", loginURL)
require.NoError(t, page.Navigate(loginURL))
b.Navigate(t, loginURL)
return kubectlOutputChan
}

View File

@ -1,4 +1,4 @@
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
@ -16,7 +16,6 @@ import (
"github.com/ory/fosite"
"github.com/ory/fosite/token/hmac"
"github.com/sclevine/agouti"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -33,24 +32,25 @@ func TestFormPostHTML_Browser_Parallel(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) {
browser := browsertest.OpenBrowser(t)
// Serve the form_post template with successful parameters.
responseParams := formpostRandomParams(t)
formpostInitiate(t, page, formpostTemplateServer(t, callbackURL, responseParams))
formpostInitiate(t, browser, 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)
formpostExpectSuccessState(t, browser)
})
t.Run("callback server error", func(t *testing.T) {
browser := browsertest.OpenBrowser(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))
formpostInitiate(t, browser, formpostTemplateServer(t, callbackURL+"?fail=500", responseParams))
// Now we handle the callback and assert that we got what we expected.
expectCallback(t, responseParams)
@ -66,13 +66,15 @@ func TestFormPostHTML_Browser_Parallel(t *testing.T) {
// In the future, we could change the Javascript code to use mode 'cors'
// because we have upgraded our CLI callback endpoint to handle CORS,
// and then we could change this to formpostExpectManualState().
formpostExpectSuccessState(t, page)
formpostExpectSuccessState(t, browser)
})
t.Run("network failure", func(t *testing.T) {
browser := browsertest.OpenBrowser(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))
formpostInitiate(t, browser, 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
@ -80,28 +82,30 @@ func TestFormPostHTML_Browser_Parallel(t *testing.T) {
expectCallback(t, responseParams)
// This failure should cause the UI to enter the "manual" state.
actualCode := formpostExpectManualState(t, page)
actualCode := formpostExpectManualState(t, browser)
require.Equal(t, responseParams.Get("code"), actualCode)
})
t.Run("timeout", func(t *testing.T) {
browser := browsertest.OpenBrowser(t)
// Serve the form_post template with successful parameters.
responseParams := formpostRandomParams(t)
formpostInitiate(t, page, formpostTemplateServer(t, callbackURL, responseParams))
formpostInitiate(t, browser, 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)
actualCode := formpostExpectManualState(t, browser)
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)
formpostExpectSuccessState(t, browser)
})
}
@ -228,88 +232,66 @@ func formpostRandomParams(t *testing.T) url.Values {
}
}
// formpostExpectTitle asserts that the page has the expected title.
func formpostExpectTitle(t *testing.T, page *agouti.Page, expected string) {
// formpostExpectFavicon asserts that the page has the expected SVG/emoji favicon.
func formpostExpectFavicon(t *testing.T, b *browsertest.Browser, expected string) {
t.Helper()
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) {
t.Helper()
iconURL, err := page.First("#favicon").Attribute("href")
require.NoError(t, err)
iconURL := b.AttrValueOfFirstMatch(t, "#favicon", "href")
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,
)
require.Contains(t, iconURL, expected)
}
// 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) {
func formpostInitiate(t *testing.T, b *browsertest.Browser, url string) {
t.Helper()
require.NoError(t, page.Reset())
t.Logf("navigating to mock form_post template URL %s...", url)
require.NoError(t, page.Navigate(url))
b.Navigate(t, url)
t.Logf("expecting to see loading animation...")
browsertest.WaitForVisibleElements(t, page, "#loading")
formpostExpectTitle(t, page, "Logging in...")
formpostExpectFavicon(t, page, "⏳")
b.WaitForVisibleElements(t, "div#loading")
require.Equal(t, "Logging in...", b.Title(t))
formpostExpectFavicon(t, b, "⏳")
}
// formpostExpectSuccessState asserts that the page is in the "success" state.
func formpostExpectSuccessState(t *testing.T, page *agouti.Page) {
func formpostExpectSuccessState(t *testing.T, b *browsertest.Browser) {
t.Helper()
t.Logf("expecting to see success message become visible...")
browsertest.WaitForVisibleElements(t, page, "#success")
successDivText, err := page.First("#success").Text()
require.NoError(t, err)
b.WaitForVisibleElements(t, "div#success")
successDivText := b.TextOfFirstMatch(t, "div#success")
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, "✅")
require.Equal(t, "Login succeeded", b.Title(t))
formpostExpectFavicon(t, b, "✅")
}
// formpostExpectManualState asserts that the page is in the "manual" state and returns the auth code.
func formpostExpectManualState(t *testing.T, page *agouti.Page) string {
func formpostExpectManualState(t *testing.T, b *browsertest.Browser) string {
t.Helper()
t.Logf("expecting to see manual message become visible...")
browsertest.WaitForVisibleElements(t, page, "#manual")
manualDivText, err := page.First("#manual").Text()
require.NoError(t, err)
b.WaitForVisibleElements(t, "div#manual")
manualDivText := b.TextOfFirstMatch(t, "div#manual")
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, "⌛")
require.Equal(t, "Finish your login", b.Title(t))
formpostExpectFavicon(t, b, "⌛")
// 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())
b.ClickFirstMatch(t, "#manual-copy-button")
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
}
matchingText, found := b.FindConsoleEventWithTextMatching("info", consoleLogPattern)
requireEventually.True(found)
if captureMatches := consoleLogPattern.FindStringSubmatch(matchingText); captureMatches != nil {
authCode = captureMatches[1]
return
}
requireEventually.FailNow("expected console log was not found")
}, 3*time.Second, 100*time.Millisecond)
}, 10*time.Second, 100*time.Millisecond)
return authCode
}

View File

@ -2429,15 +2429,15 @@ func requestAuthorizationAndExpectImmediateRedirectToCallback(t *testing.T, _, d
t.Helper()
// Open the web browser and navigate to the downstream authorize URL.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
t.Logf("opening browser to downstream authorize URL %s", testlib.MaskTokens(downstreamAuthorizeURL))
require.NoError(t, page.Navigate(downstreamAuthorizeURL))
browser.Navigate(t, downstreamAuthorizeURL)
// Expect that it immediately redirects back to the callback, which is what happens for certain types of errors
// where it is not worth redirecting to the login UI page.
t.Logf("waiting for redirect to callback")
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(downstreamCallbackURL) + `\?.+\z`)
browsertest.WaitForURL(t, page, callbackURLPattern)
browser.WaitForURL(t, callbackURLPattern)
}
func requestAuthorizationUsingBrowserAuthcodeFlowOIDC(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
@ -2451,17 +2451,17 @@ func requestAuthorizationUsingBrowserAuthcodeFlowOIDC(t *testing.T, _, downstrea
makeAuthorizationRequestAndRequireSecurityHeaders(ctx, t, downstreamAuthorizeURL, httpClient)
// Open the web browser and navigate to the downstream authorize URL.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
t.Logf("opening browser to downstream authorize URL %s", testlib.MaskTokens(downstreamAuthorizeURL))
require.NoError(t, page.Navigate(downstreamAuthorizeURL))
browser.Navigate(t, downstreamAuthorizeURL)
// Expect to be redirected to the upstream provider and log in.
browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC)
browsertest.LoginToUpstreamOIDC(t, browser, env.SupervisorUpstreamOIDC)
// Wait for the login to happen and us be redirected back to a localhost callback.
t.Logf("waiting for redirect to callback")
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(downstreamCallbackURL) + `\?.+\z`)
browsertest.WaitForURL(t, page, callbackURLPattern)
browser.WaitForURL(t, callbackURLPattern)
}
func requestAuthorizationUsingBrowserAuthcodeFlowLDAP(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client) {
@ -2474,51 +2474,51 @@ func requestAuthorizationUsingBrowserAuthcodeFlowLDAP(t *testing.T, downstreamIs
makeAuthorizationRequestAndRequireSecurityHeaders(ctx, t, downstreamAuthorizeURL, httpClient)
// Open the web browser and navigate to the downstream authorize URL.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
t.Logf("opening browser to downstream authorize URL %s", testlib.MaskTokens(downstreamAuthorizeURL))
require.NoError(t, page.Navigate(downstreamAuthorizeURL))
browser.Navigate(t, downstreamAuthorizeURL)
// Expect to be redirected to the upstream provider and log in.
browsertest.LoginToUpstreamLDAP(t, page, downstreamIssuer, username, password)
browsertest.LoginToUpstreamLDAP(t, browser, downstreamIssuer, username, password)
// Wait for the login to happen and us be redirected back to a localhost callback.
t.Logf("waiting for redirect to callback")
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(downstreamCallbackURL) + `\?.+\z`)
browsertest.WaitForURL(t, page, callbackURLPattern)
browser.WaitForURL(t, callbackURLPattern)
}
func requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentials(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, _, username, password string, _ *http.Client) {
t.Helper()
// Open the web browser and navigate to the downstream authorize URL.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
t.Logf("opening browser to downstream authorize URL %s", testlib.MaskTokens(downstreamAuthorizeURL))
require.NoError(t, page.Navigate(downstreamAuthorizeURL))
browser.Navigate(t, downstreamAuthorizeURL)
// This functions assumes that it has been passed either a bad username or a bad password, and submits the
// provided credentials. Expect to be redirected to the upstream provider and attempt to log in.
browsertest.LoginToUpstreamLDAP(t, page, downstreamIssuer, username, password)
browsertest.LoginToUpstreamLDAP(t, browser, downstreamIssuer, username, password)
// After failing login expect to land back on the login page again with an error message.
browsertest.WaitForUpstreamLDAPLoginPageWithError(t, page, downstreamIssuer)
browsertest.WaitForUpstreamLDAPLoginPageWithError(t, browser, downstreamIssuer)
}
func requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentialsAndThenGoodCredentials(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, _, username, password string, _ *http.Client) {
t.Helper()
// Open the web browser and navigate to the downstream authorize URL.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
t.Logf("opening browser to downstream authorize URL %s", testlib.MaskTokens(downstreamAuthorizeURL))
require.NoError(t, page.Navigate(downstreamAuthorizeURL))
browser.Navigate(t, downstreamAuthorizeURL)
// Expect to be redirected to the upstream provider and attempt to log in.
browsertest.LoginToUpstreamLDAP(t, page, downstreamIssuer, username, "this is the wrong password!")
browsertest.LoginToUpstreamLDAP(t, browser, downstreamIssuer, username, "this is the wrong password!")
// After failing login expect to land back on the login page again with an error message.
browsertest.WaitForUpstreamLDAPLoginPageWithError(t, page, downstreamIssuer)
browsertest.WaitForUpstreamLDAPLoginPageWithError(t, browser, downstreamIssuer)
// Already at the login page, so this time can directly submit it using the provided username and password.
browsertest.SubmitUpstreamLDAPLoginForm(t, page, username, password)
browsertest.SubmitUpstreamLDAPLoginForm(t, browser, username, password)
}
func makeAuthorizationRequestAndRequireSecurityHeaders(ctx context.Context, t *testing.T, downstreamAuthorizeURL string, httpClient *http.Client) {

View File

@ -1,4 +1,4 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
@ -355,7 +355,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
}
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest.Open(t)
browser := browsertest.OpenBrowser(t)
expectedUsername := env.SupervisorUpstreamOIDC.Username
@ -436,17 +436,17 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
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))
browser.Navigate(t, loginURL)
// Expect to be redirected to the upstream provider and log in.
browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC)
browsertest.LoginToUpstreamOIDC(t, browser, 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)))
browser.WaitForURL(t, 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)
authCode := formpostExpectManualState(t, browser)
// Enter the auth code in the waiting prompt, followed by a newline.
t.Logf("'manually' pasting authorization code %q to waiting prompt", authCode)

View File

@ -1,68 +1,265 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2023 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 (
"context"
"fmt"
"log"
"regexp"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/sclevine/agouti"
chromedpbrowser "github.com/chromedp/cdproto/browser"
chromedpruntime "github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"github.com/stretchr/testify/require"
"go.pinniped.dev/test/testlib"
)
const (
operationTimeout = 10 * time.Second
operationPollingInterval = 100 * time.Millisecond
)
// Browser abstracts the specific browser driver library that we use and provides an interface
// for integration tests to interact with the browser.
type Browser struct {
chromeCtx context.Context
consoleEvents []consoleEvent
exceptionEvents []string
lock sync.RWMutex
}
// 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 {
// consoleEvent tracks calls to the browser's console functions, like console.log().
type consoleEvent struct {
api string
args []string
}
// OpenBrowser opens a web browser as a subprocess and returns a Browser which allows
// further interactions with the browser. The subprocess will be cleaned up at the end
// of the test. Each call to OpenBrowser creates a new browser which does not share any
// cookies with other browsers from other calls.
func OpenBrowser(t *testing.T) *Browser {
t.Helper()
// make it trivial to run all browser based tests via:
// Make it trivial to run all browser based tests via:
// go test -v -race -count 1 -timeout 0 ./test/integration -run '/_Browser'
require.Contains(t, rootTestName(t), "_Browser", "browser based tests must contain the string _Browser in their name")
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"}
// Configure the browser.
options := append(
// Start with the defaults.
chromedp.DefaultExecAllocatorOptions[:],
// Add "ignore-certificate-errors" Chrome flag.
chromedp.IgnoreCertErrors,
// Uncomment this to watch the browser while the test runs.
// chromedp.Flag("headless", false), chromedp.Flag("hide-scrollbars", false), chromedp.Flag("mute-audio", false),
)
if runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
// When running on linux, assume that we are running inside a container for CI.
// Need to pass an extra flag in this case to avoid getting an error while launching Chrome.
options = append(options, chromedp.NoSandbox)
}
// Add the proxy flag when needed.
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",
})
options = append(options, chromedp.ProxyServer(env.Proxy))
}
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,
// Build the context using the above options.
configCtx, configCancelFunc := chromedp.NewExecAllocator(context.Background(), options...)
t.Cleanup(configCancelFunc)
// Create a browser context.
chromeCtx, chromeCancelFunc := chromedp.NewContext(configCtx,
// Uncomment to show Chrome debug logging.
// This can be an overwhelming amount of text, but can help to debug things.
// chromedp.WithDebugf(log.Printf),
chromedp.WithLogf(log.Printf),
chromedp.WithErrorf(log.Printf),
)
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
t.Cleanup(chromeCancelFunc)
// Create the return value.
b := &Browser{chromeCtx: chromeCtx}
// Subscribe to console events and exceptions to make them available later.
chromedp.ListenTarget(chromeCtx, func(ev interface{}) {
switch ev := ev.(type) {
case *chromedpruntime.EventConsoleAPICalled:
args := make([]string, len(ev.Args))
for i, arg := range ev.Args {
// Could also pay attention to arg.Type here, but choosing to keep it simple for now.
args[i] = fmt.Sprintf("%s", arg.Value) //nolint:gosimple // this is an acceptable way to get a string
}
b.lock.Lock()
defer b.lock.Unlock()
b.consoleEvents = append(b.consoleEvents, consoleEvent{
api: ev.Type.String(),
args: args,
})
case *chromedpruntime.EventExceptionThrown:
b.lock.Lock()
defer b.lock.Unlock()
b.exceptionEvents = append(b.exceptionEvents, ev.ExceptionDetails.Error())
}
})
// Start the web browser subprocess. Do not use a timeout here or else the browser will close after that timeout.
// The subprocess will be cleaned up at the end of the test when the browser context is cancelled.
require.NoError(t, chromedp.Run(chromeCtx))
// Grant permission to write to the clipboard because the Pinniped formpost UI has a button to copy the
// authcode to the clipboard, and we want to be able to use that button in tests.
require.NoError(t, chromedp.Run(chromeCtx,
chromedpbrowser.GrantPermissions(
[]chromedpbrowser.PermissionType{chromedpbrowser.PermissionTypeClipboardSanitizedWrite},
),
))
// To aid in debugging test failures, print the events received from the browser at the end of the test.
t.Cleanup(func() {
b.lock.RLock()
defer b.lock.RUnlock()
consoleEventCount := len(b.consoleEvents)
exceptionEventCount := len(b.exceptionEvents)
if consoleEventCount > 0 {
t.Logf("Printing %d browser console events at end of test...", consoleEventCount)
}
for _, e := range b.consoleEvents {
args := make([]string, len(e.args))
for i, arg := range e.args {
args[i] = fmt.Sprintf("%q", testlib.MaskTokens(arg))
}
t.Logf("console.%s with args: [%s]", e.api, strings.Join(args, ", "))
}
if exceptionEventCount > 0 {
t.Logf("Printing %d browser exception events at end of test...", exceptionEventCount)
}
for _, e := range b.exceptionEvents {
t.Logf("exception: %s", e)
}
})
// Done. The browser is ready to be driven by the test.
return b
}
func (b *Browser) timeout() time.Duration {
return 30 * time.Second
}
func (b *Browser) runWithTimeout(t *testing.T, timeout time.Duration, actions ...chromedp.Action) {
t.Helper()
timeoutCtx, cancel := context.WithTimeout(b.chromeCtx, timeout)
t.Cleanup(cancel)
err := chromedp.Run(timeoutCtx, actions...)
if err != nil && err == context.Canceled || err == context.DeadlineExceeded {
require.NoError(t, err, "the browser operation took longer than the allowed timeout")
}
require.NoError(t, err, "the browser operation failed")
}
func (b *Browser) Navigate(t *testing.T, url string) {
t.Helper()
b.runWithTimeout(t, b.timeout(), chromedp.Navigate(url))
}
func (b *Browser) Title(t *testing.T) string {
t.Helper()
var title string
b.runWithTimeout(t, b.timeout(), chromedp.Title(&title))
return title
}
func (b *Browser) WaitForVisibleElements(t *testing.T, selectors ...string) {
t.Helper()
for _, s := range selectors {
b.runWithTimeout(t, b.timeout(), chromedp.WaitVisible(s))
}
}
func (b *Browser) TextOfFirstMatch(t *testing.T, selector string) string {
t.Helper()
var text string
b.runWithTimeout(t, b.timeout(), chromedp.Text(selector, &text, chromedp.NodeVisible))
return text
}
func (b *Browser) AttrValueOfFirstMatch(t *testing.T, selector string, attributeName string) string {
t.Helper()
var value string
var ok bool
b.runWithTimeout(t, b.timeout(), chromedp.AttributeValue(selector, attributeName, &value, &ok))
require.Truef(t, ok, "did not find attribute named %q on first element returned by selector %q", attributeName, selector)
return value
}
func (b *Browser) SendKeysToFirstMatch(t *testing.T, selector string, runesToType string) {
t.Helper()
b.runWithTimeout(t, b.timeout(), chromedp.SendKeys(selector, runesToType, chromedp.NodeVisible))
}
func (b *Browser) ClickFirstMatch(t *testing.T, selector string) string {
t.Helper()
var text string
b.runWithTimeout(t, b.timeout(), chromedp.Click(selector, chromedp.NodeVisible))
return text
}
// 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 (b *Browser) WaitForURL(t *testing.T, regex *regexp.Regexp) {
var lastURL string
testlib.RequireEventuallyf(t,
func(requireEventually *require.Assertions) {
var url string
requireEventually.NoError(chromedp.Run(b.chromeCtx, chromedp.Location(&url)))
if url != lastURL {
t.Logf("saw URL %s", testlib.MaskTokens(url))
lastURL = url
}
requireEventually.Regexp(regex, url)
},
30*time.Second,
100*time.Millisecond,
"expected to browse to %s, but never got there",
regex,
)
}
// FindConsoleEventWithTextMatching searches the browser's console that have been observed so far
// to find an event with an argument (converted to a string) that matches the provided regexp.
// consoleEventAPIType could be any of the console.funcName() names, e.g. "log", "info", "error", etc.
// It returns the first matching event argument value. It doesn't worry about optimizing the search
// speed because there should not be too many console events and because this just is a test helper.
func (b *Browser) FindConsoleEventWithTextMatching(consoleEventAPIType string, re *regexp.Regexp) (string, bool) {
b.lock.RLock()
defer b.lock.RUnlock()
for _, e := range b.consoleEvents {
if e.api == consoleEventAPIType {
for _, arg := range e.args {
if re.Match([]byte(arg)) {
return arg, true
}
}
}
}
return "", false
}
func rootTestName(t *testing.T) string {
@ -84,50 +281,9 @@ func rootTestName(t *testing.T) string {
}
}
// 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()
testlib.RequireEventuallyf(t,
func(requireEventually *require.Assertions) {
for _, sel := range selectors {
vis, err := page.First(sel).Visible()
requireEventually.NoError(err)
requireEventually.Truef(vis, "expected element %q to be visible", sel)
}
},
operationTimeout,
operationPollingInterval,
"expected to have a page with selectors %v, but it never loaded",
selectors,
)
}
// 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
testlib.RequireEventuallyf(t,
func(requireEventually *require.Assertions) {
url, err := page.URL()
if url != lastURL {
t.Logf("saw URL %s", testlib.MaskTokens(url))
lastURL = url
}
requireEventually.NoError(err)
requireEventually.Regexp(pat, url)
},
operationTimeout,
operationPollingInterval,
"expected to browse to %s, but never got there",
pat,
)
}
// LoginToUpstreamOIDC 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 LoginToUpstreamOIDC(t *testing.T, page *agouti.Page, upstream testlib.TestOIDCUpstream) {
func LoginToUpstreamOIDC(t *testing.T, b *Browser, upstream testlib.TestOIDCUpstream) {
t.Helper()
type config struct {
@ -171,21 +327,21 @@ func LoginToUpstreamOIDC(t *testing.T, page *agouti.Page, upstream testlib.TestO
// Expect to be redirected to the login page.
t.Logf("waiting for redirect to %s login page", cfg.Name)
WaitForURL(t, page, cfg.LoginPagePattern)
b.WaitForURL(t, cfg.LoginPagePattern)
// Wait for the login page to be rendered.
WaitForVisibleElements(t, page, cfg.UsernameSelector, cfg.PasswordSelector, cfg.LoginButtonSelector)
b.WaitForVisibleElements(t, 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())
b.SendKeysToFirstMatch(t, cfg.UsernameSelector, upstream.Username)
b.SendKeysToFirstMatch(t, cfg.PasswordSelector, upstream.Password)
b.ClickFirstMatch(t, cfg.LoginButtonSelector)
}
// 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, page *agouti.Page, issuer, username, password string) {
func LoginToUpstreamLDAP(t *testing.T, b *Browser, issuer, username, password string) {
t.Helper()
loginURLRegexp, err := regexp.Compile(`\A` + regexp.QuoteMeta(issuer+"/login") + `\?state=.+\z`)
@ -193,34 +349,34 @@ func LoginToUpstreamLDAP(t *testing.T, page *agouti.Page, issuer, username, pass
// Expect to be redirected to the login page.
t.Logf("waiting for redirect to %s/login page", issuer)
WaitForURL(t, page, loginURLRegexp)
b.WaitForURL(t, loginURLRegexp)
// Wait for the login page to be rendered.
WaitForVisibleElements(t, page, "#username", "#password", "#submit")
b.WaitForVisibleElements(t, "#username", "#password", "#submit")
// Fill in the username and password and click "submit".
SubmitUpstreamLDAPLoginForm(t, page, username, password)
SubmitUpstreamLDAPLoginForm(t, b, username, password)
}
func SubmitUpstreamLDAPLoginForm(t *testing.T, page *agouti.Page, username string, password string) {
func SubmitUpstreamLDAPLoginForm(t *testing.T, b *Browser, username string, password string) {
t.Helper()
// Fill in the username and password and click "submit".
t.Logf("logging in via Supervisor's upstream LDAP/AD login UI page")
require.NoError(t, page.First("#username").Fill(username))
require.NoError(t, page.First("#password").Fill(password))
require.NoError(t, page.First("#submit").Click())
b.SendKeysToFirstMatch(t, "#username", username)
b.SendKeysToFirstMatch(t, "#password", password)
b.ClickFirstMatch(t, "#submit")
}
func WaitForUpstreamLDAPLoginPageWithError(t *testing.T, page *agouti.Page, issuer string) {
func WaitForUpstreamLDAPLoginPageWithError(t *testing.T, b *Browser, issuer string) {
t.Helper()
// Wait for redirect back to the login page again with an error.
t.Logf("waiting for redirect to back to login page with error message")
loginURLRegexp, err := regexp.Compile(`\A` + regexp.QuoteMeta(issuer+"/login") + `\?err=login_error&state=.+\z`)
require.NoError(t, err)
WaitForURL(t, page, loginURLRegexp)
b.WaitForURL(t, loginURLRegexp)
// Wait for the login page to be rendered again, this time also with an error message.
WaitForVisibleElements(t, page, "#username", "#password", "#submit", "#alert")
b.WaitForVisibleElements(t, "#username", "#password", "#submit", "#alert")
}