carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package moves
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 "time"
9)
10
11func TestJournalDirLayout(t *testing.T) {
12 got := JournalDir("/data", "app.example.com", "op1")
13 want := filepath.Join("/data", ".moves", "app.example.com", "op1")
14 if got != want {
15 t.Fatalf("JournalDir = %q, want %q", got, want)
16 }
17}
18
19func TestReadJournalAbsentIsEmpty(t *testing.T) {
20 dir := t.TempDir()
21 j, err := ReadJournal(dir, "h", "op")
22 if err != nil {
23 t.Fatalf("absent journal: %v", err)
24 }
25 if j == nil || j.Host != "h" || j.Op != "op" || j.Phase != "" || j.Attempt != 0 || len(j.Steps) != 0 {
26 t.Fatalf("absent journal not empty: %+v", j)
27 }
28 if j.Done("cut") {
29 t.Fatal("empty journal reports a step done")
30 }
31 // Reading must not create anything.
32 if _, err := os.Stat(filepath.Join(dir, ".moves")); !os.IsNotExist(err) {
33 t.Fatalf("ReadJournal created state: %v", err)
34 }
35}
36
37func TestJournalRoundTripAndDone(t *testing.T) {
38 dir := t.TempDir()
39 j, err := ReadJournal(dir, "h", "op")
40 if err != nil {
41 t.Fatal(err)
42 }
43 j.Phase = "cut"
44 j.Attempt = 2
45 if j.Done("seal") {
46 t.Fatal("Done before Mark")
47 }
48 j.Mark("seal", "sha256:abc")
49 j.Mark("flip", "")
50 if !j.Done("seal") || !j.Done("flip") || j.Done("land") {
51 t.Fatalf("Done does not reflect Mark: %+v", j.Steps)
52 }
53 if err := j.Commit(dir); err != nil {
54 t.Fatal(err)
55 }
56 if j.UpdatedAt.IsZero() || time.Since(j.UpdatedAt) > time.Minute {
57 t.Fatalf("UpdatedAt not stamped: %v", j.UpdatedAt)
58 }
59 back, err := ReadJournal(dir, "h", "op")
60 if err != nil {
61 t.Fatal(err)
62 }
63 if back.V != j.V || back.Host != "h" || back.Op != "op" || back.Phase != "cut" || back.Attempt != 2 {
64 t.Fatalf("round trip: %+v", back)
65 }
66 if back.Steps["seal"] != "sha256:abc" || !back.Done("flip") || back.Done("land") {
67 t.Fatalf("steps lost: %+v", back.Steps)
68 }
69 if !back.UpdatedAt.Equal(j.UpdatedAt) {
70 t.Fatalf("UpdatedAt %v != %v", back.UpdatedAt, j.UpdatedAt)
71 }
72}
73
74func TestJournalWriteLeavesOnlyTheJournal(t *testing.T) {
75 dir := t.TempDir()
76 j, _ := ReadJournal(dir, "h", "op")
77 j.Mark("a", "1")
78 for i := 0; i < 3; i++ {
79 j.Attempt = i
80 if err := j.Commit(dir); err != nil {
81 t.Fatal(err)
82 }
83 }
84 ents, err := os.ReadDir(JournalDir(dir, "h", "op"))
85 if err != nil {
86 t.Fatal(err)
87 }
88 if len(ents) != 1 || ents[0].Name() != "journal.json" {
89 t.Fatalf("directory holds %v, want only journal.json", ents)
90 }
91 fi, err := os.Stat(JournalDir(dir, "h", "op"))
92 if err != nil {
93 t.Fatal(err)
94 }
95 if fi.Mode().Perm() != 0o700 {
96 t.Fatalf("journal dir mode %v, want 0700", fi.Mode().Perm())
97 }
98}
99
100func TestJournalCorruptFailsClosed(t *testing.T) {
101 cases := map[string]string{
102 "garbage": "{not json",
103 "empty": "",
104 "truncated": `{"v":1,"host":"h","op":"op","steps":{"a"`,
105 "wrong host": `{"v":1,"host":"other","op":"op"}`,
106 "wrong op": `{"v":1,"host":"h","op":"other"}`,
107 "future version": `{"v":99,"host":"h","op":"op"}`,
108 "zero version": `{"host":"h","op":"op"}`,
109 }
110 for name, body := range cases {
111 t.Run(name, func(t *testing.T) {
112 dir := t.TempDir()
113 jd := JournalDir(dir, "h", "op")
114 if err := os.MkdirAll(jd, 0o700); err != nil {
115 t.Fatal(err)
116 }
117 if err := os.WriteFile(filepath.Join(jd, "journal.json"), []byte(body), 0o600); err != nil {
118 t.Fatal(err)
119 }
120 j, err := ReadJournal(dir, "h", "op")
121 if err == nil {
122 t.Fatalf("corrupt journal read as %+v", j)
123 }
124 })
125 }
126}
127
128func TestJournalRejectsUnsafeNames(t *testing.T) {
129 dir := t.TempDir()
130 for _, c := range [][2]string{{"", "op"}, {"h", ""}, {"../x", "op"}, {"h", "a/b"}, {".", "op"}, {"h", ".."}} {
131 if _, err := ReadJournal(dir, c[0], c[1]); err == nil {
132 t.Errorf("ReadJournal(%q,%q) accepted", c[0], c[1])
133 }
134 j := &Journal{V: 1, Host: c[0], Op: c[1]}
135 if err := j.Commit(dir); err == nil {
136 t.Errorf("Write(%q,%q) accepted", c[0], c[1])
137 }
138 }
139 if ents, _ := os.ReadDir(dir); len(ents) != 0 {
140 t.Fatalf("rejected names left files: %v", ents)
141 }
142}
143
144func TestJournalMarkOnZeroValue(t *testing.T) {
145 var j Journal
146 j.Mark("x", "y")
147 if !j.Done("x") || j.Steps["x"] != "y" {
148 t.Fatalf("%+v", j)
149 }
150 if strings.Contains(JournalDir("d", "h", "o"), "..") {
151 t.Fatal("unexpected")
152 }
153}
154
155func TestMissingDirsListsWhatMkdirAllWillCreate(t *testing.T) {
156 dir := t.TempDir()
157 deep := filepath.Join(dir, "a", "b", "c")
158 got, err := missingDirs(deep)
159 if err != nil {
160 t.Fatal(err)
161 }
162 want := []string{deep, filepath.Join(dir, "a", "b"), filepath.Join(dir, "a")}
163 if strings.Join(got, "|") != strings.Join(want, "|") {
164 t.Fatalf("missingDirs = %v, want %v", got, want)
165 }
166 if err := os.MkdirAll(deep, 0o700); err != nil {
167 t.Fatal(err)
168 }
169 if got, err = missingDirs(deep); err != nil || len(got) != 0 {
170 t.Fatalf("missingDirs of an existing dir = %v, %v", got, err)
171 }
172}
173
174// A first write creates .moves/<host>/<op>/ (every component new); a second
175// write to another op reuses .moves/<host>/. Both write and read back.
176func TestJournalWriteCreatesTheChainAndReadsBack(t *testing.T) {
177 dir := filepath.Join(t.TempDir(), "data") // dataDir itself is new too
178 for _, op := range []string{"op1", "op2"} {
179 j, err := ReadJournal(dir, "h", op)
180 if err != nil {
181 t.Fatal(err)
182 }
183 j.Mark("a", op)
184 if err := j.Commit(dir); err != nil {
185 t.Fatalf("Write %s: %v", op, err)
186 }
187 back, err := ReadJournal(dir, "h", op)
188 if err != nil || back.Steps["a"] != op {
189 t.Fatalf("read back %s: %+v %v", op, back, err)
190 }
191 }
192}
193
194func TestJournalWriteUnwritableParentErrorsCleanly(t *testing.T) {
195 if os.Geteuid() == 0 {
196 t.Skip("root ignores directory permissions")
197 }
198 dir := t.TempDir()
199 if err := os.MkdirAll(filepath.Join(dir, ".moves"), 0o700); err != nil {
200 t.Fatal(err)
201 }
202 if err := os.Chmod(filepath.Join(dir, ".moves"), 0o500); err != nil {
203 t.Fatal(err)
204 }
205 defer os.Chmod(filepath.Join(dir, ".moves"), 0o700)
206 j := &Journal{Host: "h", Op: "op"}
207 if err := j.Commit(dir); err == nil {
208 t.Fatal("Write under an unwritable parent succeeded")
209 }
210 if _, err := os.Stat(JournalDir(dir, "h", "op")); err == nil {
211 t.Fatal("a failed Write left the journal directory")
212 }
213}
214