Routing, html/template layout/content pattern, and the Pre-Publish check flow: POST /pruefen runs extraction (Stufe 1) then rules.Evaluate (Stufe 2) and renders the result as an htmx fragment. Nothing is persisted yet — that's the next step (wiring internal/store in). The needsClarification case is rendered explicitly as a request for more information rather than "no findings", matching the core principle. Every result carries the legal-advice disclaimer required by CLAUDE.md's guardrails. Server depends on a narrow Extractor interface rather than *extract. Client directly, so tests inject a fake instead of calling the real API — internal/web's test suite never touches the network. htmx is vendored locally (internal/web/static/htmx.min.js) instead of loaded from a CDN, keeping the UI usable without runtime internet access. cmd/deklarix/main.go now wires all of this together: reads ANTHROPIC_API_KEY (required) and RULES_DIR (default "rules"), builds the extract client and loads the rule set, and serves web.Server instead of the old inline health-only mux. This exposed the same crash-loop risk fixed earlier for DATABASE_URL: postinst's start guard only checked DATABASE_URL, so a fresh install would now crash-loop on a missing ANTHROPIC_API_KEY instead. The guard checks both. scripts/build.sh also now ships rules/*.yaml into the .deb under /usr/share/deklarix/rules (not a conffile — rules are updated via the release pipeline, never hand-edited on a server), and deklarix.env.example points RULES_DIR there by default. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
184 lines
4.9 KiB
Go
184 lines
4.9 KiB
Go
package web_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/netcell-it/deklarix/internal/extract"
|
|
"github.com/netcell-it/deklarix/internal/rules"
|
|
"github.com/netcell-it/deklarix/internal/web"
|
|
)
|
|
|
|
type fakeExtractor struct {
|
|
facts rules.Facts
|
|
err error
|
|
}
|
|
|
|
func (f fakeExtractor) Extract(ctx context.Context, in extract.Input) (rules.Facts, error) {
|
|
return f.facts, f.err
|
|
}
|
|
|
|
func loadRealRules(t *testing.T) []rules.Rule {
|
|
t.Helper()
|
|
rs, err := rules.Load(os.DirFS("../../rules"))
|
|
if err != nil {
|
|
t.Fatalf("rules.Load: %v", err)
|
|
}
|
|
return rs
|
|
}
|
|
|
|
func newTestServer(t *testing.T, ex web.Extractor) *web.Server {
|
|
t.Helper()
|
|
s, err := web.NewServer(ex, loadRealRules(t))
|
|
if err != nil {
|
|
t.Fatalf("NewServer: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestHandleHealth(t *testing.T) {
|
|
s := newTestServer(t, fakeExtractor{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
|
w := httptest.NewRecorder()
|
|
s.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
if w.Body.String() != `{"ok":true}` {
|
|
t.Fatalf("body = %q, want {\"ok\":true}", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleIndexRendersForm(t *testing.T) {
|
|
s := newTestServer(t, fakeExtractor{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
s.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
body := w.Body.String()
|
|
for _, want := range []string{`name="platform"`, `name="caption"`, `hx-post="/pruefen"`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("index body missing %q\nbody: %s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleStaticServesHTMX(t *testing.T) {
|
|
s := newTestServer(t, fakeExtractor{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/static/htmx.min.js", nil)
|
|
w := httptest.NewRecorder()
|
|
s.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
if w.Body.Len() < 1000 {
|
|
t.Fatalf("htmx.min.js suspiciously small: %d bytes", w.Body.Len())
|
|
}
|
|
}
|
|
|
|
func postCheck(t *testing.T, s *web.Server, form url.Values) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodPost, "/pruefen", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
w := httptest.NewRecorder()
|
|
s.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
func TestHandleCheckRejectsMissingFields(t *testing.T) {
|
|
s := newTestServer(t, fakeExtractor{})
|
|
|
|
w := postCheck(t, s, url.Values{"platform": {"instagram"}})
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400 for missing caption", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleCheckPropagatesExtractionError(t *testing.T) {
|
|
s := newTestServer(t, fakeExtractor{err: errTest})
|
|
|
|
w := postCheck(t, s, url.Values{"platform": {"instagram"}, "caption": {"x"}})
|
|
if w.Code != http.StatusBadGateway {
|
|
t.Fatalf("status = %d, want 502 when extraction fails", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleCheckRendersFinding(t *testing.T) {
|
|
s := newTestServer(t, fakeExtractor{facts: rules.Facts{
|
|
Platform: "instagram",
|
|
Jurisdiction: "DE",
|
|
Consideration: rules.ConsiderationPaid,
|
|
DisclosurePresent: true,
|
|
DisclosureWording: "Werbung",
|
|
DisclosureBeforeCut: false,
|
|
}})
|
|
|
|
w := postCheck(t, s, url.Values{"platform": {"instagram"}, "caption": {"Werbung, schaut mal..."}})
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "WK-004") {
|
|
t.Errorf("expected WK-004 finding in result, got: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestHandleCheckRendersNeedsClarification(t *testing.T) {
|
|
s := newTestServer(t, fakeExtractor{facts: rules.Facts{
|
|
Platform: "instagram",
|
|
Jurisdiction: "DE",
|
|
Consideration: rules.ConsiderationUnclear,
|
|
}})
|
|
|
|
w := postCheck(t, s, url.Values{"platform": {"instagram"}, "caption": {"..."}})
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
body := w.Body.String()
|
|
if strings.Contains(body, "WK-") {
|
|
t.Errorf("expected no rule findings for an unclear extraction, got: %s", body)
|
|
}
|
|
if !strings.Contains(body, "nicht sicher bestimmt") {
|
|
t.Errorf("expected a clarification message, got: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestHandleCheckCleanCaseHasNoFindings(t *testing.T) {
|
|
s := newTestServer(t, fakeExtractor{facts: rules.Facts{
|
|
Platform: "instagram",
|
|
Jurisdiction: "DE",
|
|
Consideration: rules.ConsiderationNone,
|
|
}})
|
|
|
|
w := postCheck(t, s, url.Values{"platform": {"instagram"}, "caption": {"ein ganz normaler Post"}})
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
body := w.Body.String()
|
|
if strings.Contains(body, "WK-") {
|
|
t.Errorf("expected no findings for an organic post, got: %s", body)
|
|
}
|
|
if !strings.Contains(body, "Keine Kennzeichnungsrisiken") {
|
|
t.Errorf("expected the no-findings message, got: %s", body)
|
|
}
|
|
}
|
|
|
|
var errTest = extractError("simulierter Extraktionsfehler")
|
|
|
|
type extractError string
|
|
|
|
func (e extractError) Error() string { return string(e) }
|