Files
silo-server/internal/settingsresolve/conformance_test.go
QuickandGitHub 3bdfc58512 feat(settings): sync navigation and card customization by client family (#538)
* test(web): use safe auth placeholders

* feat(settings): sync navigation and card customization

* fix(settings): address customization review feedback

* fix(settings): address customization review feedback

* fix(settings): harden customization capability handling
2026-08-04 08:20:41 -04:00

316 lines
11 KiB
Go

package settingsresolve
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/Silo-Server/silo-server/internal/settingscontract"
"github.com/Silo-Server/silo-server/internal/userstore"
)
// This is the Go runner for the cross-platform conformance fixture in
// contracts/settings/v1/conformance.json — the spec's named drift gate. The
// same hand-authored cases run against the TypeScript resolver
// (web/src/lib/settingsConformance.test.ts) and, in later phases, against the
// Kotlin and Swift resolvers in the client repos. Four independently written
// resolvers agreeing on these cases is the whole point, so this runner takes
// the fixture at face value: it decodes strictly (an unknown fixture field is
// itself drift), resolves through the real resolver against the real embedded
// manifest, and compares every declared expectation.
type conformanceFixture struct {
FixtureVersion int `json:"fixture_version"`
ManifestRevision int `json:"manifest_revision"`
Description string `json:"description"`
Cases []conformanceCase `json:"cases"`
}
type conformanceCase struct {
Name string `json:"name"`
Description string `json:"description"`
Keys []string `json:"keys"`
Context *conformanceContext `json:"context"`
Stored []conformanceRow `json:"stored"`
// Constraints are policy inputs by name, as the policy layer would supply
// them. Keys here are data, not schema, so they are not field-checked.
Constraints map[string]json.RawMessage `json:"constraints"`
// ConstraintBindings attach a constraint to a copy of a real definition,
// so constraint kinds no shipped definition carries stay testable.
ConstraintBindings []conformanceBinding `json:"constraint_bindings"`
Expected []conformanceExpected `json:"expected"`
}
type conformanceContext struct {
ProfileID string `json:"profile_id"`
ClientFamily string `json:"client_family"`
DeviceID string `json:"device_id"`
LibraryIDs []int `json:"library_ids"`
SeriesIDs []string `json:"series_ids"`
}
type conformanceRow struct {
Key string `json:"key"`
Scope string `json:"scope"`
ProfileID string `json:"profile_id"`
ClientFamily string `json:"client_family"`
DeviceID string `json:"device_id"`
LibraryID int `json:"library_id"`
SeriesID string `json:"series_id"`
Value json.RawMessage `json:"value"`
}
type conformanceBinding struct {
Key string `json:"key"`
PolicyInput string `json:"policy_input"`
Constraint string `json:"constraint"`
}
type conformanceExpected struct {
Key string `json:"key"`
Value json.RawMessage `json:"value"`
Source string `json:"source"`
// Constrained defaults to false. When true, StoredValue and ConstraintKind
// must be present; StoredValue may be JSON null for an authored null.
Constrained bool `json:"constrained"`
StoredValue json.RawMessage `json:"stored_value"`
ConstraintKind string `json:"constraint_kind"`
}
func loadConformanceFixture(t *testing.T) conformanceFixture {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", "contracts", "settings", "v1", "conformance.json"))
if err != nil {
t.Fatalf("reading conformance fixture: %v", err)
}
// DisallowUnknownFields is the fixture's own drift gate: a field this
// runner does not know is either a typo or a semantic another platform
// would silently skip, and both must fail loudly.
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
var fixture conformanceFixture
if err := dec.Decode(&fixture); err != nil {
t.Fatalf("decoding conformance fixture: %v", err)
}
if dec.More() {
t.Fatal("conformance fixture has trailing content after the fixture object")
}
return fixture
}
func TestConformanceFixture(t *testing.T) {
fixture := loadConformanceFixture(t)
manifest := mustContract(t)
if fixture.FixtureVersion != 1 {
t.Fatalf("fixture_version = %d, this runner understands 1", fixture.FixtureVersion)
}
// Expectations are derived from one specific manifest. A revision bump
// changes definitions, so the fixture author must re-confirm every case.
if fixture.ManifestRevision != manifest.Revision {
t.Fatalf("fixture targets manifest revision %d but the embedded manifest is revision %d; re-derive the fixture expectations",
fixture.ManifestRevision, manifest.Revision)
}
if len(fixture.Cases) == 0 {
t.Fatal("conformance fixture declares no cases")
}
seen := make(map[string]bool, len(fixture.Cases))
for _, tc := range fixture.Cases {
if tc.Name == "" {
t.Fatal("conformance case with no name")
}
if seen[tc.Name] {
t.Fatalf("duplicate conformance case name %q", tc.Name)
}
seen[tc.Name] = true
t.Run(tc.Name, func(t *testing.T) {
runConformanceCase(t, manifest, tc)
})
}
}
func runConformanceCase(t *testing.T, manifest *settingscontract.Manifest, tc conformanceCase) {
t.Helper()
if len(tc.Keys) == 0 {
t.Fatal("case declares no keys")
}
if len(tc.Expected) == 0 {
t.Fatal("case declares no expectations")
}
rc := Context{}
if tc.Context != nil {
rc = Context{
ProfileID: tc.Context.ProfileID,
ClientFamily: settingscontract.ClientFamily(tc.Context.ClientFamily),
DeviceID: tc.Context.DeviceID,
LibraryIDs: tc.Context.LibraryIDs,
SeriesIDs: tc.Context.SeriesIDs,
}
}
rows := make([]userstore.SettingValue, 0, len(tc.Stored))
for i, stored := range tc.Stored {
if stored.Value == nil {
t.Fatalf("stored[%d] has no value; an authored null must be spelled null", i)
}
rows = append(rows, userstore.SettingValue{
SettingIdentity: userstore.SettingIdentity{
Key: stored.Key,
Scope: settingscontract.Scope(stored.Scope),
ProfileID: stored.ProfileID,
ClientFamily: settingscontract.ClientFamily(stored.ClientFamily),
DeviceID: stored.DeviceID,
LibraryID: stored.LibraryID,
SeriesID: stored.SeriesID,
},
Value: stored.Value,
})
}
bindings, err := conformanceBindings(manifest, tc.ConstraintBindings)
if err != nil {
t.Fatal(err)
}
constraints := Constraints(tc.Constraints)
resolver := New(manifest)
store := &fakeStore{rows: rows}
var effs []Effective
if len(bindings) == 0 {
// The production path: Resolve applies whatever constraints the
// manifest itself binds.
effs, err = resolver.Resolve(context.Background(), store, rc, tc.Keys, constraints)
} else {
// Bound cases resolve unconstrained, then apply the constraint against
// a copy of the real definition carrying the injected binding —
// mirroring what Resolve does internally without mutating the shared
// loaded manifest.
effs, err = resolver.Resolve(context.Background(), store, rc, tc.Keys, nil)
if err == nil {
for i, eff := range effs {
def, ok := manifest.Lookup(eff.Key)
if !ok {
t.Fatalf("resolved key %q is not in the manifest", eff.Key)
}
bound := *def
if binding, has := bindings[eff.Key]; has {
bound.ConstrainedBy = binding
}
effs[i] = applyConstraint(&bound, eff, constraints)
}
}
}
if err != nil {
t.Fatalf("Resolve: %v", err)
}
if len(effs) != len(tc.Expected) {
t.Fatalf("resolved %d settings, fixture expects %d", len(effs), len(tc.Expected))
}
byKey := make(map[string]Effective, len(effs))
for _, eff := range effs {
byKey[eff.Key] = eff
}
for _, exp := range tc.Expected {
eff, ok := byKey[exp.Key]
if !ok {
t.Errorf("%s: no resolved value", exp.Key)
continue
}
if exp.Value == nil {
t.Errorf("%s: expectation has no value; an expected null must be spelled null", exp.Key)
continue
}
if !jsonEquivalent(eff.Value, exp.Value) {
t.Errorf("%s: value = %s, want %s", exp.Key, eff.Value, exp.Value)
}
if string(eff.Source) != exp.Source {
t.Errorf("%s: source = %q, want %q", exp.Key, eff.Source, exp.Source)
}
if eff.Constrained != exp.Constrained {
t.Errorf("%s: constrained = %v, want %v", exp.Key, eff.Constrained, exp.Constrained)
}
if string(eff.ConstraintKind) != exp.ConstraintKind {
t.Errorf("%s: constraint_kind = %q, want %q", exp.Key, eff.ConstraintKind, exp.ConstraintKind)
}
if exp.Constrained {
if exp.StoredValue == nil {
t.Errorf("%s: a constrained expectation must declare stored_value", exp.Key)
} else if !jsonEquivalent(eff.StoredValue, exp.StoredValue) {
t.Errorf("%s: stored_value = %s, want %s", exp.Key, eff.StoredValue, exp.StoredValue)
}
if exp.ConstraintKind == "" {
t.Errorf("%s: a constrained expectation must declare constraint_kind", exp.Key)
}
} else {
if exp.StoredValue != nil {
t.Errorf("%s: an unconstrained expectation must not declare stored_value", exp.Key)
}
if eff.StoredValue != nil {
t.Errorf("%s: stored_value = %s reported without a constraint", exp.Key, eff.StoredValue)
}
}
}
}
// conformanceBindings validates the case's injected bindings against the
// manifest and the known constraint kinds.
func conformanceBindings(
manifest *settingscontract.Manifest,
declared []conformanceBinding,
) (map[string]*settingscontract.Constraint, error) {
if len(declared) == 0 {
return nil, nil
}
kinds := map[settingscontract.ConstraintKind]bool{
settingscontract.ConstraintCeiling: true,
settingscontract.ConstraintFloor: true,
settingscontract.ConstraintAllowlist: true,
settingscontract.ConstraintLocked: true,
}
out := make(map[string]*settingscontract.Constraint, len(declared))
for _, binding := range declared {
if _, ok := manifest.Lookup(binding.Key); !ok {
return nil, fmt.Errorf("constraint binding names unknown key %q", binding.Key)
}
kind := settingscontract.ConstraintKind(binding.Constraint)
if !kinds[kind] {
return nil, fmt.Errorf("constraint binding on %q names unknown kind %q",
binding.Key, binding.Constraint)
}
if binding.PolicyInput == "" {
return nil, fmt.Errorf("constraint binding on %q has no policy_input", binding.Key)
}
if _, dup := out[binding.Key]; dup {
return nil, fmt.Errorf("duplicate constraint binding for %q", binding.Key)
}
out[binding.Key] = &settingscontract.Constraint{
PolicyInput: binding.PolicyInput,
Constraint: kind,
}
}
return out, nil
}
// jsonEquivalent compares two raw JSON values structurally, so formatting and
// object key order never matter.
func jsonEquivalent(a, b json.RawMessage) bool {
var av, bv any
if err := json.Unmarshal(a, &av); err != nil {
return false
}
if err := json.Unmarshal(b, &bv); err != nil {
return false
}
return reflect.DeepEqual(av, bv)
}