oficina / suite Public

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

Plain git — no account needed to clone.

Download

Download this file

1package suite
2
3import (
4 "fmt"
5 "testing"
6)
7
8// assertResults is the guard the first version of these tests lacked. Every
9// case here is about totality, and a decoder that returns nothing satisfies a
10// loop over its results without asserting anything at all — mutating the
11// non-2xx branch to `return nil` passed the whole suite. Length first, always.
12func assertResults(t *testing.T, got []Authorization, want []Authorization) {
13 t.Helper()
14 if len(got) != len(want) {
15 t.Fatalf("got %d results, want exactly %d — the decoder is total or it is nothing", len(got), len(want))
16 }
17 for i := range want {
18 if got[i] != want[i] {
19 t.Errorf("result %d: got %q, want %q", i, got[i], want[i])
20 }
21 }
22}
23
24// The four values, each returned distinctly, on pairs that differ by id and by
25// revision. Earlier versions used two ids at one revision each and never a
26// STALE, so the decoder could ignore Revision entirely — or drop STALE from
27// its vocabulary — and the suite still passed.
28func TestS2DecodeAllFourValues(t *testing.T) {
29 asked := []ItemRevision{
30 {ItemID: "doc-1", Revision: 7},
31 {ItemID: "doc-2", Revision: 3},
32 {ItemID: "doc-3", Revision: 11},
33 {ItemID: "doc-4", Revision: 2},
34 }
35 body := []byte(`{"results":[
36 {"item_id":"doc-3","revision":11,"result":"STALE"},
37 {"item_id":"doc-1","revision":7,"result":"VISIBLE"},
38 {"item_id":"doc-4","revision":2,"result":"UNKNOWN"},
39 {"item_id":"doc-2","revision":3,"result":"NOT_VISIBLE"}
40 ]}`)
41
42 got := DecodeS2(asked, 200, body)
43
44 // Answered out of order on purpose: results come back in the order asked,
45 // not the order the sibling felt like replying in.
46 assertResults(t, got, []Authorization{VISIBLE, NOT_VISIBLE, STALE, UNKNOWN})
47}
48
49// The revision is part of the identity of a pair. An answer about revision 6
50// does not answer a question about revision 7 of the same item — that is the
51// whole reason STALE exists — so it must not be matched to it.
52func TestS2AnswerForAnotherRevisionDoesNotMatch(t *testing.T) {
53 asked := []ItemRevision{{ItemID: "doc-1", Revision: 7}}
54 body := []byte(`{"results":[{"item_id":"doc-1","revision":6,"result":"VISIBLE"}]}`)
55
56 got := DecodeS2(asked, 200, body)
57
58 assertResults(t, got, []Authorization{UNKNOWN})
59}
60
61// The same item at two revisions is two questions, and each gets its own
62// answer.
63func TestS2SameItemAtTwoRevisions(t *testing.T) {
64 asked := []ItemRevision{
65 {ItemID: "doc-1", Revision: 6},
66 {ItemID: "doc-1", Revision: 7},
67 }
68 body := []byte(`{"results":[
69 {"item_id":"doc-1","revision":6,"result":"STALE"},
70 {"item_id":"doc-1","revision":7,"result":"VISIBLE"}
71 ]}`)
72
73 got := DecodeS2(asked, 200, body)
74
75 assertResults(t, got, []Authorization{STALE, VISIBLE})
76}
77
78// An item the app of record did not answer for is UNKNOWN — never the zero
79// value, and never NOT_VISIBLE. memoria §4.1: a non-answer is synthesised by
80// the caller, because a sibling that never answers cannot return anything.
81func TestS2MissingItemIsUnknown(t *testing.T) {
82 asked := []ItemRevision{
83 {ItemID: "doc-1", Revision: 7},
84 {ItemID: "doc-2", Revision: 3},
85 }
86 body := []byte(`{"results":[{"item_id":"doc-1","revision":7,"result":"VISIBLE"}]}`)
87
88 got := DecodeS2(asked, 200, body)
89
90 assertResults(t, got, []Authorization{VISIBLE, UNKNOWN})
91}
92
93// An answer nobody asked for is ignored rather than shifting the others along.
94func TestS2UnsolicitedAnswerIsIgnored(t *testing.T) {
95 asked := []ItemRevision{{ItemID: "doc-1", Revision: 7}}
96 body := []byte(`{"results":[
97 {"item_id":"doc-9","revision":1,"result":"VISIBLE"},
98 {"item_id":"doc-1","revision":7,"result":"NOT_VISIBLE"}
99 ]}`)
100
101 got := DecodeS2(asked, 200, body)
102
103 assertResults(t, got, []Authorization{NOT_VISIBLE})
104}
105
106// A non-2xx status is a whole-call failure: every pair is UNKNOWN, whatever
107// the body happens to contain. A body arriving with a 500 is not an answer.
108func TestS2NonSuccessStatusIsWholeCallUnknown(t *testing.T) {
109 asked := []ItemRevision{
110 {ItemID: "doc-1", Revision: 7},
111 {ItemID: "doc-2", Revision: 3},
112 }
113 body := []byte(`{"results":[
114 {"item_id":"doc-1","revision":7,"result":"VISIBLE"},
115 {"item_id":"doc-2","revision":3,"result":"VISIBLE"}
116 ]}`)
117
118 got := DecodeS2(asked, 500, body)
119
120 assertResults(t, got, []Authorization{UNKNOWN, UNKNOWN})
121}
122
123// A body that does not parse is a whole-call failure too.
124func TestS2MalformedBodyIsWholeCallUnknown(t *testing.T) {
125 asked := []ItemRevision{{ItemID: "doc-1", Revision: 7}}
126
127 got := DecodeS2(asked, 200, []byte(`{"results":[{"item_id":`))
128
129 assertResults(t, got, []Authorization{UNKNOWN})
130}
131
132// An item answered twice is UNKNOWN for that item. Two answers are not an
133// answer, and picking either one would be the caller inventing a decision the
134// app of record did not make. The other pairs are unaffected.
135func TestS2DuplicateRowIsUnknown(t *testing.T) {
136 asked := []ItemRevision{
137 {ItemID: "doc-1", Revision: 7},
138 {ItemID: "doc-2", Revision: 3},
139 }
140 body := []byte(`{"results":[
141 {"item_id":"doc-1","revision":7,"result":"VISIBLE"},
142 {"item_id":"doc-1","revision":7,"result":"NOT_VISIBLE"},
143 {"item_id":"doc-2","revision":3,"result":"VISIBLE"}
144 ]}`)
145
146 got := DecodeS2(asked, 200, body)
147
148 assertResults(t, got, []Authorization{UNKNOWN, VISIBLE})
149}
150
151// A duplicated *key* is the same defect one level down, and encoding/json
152// hides it: it keeps the last value silently, so the duplicate-row check above
153// never runs. Reported by the GPT-6 module review, finding 2.
154func TestS2DuplicateKeyInARowIsRefused(t *testing.T) {
155 asked := []ItemRevision{{ItemID: "doc-1", Revision: 7}}
156 body := []byte(`{"results":[
157 {"item_id":"doc-1","revision":7,"result":"NOT_VISIBLE","result":"VISIBLE"}
158 ]}`)
159
160 got := DecodeS2(asked, 200, body)
161
162 assertResults(t, got, []Authorization{UNKNOWN})
163}
164
165// The same trick at the envelope level: two `results` arrays, the second
166// overwriting the first.
167func TestS2DuplicateEnvelopeKeyIsRefused(t *testing.T) {
168 asked := []ItemRevision{{ItemID: "doc-1", Revision: 7}}
169 body := []byte(`{
170 "results":[{"item_id":"doc-1","revision":7,"result":"NOT_VISIBLE"}],
171 "results":[{"item_id":"doc-1","revision":7,"result":"VISIBLE"}]
172 }`)
173
174 got := DecodeS2(asked, 200, body)
175
176 assertResults(t, got, []Authorization{UNKNOWN})
177}
178
179// Trailing content after a complete document is refused. A sibling appending
180// a second object is not speaking this contract.
181func TestS2TrailingContentIsRefused(t *testing.T) {
182 asked := []ItemRevision{{ItemID: "doc-1", Revision: 7}}
183 body := []byte(`{"results":[{"item_id":"doc-1","revision":7,"result":"VISIBLE"}]} {"results":[]}`)
184
185 got := DecodeS2(asked, 200, body)
186
187 assertResults(t, got, []Authorization{UNKNOWN})
188}
189
190// A value outside the four is UNKNOWN. The vocabulary is closed (§4.3), so an
191// unrecognised word is a sibling this client does not understand, not a fifth
192// meaning to pass through to a caller that will switch on it.
193func TestS2UnrecognisedValueIsUnknown(t *testing.T) {
194 asked := []ItemRevision{{ItemID: "doc-1", Revision: 7}}
195 body := []byte(`{"results":[{"item_id":"doc-1","revision":7,"result":"MAYBE"}]}`)
196
197 got := DecodeS2(asked, 200, body)
198
199 assertResults(t, got, []Authorization{UNKNOWN})
200}
201
202// A full-size question decodes one result per pair and nothing else. 256 is
203// written literally rather than derived from MaxS2Pairs: a test that computes
204// both sides from the constant checks its own consistency, and changing the
205// constant to 512 passed the entire suite.
206func TestS2DecodesAFullBatch(t *testing.T) {
207 const fullBatch = 256
208
209 asked := make([]ItemRevision, fullBatch)
210 want := make([]Authorization, fullBatch)
211 body := `{"results":[`
212 for i := range asked {
213 asked[i] = ItemRevision{ItemID: fmt.Sprintf("doc-%d", i), Revision: int64(i)}
214 want[i] = VISIBLE
215 if i > 0 {
216 body += ","
217 }
218 body += fmt.Sprintf(`{"item_id":"doc-%d","revision":%d,"result":"VISIBLE"}`, i, i)
219 }
220 body += `]}`
221
222 got := DecodeS2(asked, 200, []byte(body))
223
224 assertResults(t, got, want)
225}
226