oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

1package memoria
2
3import (
4 "errors"
5 "strings"
6 "testing"
7)
8
9func env(pairs map[string]string) func(string) string {
10 return func(k string) string { return pairs[k] }
11}
12
13// The roster, and every way a half-configured one must refuse to boot.
14//
15// The shape of every refusal here is the same: **the alternative is
16// silence.** A sibling that cannot be asked withholds every passage it
17// owns, so a missing token or a missing team does not produce an error a
18// reader sees — it produces a page that is permanently short with a notice
19// blaming an app that is fine. Refused at boot, that is a startup message.
20// Left to query time, it is a mystery nobody can diagnose from the page.
21//
22// TWO-SIDED: the last case parses a good roster, or "refuses everything"
23// satisfies every assertion above it.
24//
25// MUTATION-TESTED 2026-09-06, each verified red then reverted:
26//
27// accept an entry with no token (empty Token in the struct)
28// -> the no-token case red
29// accept http:// as well as https://
30// -> the cleartext case red
31// skip the duplicate check
32// -> the duplicate case red
33// default Team to "" and carry on
34// -> the no-team case red
35// return the apps unsorted
36// -> the ordering assertion red
37// require MEMORIA_TEAM even with no siblings
38// -> the empty-roster case red, which is slice 0's whole config
39//
40// MUTATION-TESTED 2026-09-07, after a review found the host check missing:
41//
42// drop the CheckSiblingHost call
43// -> the swapped-label case red
44// key the duplicate check on the name instead of the token variable
45// -> the case-collision case red
46// echo the raw entry into the malformed-entry error
47// -> the no-echo case red
48func TestLoadSiblings(t *testing.T) {
49 t.Run("no siblings is a configuration, not a warning", func(t *testing.T) {
50 got, err := LoadSiblings(env(nil))
51 if err != nil {
52 t.Fatalf("an instance with no siblings refused to boot: %v", err)
53 }
54 if len(got.Apps) != 0 {
55 t.Errorf("got %d apps from an empty roster", len(got.Apps))
56 }
57 })
58
59 t.Run("a sibling with no token is refused", func(t *testing.T) {
60 _, err := LoadSiblings(env(map[string]string{
61 "MEMORIA_TEAM": "acme",
62 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works",
63 }))
64 if !errors.Is(err, ErrMissing) {
65 t.Fatalf("err = %v, want ErrMissing", err)
66 }
67 if !strings.Contains(err.Error(), "MEMORIA_SIBLING_TOKEN_DOCS") {
68 t.Errorf("the error does not name the variable to set: %v", err)
69 }
70 })
71
72 t.Run("a sibling on cleartext is refused", func(t *testing.T) {
73 _, err := LoadSiblings(env(map[string]string{
74 "MEMORIA_TEAM": "acme",
75 "MEMORIA_SIBLINGS": "Docs=http://acme.docs.oficina.works",
76 "MEMORIA_SIBLING_TOKEN_DOCS": "t",
77 "MEMORIA_SIBLING_TOKEN_SHEET": "t",
78 }))
79 if err == nil {
80 t.Error("a consumer token was allowed to travel over cleartext")
81 }
82 })
83
84 t.Run("a duplicate name is refused", func(t *testing.T) {
85 _, err := LoadSiblings(env(map[string]string{
86 "MEMORIA_TEAM": "acme",
87 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works,Docs=https://acme.sheets.oficina.works",
88 "MEMORIA_SIBLING_TOKEN_DOCS": "t",
89 }))
90 if err == nil {
91 t.Error("two apps configured under one name: one's item ids would " +
92 "go to the other, and a conforming sibling answers NOT_VISIBLE " +
93 "for ids it has no row for — the value that may be silent")
94 }
95 })
96
97 t.Run("siblings without a team are refused", func(t *testing.T) {
98 _, err := LoadSiblings(env(map[string]string{
99 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works",
100 "MEMORIA_SIBLING_TOKEN_DOCS": "t",
101 }))
102 if !errors.Is(err, ErrMissing) {
103 t.Fatalf("err = %v, want ErrMissing: without a team there is "+
104 "nothing for the host guard to compare against", err)
105 }
106 })
107
108 t.Run("a good roster parses, or none of the above proves anything", func(t *testing.T) {
109 got, err := LoadSiblings(env(map[string]string{
110 "MEMORIA_TEAM": "acme",
111 // **The names and the hosts sort in OPPOSITE orders**, on
112 // purpose: with Docs/Sheets against acme.docs/acme.sheets the
113 // two orderings are identical, so sorting by BaseURL passes —
114 // a review's mutation did. Here "Archive" sorts first by name
115 // and its host sorts last.
116 "MEMORIA_SIBLINGS": " Sheets=https://acme.sheets.oficina.works/ , Archive=https://acme.zzz.oficina.works ",
117 "MEMORIA_SIBLING_TOKEN_ARCHIVE": "archive-token",
118 "MEMORIA_SIBLING_TOKEN_SHEETS": "sheets-token",
119 }))
120 if err != nil {
121 t.Fatalf("a good roster was refused: %v", err)
122 }
123 if got.Team != "acme" {
124 t.Errorf("team = %q, want acme", got.Team)
125 }
126 // Sorted, so a log line and a page's notices do not depend on the
127 // order somebody happened to type the variable in.
128 if len(got.Apps) != 2 || got.Apps[0].Name != "Archive" || got.Apps[1].Name != "Sheets" {
129 t.Fatalf("apps = %+v, want Archive then Sheets — sorted by NAME, "+
130 "which is what a reader sees; these hosts sort the other way", got.Names())
131 }
132 if got.Apps[0].Token != "archive-token" || got.Apps[1].Token != "sheets-token" {
133 t.Error("a token went to the wrong app, which would send one " +
134 "sibling's credential to another")
135 }
136 // The trailing slash is trimmed, or every route becomes
137 // `//suite/s1` — which some routers serve and some 404.
138 if got.Apps[1].BaseURL != "https://acme.sheets.oficina.works" {
139 t.Errorf("base url = %q, want no trailing slash", got.Apps[1].BaseURL)
140 }
141 })
142
143 t.Run("an address the transport will refuse is refused at boot", func(t *testing.T) {
144 // **`<team>.<app>`, team first** (§6.3). This file's own example
145 // had the labels the other way round, and nothing checked, so the
146 // roster accepted an address every S1, S2 and S3 would then be
147 // refused for — before dialling, so not even a timeout. The
148 // instance boots, logs "siblings configured", and answers every
149 // query with "Docs did not answer; N results withheld" about an
150 // app that is working perfectly.
151 //
152 // That is precisely the failure this whole function exists to
153 // turn into a boot error, arriving through the one field nobody
154 // was checking. Found by a review, 2026-09-07.
155 _, err := LoadSiblings(env(map[string]string{
156 "MEMORIA_TEAM": "acme",
157 "MEMORIA_SIBLINGS": "Docs=https://docs.acme.oficina.works",
158 "MEMORIA_SIBLING_TOKEN_DOCS": "t",
159 }))
160 if err == nil {
161 t.Error("a swapped <app>.<team> address was accepted; every call to " +
162 "it would be refused before the transport dialled, forever, " +
163 "and the page would blame the sibling")
164 }
165
166 for _, bad := range []string{
167 "https://acme.docs.example.com", // outside the suite domain
168 "https://acme.docs.oficina.works.x", // suffix attack
169 "https://elsewhere.docs.oficina.works",
170 "https://acme.oficina.works", // no app label
171 } {
172 if _, err := LoadSiblings(env(map[string]string{
173 "MEMORIA_TEAM": "acme",
174 "MEMORIA_SIBLINGS": "Docs=" + bad,
175 "MEMORIA_SIBLING_TOKEN_DOCS": "t",
176 })); err == nil {
177 t.Errorf("accepted %q", bad)
178 }
179 }
180 })
181
182 t.Run("two names that resolve to one token variable are refused", func(t *testing.T) {
183 // "Docs" and "docs" are different map keys and different
184 // Document.Source values, so the name check passed them — and
185 // they read the SAME MEMORIA_SIBLING_TOKEN_DOCS, so one app
186 // silently receives the other's credential. Found by a review.
187 _, err := LoadSiblings(env(map[string]string{
188 "MEMORIA_TEAM": "acme",
189 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works,docs=https://acme.sheets.oficina.works",
190 "MEMORIA_SIBLING_TOKEN_DOCS": "t",
191 }))
192 if err == nil {
193 t.Error("two apps resolving to one token variable were accepted; " +
194 "one would be sent the other's consumer token")
195 }
196 })
197
198 t.Run("a malformed entry is named by position, never echoed", func(t *testing.T) {
199 // main logs this error, and MEMORIA_SIBLINGS sits beside the
200 // token variables in the same config. One transposed assignment
201 // and the "malformed entry" is a bearer token in `carlos logs`,
202 // which is storage we cannot delete.
203 _, err := LoadSiblings(env(map[string]string{
204 "MEMORIA_TEAM": "acme",
205 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works,sk-live-not-a-roster-entry",
206 "MEMORIA_SIBLING_TOKEN_DOCS": "t",
207 }))
208 if err == nil {
209 t.Fatal("a malformed entry was accepted")
210 }
211 if strings.Contains(err.Error(), "sk-live-not-a-roster-entry") {
212 t.Errorf("the error echoed the entry: %v", err)
213 }
214 if !strings.Contains(err.Error(), "entry 2") {
215 t.Errorf("the error does not say which entry: %v", err)
216 }
217 })
218
219 t.Run("a display name maps to one token variable", func(t *testing.T) {
220 // One spelling, so an app cannot be configured two ways and the
221 // operator cannot set the variable that is never read.
222 for _, c := range []struct{ name, want string }{
223 {"Docs", "MEMORIA_SIBLING_TOKEN_DOCS"},
224 {"Team Docs", "MEMORIA_SIBLING_TOKEN_TEAM_DOCS"},
225 {"sheets-2", "MEMORIA_SIBLING_TOKEN_SHEETS_2"},
226 } {
227 if got := tokenVar(c.name); got != c.want {
228 t.Errorf("tokenVar(%q) = %q, want %q", c.name, got, c.want)
229 }
230 }
231 })
232}
233
234// A roster must never be printed with its tokens in it.
235//
236// Names() exists so nobody reaches for %+v on the config, and this asserts
237// what it leaves out. §3.2's leak list and AGENTS.md's rule about logs:
238// `carlos logs` is platform-held storage we cannot grep, so a token that
239// reaches it is a credential that cannot be retracted — the same failure
240// as the sign-in links gate 4 was written for.
241func TestTheRosterCanBeLoggedWithoutItsTokens(t *testing.T) {
242 got, err := LoadSiblings(env(map[string]string{
243 "MEMORIA_TEAM": "acme",
244 "MEMORIA_SIBLINGS": "Docs=https://acme.docs.oficina.works",
245 "MEMORIA_SIBLING_TOKEN_DOCS": "super-secret-token",
246 }))
247 if err != nil {
248 t.Fatalf("LoadSiblings: %v", err)
249 }
250 // The presence half first: the token IS there to leak, so the absence
251 // below is an assertion rather than a description of an empty world.
252 if got.Apps[0].Token != "super-secret-token" {
253 t.Fatal("the fixture has no token in it, so the check below proves nothing")
254 }
255 for _, n := range got.Names() {
256 if strings.Contains(n, "super-secret-token") {
257 t.Errorf("Names() carried a token: %q", n)
258 }
259 }
260}
261