167 lines
5.6 KiB
Go
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)
|
|
}
|