Registry.ClaimExternal now enforces the plan gate itself via an injected domains.ExternalClaimGate (pre-lock, typed refusals), so every entry point — and any future consumer — inherits it from the allocation API. One constructor in internal/server builds the gate from the entitlements querier and connect target; it is injected into the member-facing registry constructions in server.go and fedwiki.go and drives affordance rendering on both surfaces. The duplicated helpers and resource-key constants in fedwiki web and member_domains are gone; fedwiki no longer reads entitlement tables for this gate at all. Archives the change with the domains-registry spec delta (enforcement location is now requirement-level: registry-inherited, surfaces derive). Closes the entitlement-gate placement debt in issues.md; files the separately-discovered operator force-release dead-end affordance bug that a placed claim exposed in the domains walkthrough.
61 lines
2.3 KiB
Go
61 lines
2.3 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"testing"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/domains"
|
|
)
|
|
|
|
// TestNewExternalClaimGateRefusals pins the gate's two refusal conditions in
|
|
// the order they short-circuit: no connect target refuses regardless of
|
|
// entitlement state (no serving path exists), and an absent entitlement
|
|
// source reads as not granted, never as an error.
|
|
func TestNewExternalClaimGateRefusals(t *testing.T) {
|
|
noTarget := NewExternalClaimGate(nil, " ")
|
|
if err := noTarget(context.Background(), "ws-1"); !errors.Is(err, domains.ErrNoConnectTarget) {
|
|
t.Errorf("empty connect target: got %v, want ErrNoConnectTarget", err)
|
|
}
|
|
|
|
noEntQ := NewExternalClaimGate(nil, "connect.example.test")
|
|
if err := noEntQ(context.Background(), "ws-1"); !errors.Is(err, domains.ErrExternalClaimsNotEntitled) {
|
|
t.Errorf("nil entitlements querier: got %v, want ErrExternalClaimsNotEntitled", err)
|
|
}
|
|
}
|
|
|
|
// TestExternalClaimGateRefusedClassification keeps the refusal/infrastructure
|
|
// distinction honest — surfaces close the affordance on refusals but must
|
|
// surface real failures.
|
|
func TestExternalClaimGateRefusedClassification(t *testing.T) {
|
|
if !externalClaimGateRefused(domains.ErrExternalClaimsNotEntitled) ||
|
|
!externalClaimGateRefused(domains.ErrNoConnectTarget) {
|
|
t.Error("typed refusals must classify as refused")
|
|
}
|
|
if externalClaimGateRefused(errors.New("connection reset")) {
|
|
t.Error("an infrastructure error must not classify as a refusal")
|
|
}
|
|
if externalClaimGateRefused(nil) {
|
|
t.Error("nil is admission, not refusal")
|
|
}
|
|
}
|
|
|
|
// TestMemberDomainsHandlerCarriesGate pins the constructor wiring: a member
|
|
// domains handler always holds a gate (built from its own config), so the
|
|
// affordance read can never dereference nil. The registry-side injections in
|
|
// server.go and fedwiki.go are exercised end-to-end by the walkthroughs.
|
|
func TestMemberDomainsHandlerCarriesGate(t *testing.T) {
|
|
h, err := NewMemberDomainsHandler(MemberDomainsConfig{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))})
|
|
if err != nil {
|
|
t.Fatalf("construct handler: %v", err)
|
|
}
|
|
if h.gate == nil {
|
|
t.Fatal("member domains handler constructed without a gate")
|
|
}
|
|
if err := h.gate(context.Background(), "ws-1"); !errors.Is(err, domains.ErrNoConnectTarget) {
|
|
t.Errorf("gate with empty config: got %v, want ErrNoConnectTarget", err)
|
|
}
|
|
}
|