You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

129 lines
4.7 KiB

package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"strings"
"testing"
"teraclone/internal/service"
)
func TestLoginCaptchaIsPublicPeerBoundAndForwardedWithCredentials(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Cookie") != "" || r.Header.Get("Authorization") != "" {
t.Error("credentials leaked through public login request")
}
switch r.URL.Path {
case "/api/v1/auth/login-policy":
_ = json.NewEncoder(w).Encode(map[string]bool{"captcha_enabled": true})
case "/api/v1/auth/captcha":
if r.Header.Get("X-Forwarded-For") != "192.0.2.44" {
t.Errorf("challenge peer = %q", r.Header.Get("X-Forwarded-For"))
}
_ = json.NewEncoder(w).Encode(map[string]string{"challenge_id": "test-challenge", "image_data_url": "data:image/png;base64,test", "expires_at": "2026-09-07T12:00:00Z"})
case "/api/v1/auth/login":
var input map[string]string
_ = json.NewDecoder(r.Body).Decode(&input)
if r.Header.Get("X-Forwarded-For") != "192.0.2.44" {
t.Error("login and challenge peers differ")
}
if input["captcha_id"] != "test-challenge" || input["captcha_answer"] != "23456789" {
w.WriteHeader(401)
_, _ = w.Write([]byte(`{"error":{"code":"captcha_failed","message":"login verification failed"}}`))
return
}
_ = json.NewEncoder(w).Encode(map[string]string{"actor": "admin", "role": "administrator", "session_token": "private-token"})
default:
http.NotFound(w, r)
}
}))
defer backend.Close()
app, err := service.NewAppService(filepath.Join(t.TempDir(), "frontend.db"))
if err != nil {
t.Fatal(err)
}
defer app.Close()
handler, err := NewDeviceHandler(app, backend.URL)
if err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
handler.RegisterRoutes(mux)
for _, path := range []string{"/login/backend-status", "/login/captcha"} {
method := http.MethodGet
if path == "/login/captcha" {
method = http.MethodPost
}
req := httptest.NewRequest(method, path, nil)
req.RemoteAddr = "192.0.2.44:45000"
req.Header.Set("X-Forwarded-For", "198.51.100.99")
req.Header.Set("Authorization", "Bearer untrusted")
req.Header.Set("Cookie", "untrusted=yes")
res := httptest.NewRecorder()
mux.ServeHTTP(res, req)
if res.Code != 200 || res.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("public endpoint = %d: %s", res.Code, res.Body.String())
}
if path == "/login/backend-status" && !strings.Contains(res.Body.String(), `"captcha_enabled":true`) {
t.Fatal("CAPTCHA policy lost")
}
}
for _, answer := range []string{"wrong", "23456789"} {
body := url.Values{"username": {"admin"}, "password": {"test-password"}, "captcha_id": {"test-challenge"}, "captcha_answer": {answer}}
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(body.Encode()))
req.RemoteAddr = "192.0.2.44:45001"
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res := httptest.NewRecorder()
mux.ServeHTTP(res, req)
if answer == "wrong" {
if len(res.Result().Cookies()) != 0 || !strings.Contains(res.Body.String(), "틀렸거나 만료") {
t.Fatal("CAPTCHA failure did not remain on login")
}
} else if res.Code != 303 || len(res.Result().Cookies()) == 0 {
t.Fatalf("valid challenge login = %d", res.Code)
}
if strings.Contains(res.Body.String(), "test-password") || strings.Contains(res.Body.String(), "private-token") {
t.Fatal("login response leaked secret")
}
}
}
func TestFactoryResetPageRequiresAdministratorAndExplainsScope(t *testing.T) {
app, err := service.NewAppService(filepath.Join(t.TempDir(), "frontend.db"))
if err != nil {
t.Fatal(err)
}
defer app.Close()
handler, err := NewDeviceHandler(app, "http://127.0.0.1:1")
if err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
handler.RegisterRoutes(mux)
for _, role := range []string{"administrator", "port_operator"} {
handler.sessions[role] = browserSession{Username: "test", BackendToken: "test", Role: role}
req := httptest.NewRequest(http.MethodGet, "/device/factory-reset", nil)
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: role})
res := httptest.NewRecorder()
mux.ServeHTTP(res, req)
if role == "port_operator" {
if res.Code != 403 {
t.Fatal("operator can open factory reset")
}
continue
}
for _, expected := range []string{"공장 초기화", `name="confirmation"`, "로컬 사용자 계정은 보존", "자동 재부팅하지 않습니다"} {
if !strings.Contains(res.Body.String(), expected) {
t.Errorf("missing reset warning: %s", expected)
}
}
if strings.Contains(res.Body.String(), `href="/ports/restart"`) {
t.Fatal("old restart sidebar entry still present")
}
}
}