oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "errors"
8 "os"
9 "path/filepath"
10 "testing"
11 "time"
12)
13
14func contentMigrationFixture(t *testing.T) (*App, *Document, *contentCipher) {
15 t.Helper()
16 app, err := NewForTest(t)
17 if err != nil {
18 t.Fatal(err)
19 }
20 doc, err := app.Store().Create(context.Background(), app.TestPerson(t, "owner@example.com"), "Migration")
21 if err != nil {
22 t.Fatal(err)
23 }
24 c, err := newContentCipher(base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{53}, 32)), "production")
25 if err != nil {
26 t.Fatal(err)
27 }
28 return app, doc, c
29}
30
31func TestContentMigrationPreservesBodiesAndIsIdempotent(t *testing.T) {
32 app, doc, c := contentMigrationFixture(t)
33 ctx := context.Background()
34 update := DocUpdate{DocID: doc.ID, Seq: 1, UpdateBin: validUpdate(t), CreatedAt: time.Now().UTC()}
35 snapshot := DocSnapshot{DocID: doc.ID, Seq: 1, State: validUpdate(t), ABIVersion: coreABIVersion, CreatedAt: time.Now().UTC()}
36 if err := app.db.G.Create(&update).Error; err != nil {
37 t.Fatal(err)
38 }
39 if err := app.db.G.Create(&snapshot).Error; err != nil {
40 t.Fatal(err)
41 }
42 if err := convertContent(ctx, app.db.G, c); err != nil {
43 t.Fatal(err)
44 }
45 state, err := readContentSealing(ctx, app.db.G, c)
46 if err != nil || state == nil || state.Scrubbed {
47 t.Fatalf("state: %#v, %v", state, err)
48 }
49 if err := app.db.G.First(&update, update.ID).Error; err != nil {
50 t.Fatal(err)
51 }
52 if err := app.db.G.First(&snapshot, snapshot.ID).Error; err != nil {
53 t.Fatal(err)
54 }
55 sealed := append([]byte(nil), update.UpdateBin...)
56 if bytes.Equal(update.UpdateBin, validUpdate(t)) || bytes.Equal(snapshot.State, validUpdate(t)) {
57 t.Fatal("migration left plaintext")
58 }
59 if err := c.openUpdate(doc.GUID, &update); err != nil {
60 t.Fatal(err)
61 }
62 if err := c.openSnapshot(doc.GUID, &snapshot); err != nil {
63 t.Fatal(err)
64 }
65 if !bytes.Equal(update.UpdateBin, validUpdate(t)) || !bytes.Equal(snapshot.State, validUpdate(t)) {
66 t.Fatal("migration changed content")
67 }
68 if err := convertContent(ctx, app.db.G, c); err != nil {
69 t.Fatal(err)
70 }
71 if err := app.db.G.First(&update, update.ID).Error; err != nil {
72 t.Fatal(err)
73 }
74 if !bytes.Equal(update.UpdateBin, sealed) {
75 t.Fatal("repeat migration rewrote encrypted content")
76 }
77 wrong, _ := newContentCipher("", "development")
78 if _, err := readContentSealing(ctx, app.db.G, wrong); err == nil {
79 t.Fatal("wrong key accepted")
80 }
81}
82
83func TestContentMigrationRollsBackOnInvalidDocument(t *testing.T) {
84 app, doc, c := contentMigrationFixture(t)
85 rows := []DocUpdate{
86 {DocID: doc.ID, Seq: 1, UpdateBin: []byte("valid first row"), CreatedAt: time.Now().UTC()},
87 {DocID: doc.ID + 999, Seq: 1, UpdateBin: []byte("orphan second row"), CreatedAt: time.Now().UTC()},
88 }
89 if err := app.db.G.Create(&rows).Error; err != nil {
90 t.Fatal(err)
91 }
92 if err := convertContent(context.Background(), app.db.G, c); err == nil {
93 t.Fatal("orphan was accepted")
94 }
95 var got DocUpdate
96 if err := app.db.G.First(&got, rows[0].ID).Error; err != nil {
97 t.Fatal(err)
98 }
99 if got.ContentEncoding != 0 || !bytes.Equal(got.UpdateBin, rows[0].UpdateBin) {
100 t.Fatal("failed migration changed first row")
101 }
102 var count int64
103 if err := app.db.G.Model(&contentSealing{}).Count(&count).Error; err != nil {
104 t.Fatal(err)
105 }
106 if count != 0 {
107 t.Fatal("failed migration activated sealing")
108 }
109}
110
111func TestContentRowsRefusePlaintextDowngrade(t *testing.T) {
112 c, err := newContentCipher("", "development")
113 if err != nil {
114 t.Fatal(err)
115 }
116 for _, encoding := range []int{0, 2, -1} {
117 if err := c.openUpdate("", &DocUpdate{ContentEncoding: encoding, UpdateBin: []byte("plaintext")}); !errors.Is(err, errContentEncoding) {
118 t.Fatalf("update encoding %d: %v", encoding, err)
119 }
120 if err := c.openSnapshot("", &DocSnapshot{ContentEncoding: encoding, State: []byte("plaintext")}); !errors.Is(err, errContentEncoding) {
121 t.Fatalf("snapshot encoding %d: %v", encoding, err)
122 }
123 }
124}
125
126func TestContentScrubRemovesLegacyBytesFromDatabaseAndWAL(t *testing.T) {
127 path := filepath.Join(t.TempDir(), "docs.db")
128 app, err := NewForTestWith(t, Config{DBPath: path})
129 if err != nil {
130 t.Fatal(err)
131 }
132 doc, err := app.Store().Create(context.Background(), app.TestPerson(t, "owner@example.com"), "Scrub")
133 if err != nil {
134 t.Fatal(err)
135 }
136 canary := []byte("CONTENT-SCRUB-CANARY-55de8635-70b3-488a")
137 row := DocUpdate{DocID: doc.ID, Seq: 1, UpdateBin: canary, CreatedAt: time.Now().UTC()}
138 if err := app.db.G.Create(&row).Error; err != nil {
139 t.Fatal(err)
140 }
141 wal, err := os.ReadFile(path + "-wal")
142 if err != nil || !bytes.Contains(wal, canary) {
143 t.Fatalf("legacy WAL canary absent: %v", err)
144 }
145 c, err := newContentCipher("", "development")
146 if err != nil {
147 t.Fatal(err)
148 }
149 ctx := context.Background()
150 if err := convertContent(ctx, app.db.G, c); err != nil {
151 t.Fatal(err)
152 }
153 if err := scrubContent(ctx, app.db, c); err != nil {
154 t.Fatal(err)
155 }
156 for _, filename := range []string{path, path + "-wal"} {
157 body, err := os.ReadFile(filename)
158 if err != nil {
159 t.Fatal(err)
160 }
161 if bytes.Contains(body, canary) {
162 t.Fatalf("plaintext retained in %s", filename)
163 }
164 }
165 state, err := readContentSealing(ctx, app.db.G, c)
166 if err != nil || state == nil || !state.Scrubbed {
167 t.Fatalf("scrub state: %#v, %v", state, err)
168 }
169}
170