| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package storage |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "context" |
| 8 | "errors" |
| 9 | "io" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | "time" |
| 13 | ) |
| 14 | |
| 15 | func TestMemoryStore_PutGetStat(t *testing.T) { |
| 16 | t.Parallel() |
| 17 | ctx := context.Background() |
| 18 | m := NewMemoryStore() |
| 19 | |
| 20 | res, err := m.Put(ctx, "k1", strings.NewReader("hello"), PutOpts{ContentType: "text/plain"}) |
| 21 | if err != nil { |
| 22 | t.Fatalf("Put: %v", err) |
| 23 | } |
| 24 | if res.Size != 5 || res.ETag == "" { |
| 25 | t.Fatalf("unexpected put result: %+v", res) |
| 26 | } |
| 27 | |
| 28 | rc, meta, err := m.Get(ctx, "k1") |
| 29 | if err != nil { |
| 30 | t.Fatalf("Get: %v", err) |
| 31 | } |
| 32 | defer func() { _ = rc.Close() }() |
| 33 | body, _ := io.ReadAll(rc) |
| 34 | if string(body) != "hello" { |
| 35 | t.Fatalf("body = %q, want hello", body) |
| 36 | } |
| 37 | if meta.ContentType != "text/plain" || meta.Size != 5 { |
| 38 | t.Fatalf("meta = %+v", meta) |
| 39 | } |
| 40 | |
| 41 | stat, err := m.Stat(ctx, "k1") |
| 42 | if err != nil { |
| 43 | t.Fatalf("Stat: %v", err) |
| 44 | } |
| 45 | if stat.ETag != res.ETag { |
| 46 | t.Fatalf("etag mismatch: %s vs %s", stat.ETag, res.ETag) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func TestMemoryStore_GetMissing(t *testing.T) { |
| 51 | t.Parallel() |
| 52 | m := NewMemoryStore() |
| 53 | if _, _, err := m.Get(context.Background(), "absent"); !errors.Is(err, ErrNotFound) { |
| 54 | t.Fatalf("expected ErrNotFound, got %v", err) |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | func TestMemoryStore_PutIfNoneMatch(t *testing.T) { |
| 59 | t.Parallel() |
| 60 | ctx := context.Background() |
| 61 | m := NewMemoryStore() |
| 62 | |
| 63 | if _, err := m.Put(ctx, "k", strings.NewReader("first"), PutOpts{}); err != nil { |
| 64 | t.Fatalf("first put: %v", err) |
| 65 | } |
| 66 | _, err := m.Put(ctx, "k", strings.NewReader("second"), PutOpts{IfNoneMatch: "*"}) |
| 67 | if !errors.Is(err, ErrPreconditionFailed) { |
| 68 | t.Fatalf("expected ErrPreconditionFailed, got %v", err) |
| 69 | } |
| 70 | |
| 71 | // Without IfNoneMatch, overwrite succeeds. |
| 72 | if _, err := m.Put(ctx, "k", strings.NewReader("third"), PutOpts{}); err != nil { |
| 73 | t.Fatalf("overwrite: %v", err) |
| 74 | } |
| 75 | rc, _, _ := m.Get(ctx, "k") |
| 76 | defer func() { _ = rc.Close() }() |
| 77 | body, _ := io.ReadAll(rc) |
| 78 | if string(body) != "third" { |
| 79 | t.Fatalf("got %q, want third", body) |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | func TestMemoryStore_DeleteIdempotent(t *testing.T) { |
| 84 | t.Parallel() |
| 85 | ctx := context.Background() |
| 86 | m := NewMemoryStore() |
| 87 | if err := m.Delete(ctx, "missing"); err != nil { |
| 88 | t.Fatalf("delete missing: %v", err) |
| 89 | } |
| 90 | _, _ = m.Put(ctx, "k", strings.NewReader("x"), PutOpts{}) |
| 91 | if err := m.Delete(ctx, "k"); err != nil { |
| 92 | t.Fatalf("delete: %v", err) |
| 93 | } |
| 94 | if _, err := m.Stat(ctx, "k"); !errors.Is(err, ErrNotFound) { |
| 95 | t.Fatalf("post-delete stat = %v, want ErrNotFound", err) |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | func TestMemoryStore_ListRecursiveAndDelimited(t *testing.T) { |
| 100 | t.Parallel() |
| 101 | ctx := context.Background() |
| 102 | m := NewMemoryStore() |
| 103 | for _, k := range []string{ |
| 104 | "avatars/alice/64.png", |
| 105 | "avatars/alice/128.png", |
| 106 | "avatars/bob/64.png", |
| 107 | "attachments/issue-1/x.txt", |
| 108 | } { |
| 109 | if _, err := m.Put(ctx, k, strings.NewReader("x"), PutOpts{}); err != nil { |
| 110 | t.Fatalf("seed %s: %v", k, err) |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | rec, err := m.List(ctx, "avatars/", ListOpts{Recursive: true}) |
| 115 | if err != nil { |
| 116 | t.Fatalf("recursive list: %v", err) |
| 117 | } |
| 118 | if len(rec.Objects) != 3 { |
| 119 | t.Fatalf("recursive: got %d objects, want 3", len(rec.Objects)) |
| 120 | } |
| 121 | |
| 122 | del, err := m.List(ctx, "avatars/", ListOpts{}) |
| 123 | if err != nil { |
| 124 | t.Fatalf("delimited list: %v", err) |
| 125 | } |
| 126 | if len(del.CommonPrefixes) != 2 { |
| 127 | t.Fatalf("delimited: got %d common prefixes, want 2: %v", len(del.CommonPrefixes), del.CommonPrefixes) |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | func TestMemoryStore_LargeRoundTrip(t *testing.T) { |
| 132 | t.Parallel() |
| 133 | ctx := context.Background() |
| 134 | m := NewMemoryStore() |
| 135 | body := bytes.Repeat([]byte{0xcd}, 5*1024*1024) // 5 MiB |
| 136 | if _, err := m.Put(ctx, "big", bytes.NewReader(body), PutOpts{ContentLength: int64(len(body))}); err != nil { |
| 137 | t.Fatalf("Put big: %v", err) |
| 138 | } |
| 139 | rc, meta, err := m.Get(ctx, "big") |
| 140 | if err != nil { |
| 141 | t.Fatalf("Get big: %v", err) |
| 142 | } |
| 143 | defer func() { _ = rc.Close() }() |
| 144 | got, _ := io.ReadAll(rc) |
| 145 | if !bytes.Equal(got, body) { |
| 146 | t.Fatalf("body mismatch (len got=%d want=%d)", len(got), len(body)) |
| 147 | } |
| 148 | if meta.Size != int64(len(body)) { |
| 149 | t.Fatalf("meta size = %d, want %d", meta.Size, len(body)) |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | func TestMemoryStore_SignedURL(t *testing.T) { |
| 154 | t.Parallel() |
| 155 | m := NewMemoryStore() |
| 156 | u, err := m.SignedURL(context.Background(), "k1", time.Minute, "GET") |
| 157 | if err != nil { |
| 158 | t.Fatalf("SignedURL: %v", err) |
| 159 | } |
| 160 | if !strings.HasPrefix(u, "mem://k1") { |
| 161 | t.Fatalf("unexpected url: %s", u) |
| 162 | } |
| 163 | if _, err := m.SignedURL(context.Background(), "k1", time.Minute, "POST"); err == nil { |
| 164 | t.Fatal("expected error for unsupported method") |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | func TestQuota(t *testing.T) { |
| 169 | t.Parallel() |
| 170 | q := Quota{Used: 100, Limit: 1000} |
| 171 | if q.Available() != 900 { |
| 172 | t.Fatalf("Available = %d, want 900", q.Available()) |
| 173 | } |
| 174 | if q.WouldExceed(800) { |
| 175 | t.Fatal("WouldExceed(800) = true, want false") |
| 176 | } |
| 177 | if !q.WouldExceed(901) { |
| 178 | t.Fatal("WouldExceed(901) = false, want true") |
| 179 | } |
| 180 | unlimited := Quota{Used: 1 << 40} |
| 181 | if unlimited.Available() != -1 { |
| 182 | t.Fatalf("unlimited Available = %d, want -1", unlimited.Available()) |
| 183 | } |
| 184 | if unlimited.WouldExceed(1 << 50) { |
| 185 | t.Fatal("unlimited WouldExceed = true, want false") |
| 186 | } |
| 187 | } |
| 188 |