carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8)
9
10func TestFeaturesRoundTrip(t *testing.T) {
11 ctx := context.Background()
12 s := NewDirStore(t.TempDir())
13 now := time.Date(2026, 8, 28, 12, 0, 0, 0, time.UTC)
14
15 // Absent is legal forever: no features, not an error.
16 got, err := GetFeatures(ctx, s, "bab", "titogo")
17 if err != nil || got != nil {
18 t.Fatalf("absent features = %v, %v; want nil, nil", got, err)
19 }
20
21 rec, changed, err := UpdateFeatures(ctx, s, "bab", "titogo", map[string]string{"sandbox": "on"}, nil, "alice@keymail.dev", now)
22 if err != nil {
23 t.Fatal(err)
24 }
25 if !changed || rec.Features["sandbox"] != "on" || rec.SetBy != "alice@keymail.dev" || !rec.SetAt.Equal(now) {
26 t.Fatalf("first set = %+v changed=%v, want sandbox=on by alice, changed", rec, changed)
27 }
28 got, err = GetFeatures(ctx, s, "bab", "titogo")
29 if err != nil {
30 t.Fatal(err)
31 }
32 if got == nil || got.Features["sandbox"] != "on" || got.Schema != 1 || got.App != "titogo" {
33 t.Fatalf("features = %+v, want schema 1 titogo sandbox=on", got)
34 }
35
36 // A second set MERGES: the existing key survives, the new one lands.
37 rec, changed, err = UpdateFeatures(ctx, s, "bab", "titogo", map[string]string{"beta": "1"}, nil, "bob@keymail.dev", now.Add(time.Hour))
38 if err != nil || !changed {
39 t.Fatalf("merge set: changed=%v err=%v", changed, err)
40 }
41 if rec.Features["sandbox"] != "on" || rec.Features["beta"] != "1" || rec.SetBy != "bob@keymail.dev" {
42 t.Fatalf("after merge = %+v, want both keys, set by bob", rec)
43 }
44
45 // Setting the same value again changes nothing — and says so, so the
46 // door can skip the audit row.
47 _, changed, err = UpdateFeatures(ctx, s, "bab", "titogo", map[string]string{"beta": "1"}, nil, "carol", now.Add(2*time.Hour))
48 if err != nil || changed {
49 t.Fatalf("no-op set: changed=%v err=%v, want false, nil", changed, err)
50 }
51 got, _ = GetFeatures(ctx, s, "bab", "titogo")
52 if got.SetBy != "bob@keymail.dev" {
53 t.Fatalf("a no-op set rewrote the record: set_by %q", got.SetBy)
54 }
55
56 // Unset removes a key; unsetting an absent key is a no-op.
57 rec, changed, err = UpdateFeatures(ctx, s, "bab", "titogo", nil, []string{"beta", "never"}, "dave", now.Add(3*time.Hour))
58 if err != nil || !changed {
59 t.Fatalf("unset: changed=%v err=%v", changed, err)
60 }
61 if _, ok := rec.Features["beta"]; ok || rec.Features["sandbox"] != "on" {
62 t.Fatalf("after unset = %+v, want only sandbox", rec.Features)
63 }
64 _, changed, err = UpdateFeatures(ctx, s, "bab", "titogo", nil, []string{"never"}, "dave", now.Add(4*time.Hour))
65 if err != nil || changed {
66 t.Fatalf("unset absent: changed=%v err=%v, want false, nil", changed, err)
67 }
68
69 // Unsetting the last key leaves an EMPTY record, not an absent one: the
70 // record's set_at still tells a reader when the last change happened.
71 rec, _, err = UpdateFeatures(ctx, s, "bab", "titogo", nil, []string{"sandbox"}, "erin", now.Add(5*time.Hour))
72 if err != nil || len(rec.Features) != 0 {
73 t.Fatalf("after last unset = %+v err=%v", rec, err)
74 }
75 got, err = GetFeatures(ctx, s, "bab", "titogo")
76 if err != nil || got == nil || len(got.Features) != 0 || got.SetBy != "erin" {
77 t.Fatalf("empty record = %+v, %v; want present, empty, by erin", got, err)
78 }
79}
80
81// Keys and values are the app's own vocabulary — but they are also bucket
82// bytes every consumer parses, so the shape is bounded: short lowercase
83// keys, short printable values, and a cap on how many.
84func TestUpdateFeaturesRefusesBadShapes(t *testing.T) {
85 ctx := context.Background()
86 s := NewDirStore(t.TempDir())
87 now := time.Now()
88 cases := []struct {
89 name string
90 set map[string]string
91 want string
92 }{
93 {"uppercase key", map[string]string{"Sandbox": "on"}, "key"},
94 {"empty key", map[string]string{"": "on"}, "key"},
95 {"key with space", map[string]string{"sand box": "on"}, "key"},
96 {"key too long", map[string]string{strings.Repeat("k", 65): "on"}, "key"},
97 {"empty value", map[string]string{"sandbox": ""}, "value"},
98 {"value too long", map[string]string{"sandbox": strings.Repeat("v", 257)}, "value"},
99 {"control char in value", map[string]string{"sandbox": "on\n"}, "value"},
100 }
101 for _, c := range cases {
102 t.Run(c.name, func(t *testing.T) {
103 _, _, err := UpdateFeatures(ctx, s, "bab", "titogo", c.set, nil, "x", now)
104 if err == nil || !strings.Contains(err.Error(), c.want) {
105 t.Fatalf("err = %v, want one naming the %s", err, c.want)
106 }
107 })
108 }
109 if got, err := GetFeatures(ctx, s, "bab", "titogo"); err != nil || got != nil {
110 t.Fatalf("a refused set wrote something: %+v, %v", got, err)
111 }
112
113 // The count cap: FeaturesMax keys is fine, one more is refused —
114 // whether they arrive at once or on top of an existing record.
115 set := map[string]string{}
116 for i := 0; i < FeaturesMax; i++ {
117 set["k"+strings.Repeat("x", i%3)+string(rune('a'+i%26))+string(rune('a'+i/26))] = "v"
118 }
119 if len(set) != FeaturesMax {
120 t.Fatalf("test bug: %d keys", len(set))
121 }
122 if _, _, err := UpdateFeatures(ctx, s, "bab", "titogo", set, nil, "x", now); err != nil {
123 t.Fatalf("%d keys refused: %v", FeaturesMax, err)
124 }
125 _, _, err := UpdateFeatures(ctx, s, "bab", "titogo", map[string]string{"one-more": "v"}, nil, "x", now)
126 if err == nil || !strings.Contains(err.Error(), "at most") {
127 t.Fatalf("key %d = %v, want a cap error", FeaturesMax+1, err)
128 }
129 // Replacing an existing key at the cap is not an addition.
130 if _, _, err := UpdateFeatures(ctx, s, "bab", "titogo", map[string]string{"kaa": "changed"}, nil, "x", now); err != nil {
131 t.Fatalf("replacing at the cap refused: %v", err)
132 }
133}
134
135func TestGetFeaturesRefusesLiesAndUnknownSchema(t *testing.T) {
136 ctx := context.Background()
137 s := NewDirStore(t.TempDir())
138 put := func(body string) {
139 s.Delete(ctx, FeaturesKey("bab", "titogo"))
140 if _, err := s.Put(ctx, FeaturesKey("bab", "titogo"), []byte(body), ""); err != nil {
141 t.Fatal(err)
142 }
143 }
144 put(`{"schema":1,"app":"other","features":{"a":"b"},"set_at":"2026-08-28T12:00:00Z"}`)
145 if _, err := GetFeatures(ctx, s, "bab", "titogo"); err == nil {
146 t.Fatal("a body naming a different app was accepted")
147 }
148 put(`{"schema":2,"app":"titogo","features":{"a":"b"},"set_at":"2026-08-28T12:00:00Z"}`)
149 if _, err := GetFeatures(ctx, s, "bab", "titogo"); err == nil {
150 t.Fatal("an unknown schema was accepted")
151 }
152 // A record with no features map at all reads as empty, never nil —
153 // consumers range over it without a nil check.
154 put(`{"schema":1,"app":"titogo","set_at":"2026-08-28T12:00:00Z"}`)
155 got, err := GetFeatures(ctx, s, "bab", "titogo")
156 if err != nil || got == nil || got.Features == nil {
157 t.Fatalf("missing map = %+v, %v; want a non-nil empty map", got, err)
158 }
159}
160