Phase 1: Control Service skeleton (Go, SQLite, session login, Docker, Caddy)
- Go service with its own multi-stage Dockerfile and docker-compose.yml; runs as container webrestrict-control on port 8791. - SQLite schema for the full AGENTS.md §7 data model via versioned migrations (PRAGMA user_version); audit log is append-only. - Configuration from the process environment; the real env file is gitignored and a template is committed. No secrets in the repo. - Separate session login (signed HMAC cookie + CSRF) for Sam. - GET /health; HTMX + server-rendered dashboard styled with the Family Home Lab tokens.css (copied) and Inter fonts; dark mode; phone-friendly. - Caddy vhost for webrestrict.lab.audasmedia.com.au applied on .35 (Let's Encrypt cert issued); route recorded in deploy/caddy/webrestrict.caddy. - Unit tests for config, auth and store. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
112
internal/auth/auth_test.go
Normal file
112
internal/auth/auth_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testManager(t *testing.T) *Manager {
|
||||
t.Helper()
|
||||
m, err := New([]byte("0123456789abcdef0123456789abcdef"), time.Hour, false, "sam", "", "hunter2")
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestCredentials(t *testing.T) {
|
||||
m := testManager(t)
|
||||
if !m.CheckCredentials("sam", "hunter2") {
|
||||
t.Error("valid credentials rejected")
|
||||
}
|
||||
if m.CheckCredentials("sam", "wrong") {
|
||||
t.Error("wrong password accepted")
|
||||
}
|
||||
if m.CheckCredentials("finn", "hunter2") {
|
||||
t.Error("wrong username accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionRoundTripAndTamper(t *testing.T) {
|
||||
m := testManager(t)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
m.Issue(rec)
|
||||
res := rec.Result()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for _, c := range res.Cookies() {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
if !m.Authenticated(req) {
|
||||
t.Fatal("issued session is not authenticated")
|
||||
}
|
||||
|
||||
// Tamper with the session value: the HMAC must reject it.
|
||||
tampered := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for _, c := range res.Cookies() {
|
||||
if c.Name == sessionCookie {
|
||||
c.Value = c.Value[:len(c.Value)-2] + "xx"
|
||||
}
|
||||
tampered.AddCookie(c)
|
||||
}
|
||||
if m.Authenticated(tampered) {
|
||||
t.Fatal("tampered session accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSRF(t *testing.T) {
|
||||
m := testManager(t)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/login", nil)
|
||||
token := m.EnsureCSRF(rec, req)
|
||||
if token == "" {
|
||||
t.Fatal("empty csrf token")
|
||||
}
|
||||
|
||||
// The token is delivered as a cookie; a matching form value passes.
|
||||
form := httptest.NewRequest(http.MethodPost, "/login", nil)
|
||||
form.PostForm = map[string][]string{"csrf": {token}}
|
||||
for _, c := range rec.Result().Cookies() {
|
||||
form.AddCookie(c)
|
||||
}
|
||||
if !m.CheckCSRF(form) {
|
||||
t.Error("matching csrf rejected")
|
||||
}
|
||||
|
||||
// A different form value fails.
|
||||
bad := httptest.NewRequest(http.MethodPost, "/login", nil)
|
||||
bad.PostForm = map[string][]string{"csrf": {"not-the-token"}}
|
||||
for _, c := range rec.Result().Cookies() {
|
||||
bad.AddCookie(c)
|
||||
}
|
||||
if m.CheckCSRF(bad) {
|
||||
t.Error("mismatched csrf accepted")
|
||||
}
|
||||
|
||||
// No cookie at all fails.
|
||||
none := httptest.NewRequest(http.MethodPost, "/login", nil)
|
||||
none.PostForm = map[string][]string{"csrf": {token}}
|
||||
if m.CheckCSRF(none) {
|
||||
t.Error("csrf accepted without cookie")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredSession(t *testing.T) {
|
||||
m, err := New([]byte("0123456789abcdef0123456789abcdef"), -time.Minute, false, "sam", "", "hunter2")
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
m.Issue(rec)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for _, c := range rec.Result().Cookies() {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
if m.Authenticated(req) {
|
||||
t.Error("expired session accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user