package httpapi import ( "bytes" "encoding/json" "log" "net/http" "net/http/httptest" "strings" "testing" "git.librete.ch/public/librenotes/internal/auth" ) func newSigner() *auth.Signer { return auth.NewSigner([]byte("test-secret-32-bytes-of-keymaterial!!")) } func quietLogger() *log.Logger { return log.New(&bytes.Buffer{}, "", 0) } // passthrough handler: writes the tenant info from context. func passthrough(t *testing.T) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tenant, err := TenantFrom(r.Context()) if err != nil { t.Errorf("no tenant in context: %v", err) http.Error(w, "no tenant", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(tenant) }) } func TestAuthMiddleware_Valid(t *testing.T) { signer := newSigner() tok, _ := signer.Issue("u-1", "u@example.com") mw := AuthMiddleware(signer, quietLogger()) h := mw(passthrough(t)) req := httptest.NewRequest(http.MethodGet, "/api/whoami", nil) req.Header.Set("Authorization", "Bearer "+tok) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got %d body=%s", rec.Code, rec.Body) } var got Tenant _ = json.NewDecoder(rec.Body).Decode(&got) if got.UserID != "u-1" || got.Email != "u@example.com" { t.Errorf("tenant mismatch: %+v", got) } } func TestAuthMiddleware_MissingHeader(t *testing.T) { mw := AuthMiddleware(newSigner(), quietLogger()) h := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("handler should not run") })) req := httptest.NewRequest(http.MethodGet, "/api/x", nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("got %d", rec.Code) } } func TestAuthMiddleware_BadScheme(t *testing.T) { mw := AuthMiddleware(newSigner(), quietLogger()) h := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) for _, hdr := range []string{"Basic abc", "Bearer", " ", "Token xyz"} { req := httptest.NewRequest(http.MethodGet, "/api/x", nil) req.Header.Set("Authorization", hdr) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("hdr %q: got %d", hdr, rec.Code) } } } func TestAuthMiddleware_InvalidJWT(t *testing.T) { mw := AuthMiddleware(newSigner(), quietLogger()) h := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("handler should not run") })) req := httptest.NewRequest(http.MethodGet, "/api/x", nil) req.Header.Set("Authorization", "Bearer not.a.jwt") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("got %d", rec.Code) } } func TestAuthMiddleware_TamperedJWT(t *testing.T) { signer := newSigner() tok, _ := signer.Issue("u-1", "u@example.com") tampered := tok[:len(tok)-2] + "XX" mw := AuthMiddleware(signer, quietLogger()) h := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("handler should not run") })) req := httptest.NewRequest(http.MethodGet, "/api/x", nil) req.Header.Set("Authorization", "Bearer "+tampered) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("got %d", rec.Code) } } func TestAuthMiddleware_DifferentSecretRejects(t *testing.T) { a := newSigner() b := auth.NewSigner([]byte("different-32-bytes-of-keymaterial!!!!")) tok, _ := a.Issue("u-1", "u@example.com") mw := AuthMiddleware(b, quietLogger()) h := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("handler should not run") })) req := httptest.NewRequest(http.MethodGet, "/api/x", nil) req.Header.Set("Authorization", "Bearer "+tok) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("got %d", rec.Code) } } func TestRequireTenantOwnership(t *testing.T) { mw := AuthMiddleware(newSigner(), quietLogger()) signer := newSigner() tok, _ := signer.Issue("alice", "a@x") mw = AuthMiddleware(signer, quietLogger()) cases := []struct { name, owner string want int }{ {"self", "alice", http.StatusOK}, {"other", "bob", http.StatusForbidden}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { h := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !RequireTenantOwnership(w, r, c.owner) { return } w.WriteHeader(http.StatusOK) })) req := httptest.NewRequest(http.MethodGet, "/api/notes/"+c.owner, nil) req.Header.Set("Authorization", "Bearer "+tok) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != c.want { t.Errorf("got %d want %d", rec.Code, c.want) } }) } } func TestRouterWiring(t *testing.T) { signer := newSigner() srv := &Server{ Auth: auth.Handlers{Service: nil}, // not exercised here Signer: signer, Logger: quietLogger(), } mux := srv.Routes() // /api/whoami requires auth. req := httptest.NewRequest(http.MethodGet, "/api/whoami", nil) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("unauth /api/whoami got %d", rec.Code) } // With JWT. tok, _ := signer.Issue("u-9", "x@y") req = httptest.NewRequest(http.MethodGet, "/api/whoami", nil) req.Header.Set("Authorization", "Bearer "+tok) rec = httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("auth /api/whoami got %d body=%s", rec.Code, rec.Body) } if !strings.Contains(rec.Body.String(), `"user_id":"u-9"`) { t.Errorf("body missing user_id: %s", rec.Body) } }