Files
DoGaMa-serv/tests/e2e/bootstrap_test.go
T
codex 6196542633
CI / validate (pull_request) Successful in 26m0s
test(lifecycle): cover failed start diagnostics
2026-08-15 18:45:46 +02:00

167 lines
5.6 KiB
Go

//go:build linux
package e2e_test
import (
"bytes"
"context"
"io"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
func TestV1BootstrapAuthenticationAndHTTPBoundary(t *testing.T) {
repositoryRoot := filepath.Clean(filepath.Join("..", ".."))
binary := filepath.Join(t.TempDir(), "dogama")
build := exec.Command("go", "build", "-trimpath", "-o", binary, "./cmd/dogama")
build.Dir = repositoryRoot
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build dogama: %v\n%s", err, output)
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
address := listener.Addr().String()
_ = listener.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var logs bytes.Buffer
command := exec.CommandContext(ctx, binary)
command.Env = append(os.Environ(),
"DOGAMA_LISTEN_ADDRESS="+address,
"DOGAMA_DATABASE_PATH="+filepath.Join(t.TempDir(), "dogama.db"),
"DOGAMA_MASTER_KEY_FILE="+filepath.Join(t.TempDir(), "master_key"),
"DOGAMA_TEMPLATES_ROOT="+filepath.Join(t.TempDir(), "templates"),
"DOGAMA_IMPORTS_ROOT="+filepath.Join(t.TempDir(), "imports"),
"DOGAMA_SERVERS_ROOT="+filepath.Join(t.TempDir(), "servers"),
)
command.Stdout, command.Stderr = &logs, &logs
if err := command.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
cancel()
_ = command.Wait()
})
client := &http.Client{Timeout: 2 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
baseURL := "http://" + address
setup := waitFor(t, client, baseURL+"/setup", &logs)
assertStatus(t, setup, http.StatusOK)
assertSecurityHeaders(t, setup)
csrf := cookieValue(t, setup, "dogama_csrf")
badCSRF := form(t, client, baseURL+"/setup", url.Values{"csrf_token": {"forged"}, "username": {"admin"}, "email": {"admin@example.test"}, "password": {"correct horse battery staple"}, "language": {"en"}}, csrf)
assertStatus(t, badCSRF, http.StatusForbidden)
created := form(t, client, baseURL+"/setup", url.Values{"csrf_token": {csrf}, "username": {"admin"}, "email": {"admin@example.test"}, "password": {"correct horse battery staple"}, "language": {"en"}}, csrf)
assertStatus(t, created, http.StatusSeeOther)
closed := get(t, client, baseURL+"/setup", "")
assertStatus(t, closed, http.StatusSeeOther)
loginPage := get(t, client, baseURL+"/login", "")
csrf = cookieValue(t, loginPage, "dogama_csrf")
login := form(t, client, baseURL+"/login", url.Values{"csrf_token": {csrf}, "username": {"admin"}, "password": {"correct horse battery staple"}}, csrf)
assertStatus(t, login, http.StatusSeeOther)
session := cookieValue(t, login, "dogama_session")
csrf = cookieValue(t, login, "dogama_csrf")
unauthenticated := get(t, client, baseURL+"/api/v1/catalog", "")
assertStatus(t, unauthenticated, http.StatusUnauthorized)
authenticated := get(t, client, baseURL+"/api/v1/catalog", "dogama_session="+session+"; dogama_csrf="+csrf)
assertStatus(t, authenticated, http.StatusOK)
if !strings.Contains(readBody(t, authenticated), "palworld-official") {
t.Fatal("authenticated catalog omitted the Palworld reference template")
}
}
func waitFor(t *testing.T, client *http.Client, target string, logs *bytes.Buffer) *http.Response {
t.Helper()
deadline := time.Now().Add(15 * time.Second)
for time.Now().Before(deadline) {
response, err := client.Get(target)
if err == nil {
return response
}
time.Sleep(50 * time.Millisecond)
}
t.Fatalf("application did not start:\n%s", logs.String())
return nil
}
func get(t *testing.T, client *http.Client, target, cookie string) *http.Response {
t.Helper()
request, _ := http.NewRequest(http.MethodGet, target, nil)
if cookie != "" {
request.Header.Set("Cookie", cookie)
}
response, err := client.Do(request)
if err != nil {
t.Fatal(err)
}
return response
}
func form(t *testing.T, client *http.Client, target string, values url.Values, csrf string) *http.Response {
t.Helper()
request, _ := http.NewRequest(http.MethodPost, target, strings.NewReader(values.Encode()))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.Header.Set("Cookie", "dogama_csrf="+csrf)
response, err := client.Do(request)
if err != nil {
t.Fatal(err)
}
return response
}
func cookieValue(t *testing.T, response *http.Response, name string) string {
t.Helper()
for _, cookie := range response.Cookies() {
if cookie.Name == name {
if cookie.Secure || !cookie.HttpOnly || cookie.SameSite != http.SameSiteStrictMode {
t.Fatalf("insecure %s cookie: %#v", name, cookie)
}
return cookie.Value
}
}
t.Fatalf("cookie %s not found", name)
return ""
}
func assertSecurityHeaders(t *testing.T, response *http.Response) {
t.Helper()
for name, expected := range map[string]string{"X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Cross-Origin-Opener-Policy": "same-origin"} {
if got := response.Header.Get(name); got != expected {
t.Fatalf("%s = %q, want %q", name, got, expected)
}
}
if !strings.Contains(response.Header.Get("Content-Security-Policy"), "default-src 'none'") || response.Header.Get("Strict-Transport-Security") != "" {
t.Fatal("browser security headers are inconsistent for HTTP")
}
}
func assertStatus(t *testing.T, response *http.Response, expected int) {
t.Helper()
if response.StatusCode != expected {
t.Fatalf("status = %d, want %d; body=%s", response.StatusCode, expected, readBody(t, response))
}
}
func readBody(t *testing.T, response *http.Response) string {
t.Helper()
defer func() { _ = response.Body.Close() }()
body, err := io.ReadAll(response.Body)
if err != nil {
t.Fatal(err)
}
return string(body)
}