Replace agouti and chromedriver with chromedp across the whole project
This commit is contained in:
parent
2c27db0c85
commit
4512eeca9a
@ -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
8
go.mod
@ -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
40
go.sum
@ -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=
|
||||
|
@ -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)"
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user