carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "net/http"
5 "os"
6 "path/filepath"
7 "testing"
8)
9
10func TestScratchLimitsPerAppAndDeployment(t *testing.T) {
11 p := newSeedScratch(t.TempDir(), func(string) (int64, error) { return 100 << 30, nil })
12 rel1, ref := p.reserve("acct/shop", 1<<30)
13 if ref != nil {
14 t.Fatalf("first: %+v", ref)
15 }
16 if _, ref := p.reserve("acct/shop", 1<<30); ref == nil || ref.Status != http.StatusTooManyRequests {
17 t.Fatalf("second for the same app: %+v, want 429", ref)
18 } else if want := "another copy or upload is running for shop. Try again when it finishes."; ref.Msg != want {
19 t.Fatalf("per-app message %q, want %q", ref.Msg, want)
20 }
21 rel2, ref := p.reserve("acct/kass", 1<<30)
22 if ref != nil {
23 t.Fatalf("another app: %+v", ref)
24 }
25 if _, ref := p.reserve("acct/blog", 1<<30); ref == nil || ref.Status != http.StatusTooManyRequests {
26 t.Fatalf("a third across the deployment: %+v, want 429", ref)
27 } else if want := "the console is already running two copies or uploads. Try again in a few minutes."; ref.Msg != want {
28 t.Fatalf("deployment message %q, want %q", ref.Msg, want)
29 }
30 rel1()
31 rel1() // releasing twice is harmless
32 if rel3, ref := p.reserve("acct/blog", 1<<30); ref != nil {
33 t.Fatalf("after a release: %+v", ref)
34 } else {
35 rel3()
36 }
37 rel2()
38}
39
40func TestScratchCapacityIsFreeSpaceLessAGiB(t *testing.T) {
41 free := int64(5 << 30)
42 reads := 0
43 p := newSeedScratch(t.TempDir(), func(string) (int64, error) { reads++; return free, nil })
44 if _, ref := p.reserve("a/x", 4<<30+1); ref == nil || ref.Status != http.StatusInsufficientStorage {
45 t.Fatalf("over capacity: %+v, want 507", ref)
46 } else if want := "the console has no room for this right now. Try again later."; ref.Msg != want {
47 t.Fatalf("capacity message %q, want %q", ref.Msg, want)
48 }
49 rel, ref := p.reserve("a/x", 4<<30)
50 if ref != nil {
51 t.Fatalf("at capacity: %+v", ref)
52 }
53 heldReads := reads
54 if _, ref := p.reserve("a/y", 1); ref == nil || ref.Status != http.StatusInsufficientStorage {
55 t.Fatalf("a full pool: %+v, want 507", ref)
56 }
57 if reads != heldReads {
58 t.Fatalf("free space re-read while a reservation was held (%d reads, was %d)", reads, heldReads)
59 }
60 rel()
61 free = 2 << 30 // the disk filled from elsewhere; an empty pool re-reads it
62 emptyReads := reads
63 if _, ref := p.reserve("a/x", 2<<30); ref == nil || ref.Status != http.StatusInsufficientStorage {
64 t.Fatalf("after a re-read: %+v, want 507", ref)
65 }
66 if reads != emptyReads+1 {
67 t.Fatalf("free space read %d times after the pool emptied, want exactly one more than %d", reads, emptyReads)
68 }
69}
70
71func TestSeedScratchReleaseIsExactlyOnce(t *testing.T) {
72 p := newSeedScratch(t.TempDir(), func(string) (int64, error) { return 10 << 30, nil })
73 relA, _ := p.reserve("acct/shop", 1<<30)
74 relB, _ := p.reserve("acct/blog", 1<<30)
75 relA()
76 relA() // must not give back a second reservation's bytes or slot
77 if _, ref := p.reserve("acct/kass", 1<<30); ref != nil {
78 t.Fatalf("one slot is free: %+v", ref)
79 }
80 if _, ref := p.reserve("acct/more", 1<<30); ref == nil || ref.Status != http.StatusTooManyRequests {
81 t.Fatalf("a double release freed a second slot: %+v", ref)
82 }
83 relB()
84}
85
86func TestSeedScratchWithoutADirectoryIsUnavailable(t *testing.T) {
87 p := startSeedScratch("")
88 if _, ref := p.reserve("acct/shop", 1); ref == nil || ref.Status != http.StatusInsufficientStorage {
89 t.Fatalf("no directory: %+v, want 507", ref)
90 }
91 // Not even a zero-byte reservation: a caller would go on to spool into dir().
92 if _, ref := p.reserve("acct/shop", 0); ref == nil || ref.Status != http.StatusInsufficientStorage {
93 t.Fatalf("no directory, zero bytes: %+v, want 507", ref)
94 }
95}
96
97func TestSeedScratchRefusedDirectoryRefusesZeroBytes(t *testing.T) {
98 p := startSeedScratch("relative/dir")
99 if _, ref := p.reserve("acct/shop", 0); ref == nil || ref.Status != http.StatusInsufficientStorage {
100 t.Fatalf("refused directory, zero bytes: %+v, want 507", ref)
101 }
102}
103
104func TestSeedScratchClearNarrowsAnExistingDirectory(t *testing.T) {
105 dir := filepath.Join(t.TempDir(), "seeds")
106 if err := os.Mkdir(dir, 0o755); err != nil {
107 t.Fatal(err)
108 }
109 if err := os.Chmod(dir, 0o755); err != nil { // whatever the umask was
110 t.Fatal(err)
111 }
112 if err := clearSeedScratch(dir); err != nil {
113 t.Fatal(err)
114 }
115 st, err := os.Stat(dir)
116 if err != nil || st.Mode().Perm() != 0o700 {
117 t.Fatalf("mode %v, %v; want 0700", st.Mode().Perm(), err)
118 }
119}
120
121func TestClearSeedScratch(t *testing.T) {
122 dir := filepath.Join(t.TempDir(), "scratch", "seeds")
123 if err := os.MkdirAll(filepath.Join(dir, "from-123"), 0o700); err != nil {
124 t.Fatal(err)
125 }
126 if err := os.WriteFile(filepath.Join(dir, "from-123", "restore.db"), []byte("spool"), 0o600); err != nil {
127 t.Fatal(err)
128 }
129 if err := os.WriteFile(filepath.Join(dir, "from-456"), []byte("spool"), 0o600); err != nil {
130 t.Fatal(err)
131 }
132 if err := clearSeedScratch(dir); err != nil {
133 t.Fatal(err)
134 }
135 ents, err := os.ReadDir(dir)
136 if err != nil || len(ents) != 0 {
137 t.Fatalf("after clear: %v, %v", ents, err)
138 }
139 st, _ := os.Stat(dir)
140 if st.Mode().Perm() != 0o700 {
141 t.Fatalf("mode %v, want 0700", st.Mode().Perm())
142 }
143}
144
145func TestSeedScratchClearKeepsWhatThePoolDidNotCreate(t *testing.T) {
146 dir := t.TempDir()
147 keep := filepath.Join(dir, "shop.example.com.db")
148 keepDir := filepath.Join(dir, "notes")
149 if err := os.WriteFile(keep, []byte("not ours"), 0o600); err != nil {
150 t.Fatal(err)
151 }
152 if err := os.MkdirAll(keepDir, 0o700); err != nil {
153 t.Fatal(err)
154 }
155 if err := os.MkdirAll(filepath.Join(dir, "from-9"), 0o700); err != nil {
156 t.Fatal(err)
157 }
158 p := startSeedScratch(dir)
159 if _, err := os.Stat(keep); err != nil {
160 t.Fatalf("a file the pool did not create was removed: %v", err)
161 }
162 if _, err := os.Stat(keepDir); err != nil {
163 t.Fatalf("a directory the pool did not create was removed: %v", err)
164 }
165 if _, err := os.Stat(filepath.Join(dir, "from-9")); !os.IsNotExist(err) {
166 t.Fatalf("the pool's own leftover survived: %v", err)
167 }
168 if p.dir() != dir {
169 t.Fatalf("dir() = %q, want %q", p.dir(), dir)
170 }
171}
172
173func TestSeedScratchClearRefusesUnsafePaths(t *testing.T) {
174 for _, dir := range []string{"/", "scratch/seeds", ".", "../x"} {
175 if err := clearSeedScratch(dir); err == nil {
176 t.Errorf("clearSeedScratch(%q) = nil, want a refusal", dir)
177 }
178 }
179 if err := clearSeedScratch(""); err != nil {
180 t.Errorf("empty directory: %v, want nil", err)
181 }
182 // A refused path leaves the pool unavailable rather than counting space.
183 p := startSeedScratch("relative/dir")
184 if _, ref := p.reserve("acct/shop", 1); ref == nil || ref.Status != http.StatusInsufficientStorage {
185 t.Fatalf("refused directory: %+v, want 507", ref)
186 }
187}
188
189func TestSeedScratchClearDoesNotFollowSymlinks(t *testing.T) {
190 dir := t.TempDir()
191 outside := t.TempDir()
192 victim := filepath.Join(outside, "victim.db")
193 if err := os.WriteFile(victim, []byte("keep"), 0o600); err != nil {
194 t.Fatal(err)
195 }
196 if err := os.Symlink(outside, filepath.Join(dir, "from-link")); err != nil {
197 t.Skip("no symlinks here")
198 }
199 if err := clearSeedScratch(dir); err != nil {
200 t.Fatal(err)
201 }
202 if _, err := os.Stat(victim); err != nil {
203 t.Fatalf("clearing followed a symlink out of the directory: %v", err)
204 }
205 if _, err := os.Lstat(filepath.Join(dir, "from-link")); !os.IsNotExist(err) {
206 t.Fatalf("the link itself survived: %v", err)
207 }
208}
209