// Copyright (C) 2026 Dcntrlized // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package main import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" ) // TestRoutesConstruct builds exactly the wiring main() builds and fails on a // panic. http.ServeMux panics at registration time when the same pattern is // registered twice, which kills the process before it binds :8080 — and // go build/vet/gofmt/go test cannot see it unless a test constructs the mux. // This test exists because a duplicate /auth/logout registration shipped and // only a real launch caught it. func TestRoutesConstruct(t *testing.T) { defer func() { if r := recover(); r != nil { t.Fatalf("route registration panicked — the service would not boot: %v", r) } }() apiMux := newAPIMux() if apiMux == nil { t.Fatal("newAPIMux returned nil") } outer := newOuterMux(apiMux) if outer == nil { t.Fatal("newOuterMux returned nil") } } // TestWebSocketPatternIsFree proves main.go leaves /ws/alarm to the realtime // layer: registering it on the outer mux must not conflict with anything // main.go registered (realtime.Register does exactly this at boot). func TestWebSocketPatternIsFree(t *testing.T) { defer func() { if r := recover(); r != nil { t.Fatalf("/ws/alarm conflicts with a pattern registered by main.go: %v", r) } }() outer := newOuterMux(newAPIMux()) outer.Handle("/ws/alarm", http.NotFoundHandler()) } // TestManifestThroughOuterChain serves GET /_manifest through the real outer // mux (logging + CORS chain included), which is what a caller hits. func TestManifestThroughOuterChain(t *testing.T) { outer := newOuterMux(newAPIMux()) rec := httptest.NewRecorder() outer.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/_manifest", nil)) if rec.Code != http.StatusOK { t.Fatalf("GET /_manifest = %d, want 200 (body: %s)", rec.Code, rec.Body.String()) } if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") { t.Fatalf("GET /_manifest Content-Type = %q, want application/json", ct) } if body := rec.Body.String(); !strings.Contains(body, `"name": "Alarm"`) && !strings.Contains(body, `"name":"Alarm"`) { t.Fatalf("GET /_manifest body does not name the service: %s", body) } } // TestLogoutServesBothMethods pins the method-aware registration: GET (bodyless // no-op) and POST (revoke the refresh token) must both be routed, and any other // verb must be rejected with 405. The handler proxies to dcntrlized-auth, so the // requests carry a short deadline: the assertion is about routing, and the test // must not depend on (or wait for) a live auth service. func TestLogoutServesBothMethods(t *testing.T) { outer := newOuterMux(newAPIMux()) do := func(method string) int { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() req := httptest.NewRequest(method, "/auth/logout", nil).WithContext(ctx) rec := httptest.NewRecorder() outer.ServeHTTP(rec, req) return rec.Code } for _, method := range []string{http.MethodGet, http.MethodPost} { if code := do(method); code == http.StatusMethodNotAllowed || code == http.StatusNotFound { t.Fatalf("%s /auth/logout = %d, want the method to be routed", method, code) } } if code := do(http.MethodPut); code != http.StatusMethodNotAllowed { t.Fatalf("PUT /auth/logout = %d, want 405", code) } }