Skip to content

Commit b0ddaa9

Browse files
committed
test(e2e): exercise browser challenge flow
Run the real challenge bundle in headless Chrome against isolated HAProxy and SPOP fixtures. Use a fixed POW level so the test never depends on rate-limit timing or shared stick-table state. Closes #9.
1 parent 0bab484 commit b0ddaa9

8 files changed

Lines changed: 306 additions & 53 deletions

File tree

.github/workflows/go.yml

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,60 +12,86 @@ jobs:
1212
lint:
1313
runs-on: ubuntu-22.04
1414
steps:
15-
- uses: actions/checkout@v3
15+
- uses: actions/checkout@v7
1616

1717
- name: Set up Go
18-
uses: actions/setup-go@v4
18+
uses: actions/setup-go@v6
1919
with:
20-
go-version: '1.21'
20+
go-version-file: go.mod
2121

2222
- uses: dominikh/staticcheck-action@v1.3.0
2323
with:
2424
install-go: false
25+
version: v0.6.1
2526

2627
build:
2728
runs-on: ubuntu-22.04
2829
steps:
29-
- uses: actions/checkout@v3
30+
- uses: actions/checkout@v7
3031

3132
- name: Set up Go
32-
uses: actions/setup-go@v4
33+
uses: actions/setup-go@v6
3334
with:
34-
go-version: '1.21'
35+
go-version-file: go.mod
3536

3637
- name: Build
3738
run: go build -v ./...
3839

3940
test:
4041
runs-on: ubuntu-22.04
4142
steps:
42-
- uses: actions/checkout@v3
43+
- uses: actions/checkout@v7
4344

4445
- name: Set up Go
45-
uses: actions/setup-go@v4
46+
uses: actions/setup-go@v6
4647
with:
47-
go-version: '1.21'
48+
go-version-file: go.mod
4849

4950
- name: Test
5051
run: go test -v ./...
5152

5253
e2e:
5354
runs-on: ubuntu-22.04
5455
steps:
55-
- uses: actions/checkout@v3
56+
- uses: actions/checkout@v7
5657

5758
- name: Set up Go
58-
uses: actions/setup-go@v4
59+
uses: actions/setup-go@v6
5960
with:
60-
go-version: '1.24'
61+
go-version: '1.26'
62+
cache-dependency-path: |
63+
go.sum
64+
test/e2e/go.sum
6165
62-
- name: setup environment
66+
- name: Set up Node
67+
uses: actions/setup-node@v6
68+
with:
69+
node-version: '24'
70+
cache: npm
71+
cache-dependency-path: web/package-lock.json
72+
73+
- name: Set up Chrome
74+
id: setup-chrome
75+
uses: browser-actions/setup-chrome@v2
76+
77+
- name: Install HAProxy
6378
run: |
6479
sudo apt-get install -y software-properties-common
6580
sudo add-apt-repository -y ppa:vbernat/haproxy-3.0
6681
sudo apt-get update
6782
sudo apt-get install -y haproxy
6883
haproxy -vv
6984
70-
- name: Test E2E
71-
run: go test -v ./... --tags=e2e
85+
- name: Build web bundles
86+
working-directory: web
87+
run: |
88+
npm ci
89+
npm run build
90+
91+
- name: Check E2E HAProxy configuration
92+
run: haproxy -c -f test/e2e/haproxy.cfg
93+
94+
- name: Test browser challenge flow
95+
env:
96+
CHROME_BIN: ${{ steps.setup-chrome.outputs.chrome-path }}
97+
run: test/e2e/run.sh

.github/workflows/js.yml

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,59 +12,43 @@ jobs:
1212
lint:
1313
runs-on: ubuntu-22.04
1414
steps:
15-
- uses: actions/checkout@v3
15+
- uses: actions/checkout@v7
1616

1717
- name: Set up NodeJS
18-
uses: actions/setup-node@v4
19-
20-
- uses: dominikh/staticcheck-action@v1.3.0
21-
with:
22-
install-go: false
23-
24-
- name: Restore cached dependencies
25-
id: cache-restore
26-
uses: actions/cache@v3
18+
uses: actions/setup-node@v6
2719
with:
28-
path: ./web/node_modules
29-
key: npm-${{ hashFiles('./src/package-lock.json') }}
20+
node-version: '24'
21+
cache: npm
22+
cache-dependency-path: web/package-lock.json
3023

3124
- name: Install dependencies
32-
if: steps.cache-restore.outputs.cache-hit != 'true'
33-
run: |
34-
cd web
35-
npm ci - ignore-scripts
25+
working-directory: web
26+
run: npm ci
3627

3728
- name: Lint
38-
run: |
39-
cd web
40-
npm run lint
29+
working-directory: web
30+
run: npm run lint
31+
32+
- name: Test
33+
working-directory: web
34+
run: npm run test --if-present
4135

4236
build:
4337
runs-on: ubuntu-22.04
4438
steps:
45-
- uses: actions/checkout@v3
39+
- uses: actions/checkout@v7
4640

4741
- name: Set up NodeJS
48-
uses: actions/setup-node@v4
49-
50-
- uses: dominikh/staticcheck-action@v1.3.0
51-
with:
52-
install-go: false
53-
54-
- name: Restore cached dependencies
55-
id: cache-restore
56-
uses: actions/cache@v3
42+
uses: actions/setup-node@v6
5743
with:
58-
path: ./web/node_modules
59-
key: npm-${{ hashFiles('./src/package-lock.json') }}
44+
node-version: '24'
45+
cache: npm
46+
cache-dependency-path: web/package-lock.json
6047

6148
- name: Install dependencies
62-
if: steps.cache-restore.outputs.cache-hit != 'true'
63-
run: |
64-
cd web
65-
npm ci - ignore-scripts
49+
working-directory: web
50+
run: npm ci
6651

6752
- name: Build
68-
run: |
69-
cd web
70-
npm run build
53+
working-directory: web
54+
run: npm run build

test/e2e/browser_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//go:build e2e
2+
3+
// Package e2e exercises the complete browser challenge flow against a running
4+
// Berghain and HAProxy stack.
5+
package e2e
6+
7+
import (
8+
"context"
9+
"io"
10+
"net/http"
11+
"os"
12+
"strings"
13+
"testing"
14+
"time"
15+
16+
"github.com/chromedp/chromedp"
17+
)
18+
19+
const (
20+
defaultBaseURL = "http://localhost:18080"
21+
backendBody = "Berghain E2E backend reached"
22+
)
23+
24+
func baseURL() string {
25+
if value := os.Getenv("BERGHAIN_E2E_BASE_URL"); value != "" {
26+
return strings.TrimRight(value, "/")
27+
}
28+
return defaultBaseURL
29+
}
30+
31+
func requireChallengePage(t *testing.T, url string) {
32+
t.Helper()
33+
34+
client := &http.Client{Timeout: 5 * time.Second}
35+
response, err := client.Get(url + "/")
36+
if err != nil {
37+
t.Fatalf("request challenge page: %v", err)
38+
}
39+
defer response.Body.Close()
40+
41+
body, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
42+
if err != nil {
43+
t.Fatalf("read challenge page: %v", err)
44+
}
45+
if response.StatusCode != http.StatusForbidden {
46+
t.Fatalf("expected challenge status 403, got %d: %q", response.StatusCode, body)
47+
}
48+
if !strings.Contains(string(body), "Request on Hold") {
49+
t.Fatalf("expected challenge page, got: %q", body)
50+
}
51+
}
52+
53+
func TestBrowserSolvesChallenge(t *testing.T) {
54+
url := baseURL()
55+
requireChallengePage(t, url)
56+
57+
options := append([]chromedp.ExecAllocatorOption{}, chromedp.DefaultExecAllocatorOptions[:]...)
58+
options = append(options, chromedp.NoSandbox)
59+
if binary := os.Getenv("CHROME_BIN"); binary != "" {
60+
options = append(options, chromedp.ExecPath(binary))
61+
}
62+
63+
allocatorContext, cancelAllocator := chromedp.NewExecAllocator(context.Background(), options...)
64+
defer cancelAllocator()
65+
browserContext, cancelBrowser := chromedp.NewContext(allocatorContext)
66+
defer cancelBrowser()
67+
browserContext, cancelTimeout := context.WithTimeout(browserContext, 90*time.Second)
68+
defer cancelTimeout()
69+
70+
var title string
71+
if err := chromedp.Run(browserContext,
72+
chromedp.Navigate(url+"/"),
73+
chromedp.Title(&title),
74+
); err != nil {
75+
t.Fatalf("navigate to challenge page: %v", err)
76+
}
77+
if title != "Request on Hold" {
78+
t.Fatalf("expected browser challenge page, got title %q", title)
79+
}
80+
81+
deadline := time.Now().Add(75 * time.Second)
82+
var body string
83+
for time.Now().Before(deadline) {
84+
err := chromedp.Run(browserContext,
85+
chromedp.Evaluate(`document.body ? document.body.textContent : ""`, &body),
86+
)
87+
if err == nil && strings.Contains(body, backendBody) {
88+
return
89+
}
90+
time.Sleep(250 * time.Millisecond)
91+
}
92+
93+
t.Fatalf("browser did not reach the backend after solving the challenge; last body: %q", body)
94+
}

test/e2e/go.mod

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module github.com/DropMorePackets/berghain/test/e2e
2+
3+
go 1.26
4+
5+
require github.com/chromedp/chromedp v0.15.1
6+
7+
require (
8+
github.com/chromedp/cdproto v0.0.0-20260704091341-6ca7914c3938 // indirect
9+
github.com/chromedp/sysutil v1.1.0 // indirect
10+
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
11+
github.com/gobwas/httphead v0.1.0 // indirect
12+
github.com/gobwas/pool v0.2.1 // indirect
13+
github.com/gobwas/ws v1.4.0 // indirect
14+
golang.org/x/sys v0.42.0 // indirect
15+
)

test/e2e/go.sum

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
github.com/chromedp/cdproto v0.0.0-20260704091341-6ca7914c3938 h1:oVtHWQ/Vml5KJjkiQGCs00Jh7xYBZLyL98RDVZx+lGU=
2+
github.com/chromedp/cdproto v0.0.0-20260704091341-6ca7914c3938/go.mod h1:cbyjALe67vDvlvdiG9369P8w5U2w6IshwtyD2f2Tvag=
3+
github.com/chromedp/chromedp v0.15.1 h1:EJWiPm7BNqDqjYy6U0lTSL5wNH+iNt9GjC3a4gfjNyQ=
4+
github.com/chromedp/chromedp v0.15.1/go.mod h1:CdTHtUqD/dqaFw/cvFWtTydoEQS44wLBuwbMR9EkOY4=
5+
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
6+
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
7+
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
8+
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
9+
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
10+
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
11+
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
12+
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
13+
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
14+
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
15+
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
16+
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
17+
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
18+
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
19+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
20+
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
21+
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

test/e2e/haproxy.cfg

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
global
2+
log stdout format raw local0
3+
tune.bufsize 32768
4+
5+
defaults
6+
mode http
7+
log global
8+
timeout client 5s
9+
timeout server 5s
10+
timeout connect 5s
11+
12+
frontend e2e
13+
bind 127.0.0.1:18080
14+
15+
# Keep the test deterministic: every request is challenged at the fixed
16+
# test level instead of depending on a production rate-limit stick table.
17+
http-request set-var(req.berghain.level) int(1)
18+
19+
filter spoe engine berghain config examples/haproxy/berghain.cfg
20+
21+
acl berghain_path path /cdn-cgi/challenge-platform/challenge
22+
http-request send-spoe-group berghain validate if !berghain_path
23+
http-request return status 501 if { var(txn.berghain.error) -m found }
24+
25+
acl berghain_valid var(txn.berghain.valid) -m bool
26+
http-request return status 403 content-type "text/html" file "web/dist/default/index.html" if !berghain_valid !berghain_path
27+
http-request wait-for-body time 5s if berghain_path METH_POST
28+
use_backend berghain_http if berghain_path
29+
30+
default_backend app_backend
31+
32+
backend app_backend
33+
http-request return status 200 content-type "text/plain" string "Berghain E2E backend reached"
34+
35+
backend berghain_http
36+
filter spoe engine berghain config examples/haproxy/berghain.cfg
37+
38+
acl is_challenge_path path /cdn-cgi/challenge-platform/challenge
39+
http-request send-spoe-group berghain challenge if is_challenge_path
40+
http-request return status 501 if { var(txn.berghain.error) -m found }
41+
42+
acl has_token var(txn.berghain.token) -m found
43+
http-after-response add-header set-cookie "berghain=%[var(txn.berghain.token)]; %[var(txn.berghain.domain)] path=/;" if has_token
44+
http-request return status 200 content-type "application/json" lf-string "%[var(txn.berghain.response)]" if is_challenge_path
45+
http-request return status 404
46+
47+
backend berghain_spop
48+
mode tcp
49+
option spop-check
50+
server localhost 127.0.0.1:19091 check

0 commit comments

Comments
 (0)