Add shared D&D registry resolver
This commit is contained in:
356
internal/modules/dnd/shared/registryresolver/resolver_test.go
Normal file
356
internal/modules/dnd/shared/registryresolver/resolver_test.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package registryresolver_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/registryresolver"
|
||||
)
|
||||
|
||||
const (
|
||||
testSlot = "registry"
|
||||
testMediaType = "application/json"
|
||||
)
|
||||
|
||||
type immutableView struct {
|
||||
value string
|
||||
identity string
|
||||
}
|
||||
|
||||
func TestResolverHandlesConstructionStateFallbackAndOverrides(t *testing.T) {
|
||||
config := resolverConfig(nil)
|
||||
absent, err := registryresolver.New(config, contracts.ReferenceSet{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
placeholder, err := registryresolver.New(config, referenceSet())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if absent.Seeded().value != "absent" || placeholder.Seeded().value != "absent" {
|
||||
t.Fatalf("seeded views = %#v / %#v, want absent", absent.Seeded(), placeholder.Seeded())
|
||||
}
|
||||
|
||||
seedContent := []byte("seeded")
|
||||
seeded, err := registryresolver.New(config, referenceSet(referenceItem(seedContent)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fallback, err := seeded.Resolve(contracts.ReferenceSet{})
|
||||
if err != nil || fallback != seeded.Seeded() {
|
||||
t.Fatalf("fallback = %p, %v; seeded = %p", fallback, err, seeded.Seeded())
|
||||
}
|
||||
override, err := seeded.Resolve(referenceSet(referenceItem([]byte("override"))))
|
||||
if err != nil || override == seeded.Seeded() || override.value != "override" {
|
||||
t.Fatalf("override = %#v, %v", override, err)
|
||||
}
|
||||
if _, err := seeded.Resolve(referenceSet()); err == nil || !strings.Contains(err.Error(), "exactly one item") {
|
||||
t.Fatalf("operation placeholder error = %v", err)
|
||||
}
|
||||
if _, err := registryresolver.New(config, referenceSet(referenceItem([]byte("one")), referenceItem([]byte("two")))); err == nil || !strings.Contains(err.Error(), "exactly one item") {
|
||||
t.Fatalf("construction cardinality error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOptionalSingleItemValidatesAndOwnsContent(t *testing.T) {
|
||||
spec := referenceSpec()
|
||||
if item, present, err := registryresolver.ResolveOptionalSingleItem(contracts.ReferenceSet{}, spec); err != nil || present || item.Content != nil {
|
||||
t.Fatalf("absent item = %#v, %t, %v", item, present, err)
|
||||
}
|
||||
|
||||
content := []byte("approved")
|
||||
set := referenceSet(contracts.ReferenceItem{
|
||||
SlotName: testSlot,
|
||||
MediaType: "Application/JSON; Charset=UTF-8",
|
||||
Content: content,
|
||||
Digest: "sha256:caller-supplied",
|
||||
Origin: contracts.ReferenceOrigin{Type: "file", URI: "/private/campaign.json"},
|
||||
})
|
||||
item, present, err := registryresolver.ResolveOptionalSingleItem(set, spec)
|
||||
if err != nil || !present || item.MediaType != testMediaType || string(item.Content) != "approved" {
|
||||
t.Fatalf("resolved item = %#v, %t, %v", item, present, err)
|
||||
}
|
||||
content[0] = 'X'
|
||||
if string(item.Content) != "approved" {
|
||||
t.Fatalf("resolved item retained caller bytes: %q", item.Content)
|
||||
}
|
||||
|
||||
secret := "private campaign material"
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
spec registryresolver.ReferenceSpec
|
||||
set contracts.ReferenceSet
|
||||
want string
|
||||
}{
|
||||
{"blank slot", registryresolver.ReferenceSpec{AcceptedMediaType: testMediaType, MaxBytes: 64}, contracts.ReferenceSet{}, "slot name"},
|
||||
{"invalid accepted media", registryresolver.ReferenceSpec{SlotName: testSlot, AcceptedMediaType: "not a type", MaxBytes: 64}, contracts.ReferenceSet{}, "accepted media type"},
|
||||
{"invalid maximum", registryresolver.ReferenceSpec{SlotName: testSlot, AcceptedMediaType: testMediaType}, contracts.ReferenceSet{}, "maximum size"},
|
||||
{"empty slot", spec, referenceSet(), "exactly one item"},
|
||||
{"multiple items", spec, referenceSet(referenceItem([]byte("one")), referenceItem([]byte("two"))), "exactly one item"},
|
||||
{"invalid item media", spec, referenceSet(contracts.ReferenceItem{MediaType: secret, Content: []byte(secret)}), "media type is invalid"},
|
||||
{"wrong item media", spec, referenceSet(contracts.ReferenceItem{MediaType: "text/plain", Content: []byte(secret)}), "must be application/json"},
|
||||
{"oversized", spec, referenceSet(referenceItem([]byte(strings.Repeat("x", 65)))), "limit 64"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, _, err := registryresolver.ResolveOptionalSingleItem(test.set, test.spec)
|
||||
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("error = %v, want %q", err, test.want)
|
||||
}
|
||||
if strings.Contains(err.Error(), secret) || strings.Contains(err.Error(), "/private/") {
|
||||
t.Fatalf("error exposed reference content or provenance: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverReusesRawSemanticAndSeededViewsByActualContent(t *testing.T) {
|
||||
var loads atomic.Int32
|
||||
resolver, err := registryresolver.New(resolverConfig(&loads), referenceSet(referenceItem([]byte("alpha"))))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if loads.Load() != 1 {
|
||||
t.Fatalf("construction loads = %d, want 1", loads.Load())
|
||||
}
|
||||
|
||||
seedEquivalent := referenceSet(referenceItem([]byte(" alpha ")))
|
||||
resolvedSeed, err := resolver.Resolve(seedEquivalent)
|
||||
if err != nil || resolvedSeed != resolver.Seeded() {
|
||||
t.Fatalf("semantic seeded reuse = %p, %v; want %p", resolvedSeed, err, resolver.Seeded())
|
||||
}
|
||||
if _, err := resolver.Resolve(seedEquivalent); err != nil || loads.Load() != 2 {
|
||||
t.Fatalf("raw seeded reuse loads = %d, err = %v; want 2", loads.Load(), err)
|
||||
}
|
||||
|
||||
betaSet := referenceSet(referenceItem([]byte("beta")))
|
||||
beta, err := resolver.Resolve(betaSet)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
betaAgain, err := resolver.Resolve(betaSet)
|
||||
if err != nil || betaAgain != beta {
|
||||
t.Fatalf("raw reuse = %p / %p, %v", beta, betaAgain, err)
|
||||
}
|
||||
betaEquivalent, err := resolver.Resolve(referenceSet(referenceItem([]byte("\n beta \t"))))
|
||||
if err != nil || betaEquivalent != beta {
|
||||
t.Fatalf("semantic reuse = %p / %p, %v", beta, betaEquivalent, err)
|
||||
}
|
||||
mediaVariant := referenceItem([]byte("beta"))
|
||||
mediaVariant.MediaType = "APPLICATION/JSON; charset=utf-8"
|
||||
if got, err := resolver.Resolve(referenceSet(mediaVariant)); err != nil || got != beta {
|
||||
t.Fatalf("normalized media raw reuse = %p / %p, %v", beta, got, err)
|
||||
}
|
||||
if loads.Load() != 4 {
|
||||
t.Fatalf("loads after raw and semantic reuse = %d, want 4", loads.Load())
|
||||
}
|
||||
|
||||
first := referenceItem([]byte("gamma"))
|
||||
second := referenceItem([]byte("delta"))
|
||||
first.Digest = "sha256:" + strings.Repeat("0", 64)
|
||||
second.Digest = first.Digest
|
||||
gamma, err := resolver.Resolve(referenceSet(first))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
delta, err := resolver.Resolve(referenceSet(second))
|
||||
if err != nil || gamma == delta || gamma.value == delta.value {
|
||||
t.Fatalf("caller digest aliased different bytes: %#v / %#v, %v", gamma, delta, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverSerializesConcurrentLoads(t *testing.T) {
|
||||
var loads atomic.Int32
|
||||
resolver, err := registryresolver.New(resolverConfig(&loads), contracts.ReferenceSet{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loads.Store(0)
|
||||
references := referenceSet(referenceItem([]byte("shared")))
|
||||
|
||||
const callers = 32
|
||||
results := make(chan *immutableView, callers)
|
||||
errors := make(chan error, callers)
|
||||
var group sync.WaitGroup
|
||||
for range callers {
|
||||
group.Add(1)
|
||||
go func() {
|
||||
defer group.Done()
|
||||
view, err := resolver.Resolve(references)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
results <- view
|
||||
}()
|
||||
}
|
||||
group.Wait()
|
||||
close(results)
|
||||
close(errors)
|
||||
for err := range errors {
|
||||
t.Error(err)
|
||||
}
|
||||
var first *immutableView
|
||||
for result := range results {
|
||||
if first == nil {
|
||||
first = result
|
||||
}
|
||||
if result != first {
|
||||
t.Fatalf("concurrent result = %p, want %p", result, first)
|
||||
}
|
||||
}
|
||||
if loads.Load() != 1 {
|
||||
t.Fatalf("concurrent loads = %d, want 1", loads.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverDoesNotCacheFailuresOrRetainCallerState(t *testing.T) {
|
||||
loaderFailure := errors.New("loader unavailable")
|
||||
var loads atomic.Int32
|
||||
var loadedBytes []byte
|
||||
config := resolverConfig(&loads)
|
||||
config.Load = func(content []byte) (*immutableView, error) {
|
||||
loads.Add(1)
|
||||
loadedBytes = content
|
||||
if string(content) == "fail" {
|
||||
return nil, loaderFailure
|
||||
}
|
||||
value := strings.TrimSpace(string(content))
|
||||
return &immutableView{value: value, identity: value}, nil
|
||||
}
|
||||
|
||||
seedBytes := []byte("seed")
|
||||
seedReferences := referenceSet(referenceItem(seedBytes))
|
||||
resolver, err := registryresolver.New(config, seedReferences)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedBytes[0] = 'X'
|
||||
delete(seedReferences.Slots, testSlot)
|
||||
if resolver.Seeded().value != "seed" || string(loadedBytes) != "seed" {
|
||||
t.Fatalf("construction retained caller state: %#v / %q", resolver.Seeded(), loadedBytes)
|
||||
}
|
||||
|
||||
failing := referenceSet(referenceItem([]byte("fail")))
|
||||
for range 2 {
|
||||
if _, err := resolver.Resolve(failing); !errors.Is(err, loaderFailure) {
|
||||
t.Fatalf("loader error = %v", err)
|
||||
}
|
||||
}
|
||||
if loads.Load() != 3 {
|
||||
t.Fatalf("loads after repeated failure = %d, want 3", loads.Load())
|
||||
}
|
||||
|
||||
operationBytes := []byte("operation")
|
||||
operationReferences := referenceSet(referenceItem(operationBytes))
|
||||
view, err := resolver.Resolve(operationReferences)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
operationBytes[0] = 'X'
|
||||
operationReferences.Slots[testSlot] = contracts.ResolvedReferenceSlot{}
|
||||
if view.value != "operation" || string(loadedBytes) != "operation" {
|
||||
t.Fatalf("operation retained caller state: %#v / %q", view, loadedBytes)
|
||||
}
|
||||
|
||||
var invalidLoads atomic.Int32
|
||||
config.Load = func([]byte) (*immutableView, error) {
|
||||
invalidLoads.Add(1)
|
||||
return &immutableView{value: "invalid"}, nil
|
||||
}
|
||||
missingIdentity, err := registryresolver.New(config, contracts.ReferenceSet{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for range 2 {
|
||||
if _, err := missingIdentity.Resolve(referenceSet(referenceItem([]byte("invalid")))); err == nil || !strings.Contains(err.Error(), "semantic identity") {
|
||||
t.Fatalf("missing identity error = %v", err)
|
||||
}
|
||||
}
|
||||
if invalidLoads.Load() != 2 {
|
||||
t.Fatalf("invalid view loads = %d, want 2", invalidLoads.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewValidatesCallbacksAndDoesNotCacheAbsentFailure(t *testing.T) {
|
||||
base := resolverConfig(nil)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
mutate func(*registryresolver.Config[*immutableView])
|
||||
want string
|
||||
}{
|
||||
{"absent", func(config *registryresolver.Config[*immutableView]) { config.Absent = nil }, "absent-view"},
|
||||
{"loader", func(config *registryresolver.Config[*immutableView]) { config.Load = nil }, "loader"},
|
||||
{"identity", func(config *registryresolver.Config[*immutableView]) { config.SemanticIdentity = nil }, "semantic-identity"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
config := base
|
||||
test.mutate(&config)
|
||||
if _, err := registryresolver.New(config, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("New() error = %v, want %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
absentFailure := errors.New("absent unavailable")
|
||||
base.Absent = func() (*immutableView, error) { return nil, absentFailure }
|
||||
if _, err := registryresolver.New(base, contracts.ReferenceSet{}); !errors.Is(err, absentFailure) {
|
||||
t.Fatalf("absent error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func resolverConfig(loads *atomic.Int32) registryresolver.Config[*immutableView] {
|
||||
return registryresolver.Config[*immutableView]{
|
||||
Reference: referenceSpec(),
|
||||
Absent: func() (*immutableView, error) {
|
||||
return &immutableView{value: "absent", identity: "absent"}, nil
|
||||
},
|
||||
Load: func(content []byte) (*immutableView, error) {
|
||||
if loads != nil {
|
||||
loads.Add(1)
|
||||
}
|
||||
value := strings.TrimSpace(string(content))
|
||||
return &immutableView{value: value, identity: "identity:" + value}, nil
|
||||
},
|
||||
SemanticIdentity: func(view *immutableView) string {
|
||||
if view == nil {
|
||||
return ""
|
||||
}
|
||||
return view.identity
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func referenceSpec() registryresolver.ReferenceSpec {
|
||||
return registryresolver.ReferenceSpec{SlotName: testSlot, AcceptedMediaType: testMediaType, MaxBytes: 64}
|
||||
}
|
||||
|
||||
func referenceItem(content []byte) contracts.ReferenceItem {
|
||||
return contracts.ReferenceItem{SlotName: testSlot, MediaType: testMediaType, Content: content}
|
||||
}
|
||||
|
||||
func referenceSet(items ...contracts.ReferenceItem) contracts.ReferenceSet {
|
||||
return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||
testSlot: {Slot: contracts.ReferenceSlot{Name: testSlot}, Items: items},
|
||||
}}
|
||||
}
|
||||
|
||||
func TestErrorsDoNotFormatReferenceData(t *testing.T) {
|
||||
secret := "hidden campaign notes"
|
||||
config := resolverConfig(nil)
|
||||
config.Load = func([]byte) (*immutableView, error) { return nil, fmt.Errorf("safe loader failure") }
|
||||
resolver, err := registryresolver.New(config, contracts.ReferenceSet{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
item := referenceItem([]byte(secret))
|
||||
item.Origin.URI = "/private/campaign.json"
|
||||
_, err = resolver.Resolve(referenceSet(item))
|
||||
if err == nil || strings.Contains(err.Error(), secret) || strings.Contains(err.Error(), item.Origin.URI) {
|
||||
t.Fatalf("error exposed reference data: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user