oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1package sheets_test
2
3import (
4 "reflect"
5 "strings"
6 "testing"
7
8 "amadan.net/oficina/sheets/internal/sheets"
9)
10
11// The models must carry no column that could hold a readable cell value.
12// This is a schema-level restatement of the one rule: there is nothing
13// to accidentally populate.
14//
15// It is a genuine reflection-based assertion, not a check of a
16// hardcoded list against itself. It walks every model and asserts two
17// properties directly on the struct shape:
18//
19// 1. every field whose name ends in "Sealed" has type []byte — a
20// sealed field typed string would mean somebody stored plaintext in
21// it.
22// 2. no field is named exactly one of the forbidden bare names — those
23// names without the "Sealed" suffix are the shape a plaintext
24// column takes.
25//
26// Add "Title string" to Sheet and this test fails; that is the point.
27//
28// It walks sheets.Models rather than a list of its own. Models is the
29// list `rastrillo migration check` reads, so it is the one list that
30// cannot be quietly incomplete: a model missing from it fails CI at the
31// migration gate. A second list here could silently omit a new table —
32// which is exactly what a plaintext column would need in order to
33// arrive unnoticed. (The structs themselves moved to package store when
34// the HTTP layer needed to open a Handle; this test followed the list,
35// not the package.)
36func TestNoPlaintextValueColumns(t *testing.T) {
37 forbidden := map[string]bool{
38 "Title": true,
39 "Name": true,
40 "Input": true,
41 "Cached": true,
42 "Value": true,
43 }
44
45 if len(sheets.Models) == 0 {
46 t.Fatal("sheets.Models is empty; this test would pass by walking nothing")
47 }
48
49 byteSliceType := reflect.TypeOf([]byte(nil))
50
51 for _, m := range sheets.Models {
52 typ := reflect.TypeOf(m)
53 for i := 0; i < typ.NumField(); i++ {
54 field := typ.Field(i)
55
56 if strings.HasSuffix(field.Name, "Sealed") {
57 if field.Type != byteSliceType {
58 t.Errorf("%s.%s ends in Sealed but has type %s, want []byte",
59 typ.Name(), field.Name, field.Type)
60 }
61 continue
62 }
63
64 if forbidden[field.Name] {
65 t.Errorf("%s.%s is a plaintext-shaped field name; sealed content must use a %sSealed []byte field instead",
66 typ.Name(), field.Name, field.Name)
67 }
68 }
69 }
70}
71
72// Member.Address is deliberately plaintext (spec §2.2 lists readable
73// email addresses among what this design does not seal), so it is worth
74// stating that the test above passes it on purpose rather than by
75// accident: "Address" is not in the forbidden set, and it is not in it
76// because membership is published in the README as plaintext.
77func TestMemberAddressIsIntentionallyPlaintext(t *testing.T) {
78 f, ok := reflect.TypeOf(sheets.Member{}).FieldByName("Address")
79 if !ok {
80 t.Fatal("Member has no Address field")
81 }
82 if f.Type.Kind() != reflect.String {
83 t.Fatalf("Member.Address is %s; spec §2.2 says membership addresses are readable, "+
84 "and a sealed one could not carry the unique index either", f.Type)
85 }
86}
87