87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
|
|
//go:build e2e
|
||
|
|
|
||
|
|
package e2e
|
||
|
|
|
||
|
|
import (
|
||
|
|
"sync/atomic"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Journey is a k6-style user journey: an ordered sequence of HTTP steps that
|
||
|
|
// share local state (closures over the parent test). If any step fails the
|
||
|
|
// remaining steps are auto-skipped, mirroring k6 scenario "abort on fail"
|
||
|
|
// behaviour so logs are easy to read — you stop at the first broken step.
|
||
|
|
//
|
||
|
|
// Use j.Step(id, desc, fn) to add steps; the framework prints:
|
||
|
|
//
|
||
|
|
// ▶ [J-1] Tenant Owner 入職第一天
|
||
|
|
// ▶ [J-1.1] GET /me 看自己是誰
|
||
|
|
// ▶ [J-1.2] PATCH /me 更新 display_name
|
||
|
|
// ⊘ [J-1.3] skipped (journey aborted)
|
||
|
|
type Journey struct {
|
||
|
|
t *testing.T
|
||
|
|
id string
|
||
|
|
title string
|
||
|
|
aborted atomic.Bool
|
||
|
|
total int
|
||
|
|
ran int
|
||
|
|
failed bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewJourney prints the journey banner and returns a builder.
|
||
|
|
// Call j.Run() at the end to print the final summary.
|
||
|
|
func NewJourney(t *testing.T, id, title string) *Journey {
|
||
|
|
t.Helper()
|
||
|
|
t.Logf("▶ [%s] %s", id, title)
|
||
|
|
return &Journey{t: t, id: id, title: title}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step adds a step. fn receives a sub-test t scoped to this step so testify
|
||
|
|
// require.* abort just this step (not the whole Test function), letting the
|
||
|
|
// framework print a clean "aborted" line for subsequent steps.
|
||
|
|
//
|
||
|
|
// Pre-existing failures in earlier steps short-circuit the step and emit a
|
||
|
|
// "⊘ skipped" line.
|
||
|
|
func (j *Journey) Step(id, desc string, fn func(t *testing.T)) {
|
||
|
|
j.total++
|
||
|
|
stepID := j.id + "." + id
|
||
|
|
name := stepID + " " + desc
|
||
|
|
j.t.Run(name, func(t *testing.T) {
|
||
|
|
if j.aborted.Load() {
|
||
|
|
t.Skipf("⊘ [%s] skipped — journey aborted at an earlier step", stepID)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
t.Logf(" ▶ [%s] %s", stepID, desc)
|
||
|
|
j.ran++
|
||
|
|
// fn may call require.* which marks t failed and FailNow's. After it
|
||
|
|
// returns we inspect t.Failed() to decide whether to abort siblings.
|
||
|
|
fn(t)
|
||
|
|
if t.Failed() {
|
||
|
|
j.aborted.Store(true)
|
||
|
|
j.failed = true
|
||
|
|
t.Logf("✗ [%s] FAIL — aborting remaining steps", stepID)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// SkipStep marks a step as intentionally skipped (e.g. requires ZITADEL).
|
||
|
|
// The journey is NOT aborted; the next Step still runs.
|
||
|
|
func (j *Journey) SkipStep(id, desc, reason string) {
|
||
|
|
j.total++
|
||
|
|
stepID := j.id + "." + id
|
||
|
|
name := stepID + " " + desc
|
||
|
|
j.t.Run(name, func(t *testing.T) {
|
||
|
|
t.Skipf("⊘ [%s] %s — %s", stepID, desc, reason)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// Summary prints the final journey result. Always defer this right after
|
||
|
|
// NewJourney so it runs even on require.* abort.
|
||
|
|
func (j *Journey) Summary() {
|
||
|
|
status := "✔"
|
||
|
|
if j.failed {
|
||
|
|
status = "✗"
|
||
|
|
}
|
||
|
|
j.t.Logf("%s [%s] %s — %d/%d steps executed", status, j.id, j.title, j.ran, j.total)
|
||
|
|
}
|