NextMyDay/internal/core/routing/fakes_test.go
Claus Lohmar e114a2be61 feat(routing): add the pure routing model (SPEC-002)
Turns a request path plus the two routing fields (`do`/`action`) into a single
Resolution for BOTH doors (D-039): the RESERVED level-1 switch first
(home/access/setup/api/static/media/ws/dev — D-004), then the GUID classifier,
then the registry. Identity and the module registry are injected interfaces, so
the package is PURE — no HTTP stack, no DB (asserted by purity_test.go). All 22
spec rows are covered.
2026-10-14 10:53:48 +00:00

110 lines
2.9 KiB
Go

package routing
// This file holds in-memory fakes for the two injected dependencies. It must
// import no HTTP and no DB package (SPEC-002 §5, the purity assertion).
import (
"fmt"
"testing"
)
// fakeResolver is an in-memory Resolver: guid -> enrolled. A missing guid is
// "not found". It records the guids it was asked about.
type fakeResolver struct {
users map[string]bool // guid -> enrolled
calls []string
}
func (f *fakeResolver) GUID(guid string) (bool, bool) {
f.calls = append(f.calls, guid)
enrolled, found := f.users[guid]
return found, enrolled
}
// forbiddenResolver fails the test if it is ever called. It proves the reserved
// switch never touches identity (SPEC-002 §3.5 / test row 2).
type forbiddenResolver struct {
t *testing.T
}
func (r forbiddenResolver) GUID(guid string) (bool, bool) {
r.t.Helper()
r.t.Errorf("resolver must not be called (guid=%q)", guid)
return false, false
}
// fakeModule is an in-memory Module: a function whitelist, an optional numeric
// classifier, a default function, and an action whitelist.
type fakeModule struct {
name string
deflt string // function for an empty/absent PARAM3
routes map[string]string // PARAM3 -> function
numeric bool // classify numeric PARAM3 as "detail"
actions map[string]string // action -> function
}
func (m *fakeModule) Name() string { return m.name }
func (m *fakeModule) Route(function string, tail Params) (string, Params, bool) {
if function == "" {
return m.deflt, tail, true
}
if fn, ok := m.routes[function]; ok {
return fn, tail, true
}
if m.numeric && IsNumeric(function) {
return "detail", Params{function}, true
}
return "", nil, false
}
func (m *fakeModule) Action(action string) (string, bool) {
fn, ok := m.actions[action]
return fn, ok
}
// fakeRegistry is an in-memory Registry.
type fakeRegistry map[string]Module
func (r fakeRegistry) Module(name string) (Module, bool) {
m, ok := r[name]
return m, ok
}
// The module set used by the test matrix (SPEC-002 §3.7 uses `requests`).
var (
homeModule = &fakeModule{name: "home", deflt: "dashboard"}
requestsModule = &fakeModule{
name: "requests",
deflt: "list",
routes: map[string]string{
"open": "open",
"closed": "closed",
"pto": "pto",
"equipment": "equipment",
},
numeric: true,
actions: map[string]string{"submit": "submit"},
}
hrModule = &fakeModule{
name: "hr",
routes: map[string]string{"save-data": "save-data"},
actions: map[string]string{"save-data": "save-data"},
}
)
var testRegistry = fakeRegistry{
"home": homeModule,
"requests": requestsModule,
"hr": hrModule,
}
// resolveFmt renders a Resolution compactly for failure messages.
func resolveFmt(r Resolution) string {
return fmt.Sprintf(
"{Kind:%s Workflow:%q Module:%q Function:%q Params:%v Redirect:%q}",
r.Kind, r.Workflow, r.Module, r.Function, []string(r.Params), r.Redirect,
)
}