Files
helix-proxy/cmd/helix-proxy/bootstrap_test.go
T
s1d3sw1ped_bot 13070a275d
Format / gofmt (push) Successful in 7s
CI / Build (push) Successful in 14s
Format / gofmt (pull_request) Successful in 7s
CI / Build (pull_request) Successful in 13s
CI / Go Tests (push) Successful in 50s
CI / Go Tests (pull_request) Successful in 48s
Lock bootstrap admin token until password is changed.
Closes #2: bootstrap JWTs cannot mutate admin APIs except change-password, production requires ADMIN_PASSWORD on first boot, admin binds loopback.
2026-09-01 03:54:09 +00:00

168 lines
5.2 KiB
Go

package main
import (
"net/http"
"strings"
"testing"
"helix-proxy/internal/config"
"helix-proxy/internal/store"
)
func newBootstrapAPIServer(t *testing.T) *apiTestEnv {
t.Helper()
t.Setenv("PROXY_MODE", "development")
t.Setenv("ADMIN_PASSWORD", "")
return newAPITestServer(t)
}
func TestBootstrapLock_MutatingAdminForbidden(t *testing.T) {
env := newBootstrapAPIServer(t)
resp := apiPost(t, env.BaseURL+"/api/login", map[string]any{"password": defaultAdminPassword}, "")
if resp.StatusCode != http.StatusOK {
t.Fatalf("bootstrap login: %d %s", resp.StatusCode, readBody(resp))
}
var out struct {
Token string `json:"token"`
}
decodeJSON(t, resp, &out)
if out.Token == "" {
t.Fatal("empty bootstrap token")
}
tok := out.Token
mutating := []struct {
method string
path string
body any
}{
{http.MethodPost, "/api/proxy-hosts", map[string]any{
"domainNames": []string{"boot.example"},
"forwardHost": "127.0.0.1",
"forwardPort": 8080,
"forwardScheme": "http",
}},
{http.MethodPost, "/api/settings", map[string]any{"default_site": "404"}},
{http.MethodPost, "/api/access-lists", map[string]any{"name": "boot"}},
{http.MethodPost, "/api/streams", map[string]any{"incomingPort": 9000, "forwardingHost": "127.0.0.1", "forwardingPort": 9001}},
}
for _, tc := range mutating {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
r := apiRequest(t, tc.method, env.BaseURL+tc.path, tok, tc.body)
assertStatus(t, r, http.StatusForbidden)
})
}
assertStatus(t, apiGet(t, env.BaseURL+"/api/users/me", tok), http.StatusOK)
assertStatus(t, apiGet(t, env.BaseURL+"/api/proxy-hosts", tok), http.StatusOK)
}
func TestBootstrapLock_ChangePasswordThenDefaultFails(t *testing.T) {
env := newBootstrapAPIServer(t)
resp := apiPost(t, env.BaseURL+"/api/login", map[string]any{"password": defaultAdminPassword}, "")
if resp.StatusCode != http.StatusOK {
t.Fatalf("bootstrap login: %d %s", resp.StatusCode, readBody(resp))
}
var out struct {
Token string `json:"token"`
}
decodeJSON(t, resp, &out)
ch := apiPost(t, env.BaseURL+"/api/users/me/password", map[string]any{
"currentPassword": defaultAdminPassword,
"newPassword": "changed-after-bootstrap",
}, out.Token)
assertStatus(t, ch, http.StatusOK)
fail := apiPost(t, env.BaseURL+"/api/login", map[string]any{"password": defaultAdminPassword}, "")
assertStatus(t, fail, http.StatusUnauthorized)
okLogin := apiPost(t, env.BaseURL+"/api/login", map[string]any{"password": "changed-after-bootstrap"}, "")
if okLogin.StatusCode != http.StatusOK {
t.Fatalf("new password login: %d %s", okLogin.StatusCode, readBody(okLogin))
}
var out2 struct {
Token string `json:"token"`
}
decodeJSON(t, okLogin, &out2)
create := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
"domainNames": []string{"after-change.example"},
"forwardHost": "127.0.0.1",
"forwardPort": 8080,
"forwardScheme": "http",
}, out2.Token)
if create.StatusCode != http.StatusCreated {
t.Fatalf("full token after change should mutate: %d %s", create.StatusCode, readBody(create))
}
_ = create.Body.Close()
locked := apiPost(t, env.BaseURL+"/api/settings", map[string]any{"default_site": "404"}, out.Token)
assertStatus(t, locked, http.StatusForbidden)
}
func TestDevTestsBootWithAdminPassword(t *testing.T) {
t.Setenv("PROXY_MODE", "development")
env := newAPITestServer(t)
if adminPasswordUnset(env.Store) {
t.Fatal("test env should store ADMIN_PASSWORD hash")
}
token := env.loginToken(t)
create := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
"domainNames": []string{"dev-admin.example"},
"forwardHost": "127.0.0.1",
"forwardPort": 8080,
"forwardScheme": "http",
}, token)
if create.StatusCode != http.StatusCreated {
t.Fatalf("dev ADMIN_PASSWORD token should be fully privileged: %d %s", create.StatusCode, readBody(create))
}
_ = create.Body.Close()
fail := apiPost(t, env.BaseURL+"/api/login", map[string]any{"password": defaultAdminPassword}, "")
assertStatus(t, fail, http.StatusUnauthorized)
}
func TestApplyInitialAdminPassword_RejectsDefault(t *testing.T) {
t.Setenv("PROXY_MODE", "development")
t.Setenv("ADMIN_PASSWORD", defaultAdminPassword)
tmp := t.TempDir()
config.ResetForTest()
t.Setenv("DATA_DIR", tmp)
if err := config.EnsureDataDirs(); err != nil {
t.Fatal(err)
}
st, err := store.New()
if err != nil {
t.Fatal(err)
}
if c, ok := st.(interface{ Close() error }); ok {
t.Cleanup(func() { _ = c.Close() })
}
err = applyInitialAdminPassword(st)
if err == nil || !strings.Contains(err.Error(), "default") {
t.Fatalf("expected default ADMIN_PASSWORD error, got %v", err)
}
}
func TestApplyInitialAdminPassword_ProductionRequiresEnv(t *testing.T) {
t.Setenv("PROXY_MODE", "production")
t.Setenv("ADMIN_PASSWORD", "")
tmp := t.TempDir()
config.ResetForTest()
t.Setenv("DATA_DIR", tmp)
if err := config.EnsureDataDirs(); err != nil {
t.Fatal(err)
}
st, err := store.New()
if err != nil {
t.Fatal(err)
}
if c, ok := st.(interface{ Close() error }); ok {
t.Cleanup(func() { _ = c.Close() })
}
err = applyInitialAdminPassword(st)
if err == nil || !strings.Contains(err.Error(), "ADMIN_PASSWORD") {
t.Fatalf("expected refuse without ADMIN_PASSWORD, got %v", err)
}
}