| 1 | package dbdoor |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // Range semantics must match litestream's S3 client exactly |
| 9 | // (s3/replica_client.go:646-651), or a resumed read returns the wrong bytes. |
| 10 | func TestRangeHeaderMatchesTheS3Client(t *testing.T) { |
| 11 | cases := []struct { |
| 12 | name string |
| 13 | offset, size int64 |
| 14 | want string |
| 15 | }{ |
| 16 | {"whole file", 0, 0, "bytes=0-"}, |
| 17 | {"open-ended from an offset", 512, 0, "bytes=512-"}, |
| 18 | {"sized read", 0, 100, "bytes=0-99"}, |
| 19 | {"sized read at an offset", 512, 100, "bytes=512-611"}, |
| 20 | } |
| 21 | for _, tc := range cases { |
| 22 | t.Run(tc.name, func(t *testing.T) { |
| 23 | if got := RangeHeader(tc.offset, tc.size); got != tc.want { |
| 24 | t.Errorf("RangeHeader(%d, %d) = %q, want %q", tc.offset, tc.size, got, tc.want) |
| 25 | } |
| 26 | }) |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | // ParseRange is RangeHeader's inverse, used by the door. Anything it does not |
| 31 | // produce is refused rather than guessed at. |
| 32 | func TestParseRangeInvertsRangeHeader(t *testing.T) { |
| 33 | for _, c := range []struct{ offset, size int64 }{{0, 0}, {512, 0}, {0, 100}, {512, 100}} { |
| 34 | off, size, err := ParseRange(RangeHeader(c.offset, c.size)) |
| 35 | if err != nil || off != c.offset || size != c.size { |
| 36 | t.Errorf("ParseRange(RangeHeader(%d, %d)) = %d, %d, %v", c.offset, c.size, off, size, err) |
| 37 | } |
| 38 | } |
| 39 | if off, size, err := ParseRange(""); err != nil || off != 0 || size != 0 { |
| 40 | t.Errorf("ParseRange(\"\") = %d, %d, %v, want the whole file", off, size, err) |
| 41 | } |
| 42 | for _, bad := range []string{"bytes=-5", "bytes=5-2", "bytes=0-1,3-4", "items=0-", "bytes=x-", "bytes=0-y"} { |
| 43 | if _, _, err := ParseRange(bad); err == nil { |
| 44 | t.Errorf("ParseRange(%q) accepted", bad) |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | // Coordinates, never keys: the caller names a level and a TXID range, and |
| 50 | // the server decides what object that is. lock.json has no coordinate. |
| 51 | func TestGetPathIsCoordinateAddressed(t *testing.T) { |
| 52 | got := GetPath("bab", "titogo", "a.bab.oncarlos.com", 3, 16, 32) |
| 53 | for _, want := range []string{"bab", "titogo", "a.bab.oncarlos.com", "/3/", "16", "32"} { |
| 54 | if !strings.Contains(got, want) { |
| 55 | t.Fatalf("GetPath = %q, missing %q", got, want) |
| 56 | } |
| 57 | } |
| 58 | if strings.Contains(got, ".ltx") || strings.Contains(got, "lock") { |
| 59 | t.Fatalf("GetPath = %q leaks the key layout", got) |
| 60 | } |
| 61 | if !strings.HasSuffix(got, GetTail(3, 16, 32)) || !strings.HasSuffix(ListPath("bab", "titogo", "h.example.com", 3), ListTail(3)) { |
| 62 | t.Fatalf("full paths and tails disagree") |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | // The span segment round-trips, and nothing that is not a span parses. |
| 67 | func TestParseSpanRoundTripsAndRefusesTheRest(t *testing.T) { |
| 68 | min, max, err := ParseSpan(Span(16, 32)) |
| 69 | if err != nil || min != 16 || max != 32 { |
| 70 | t.Fatalf("ParseSpan(Span(16, 32)) = %d, %d, %v", min, max, err) |
| 71 | } |
| 72 | for _, bad := range []string{"", "16", "16-", "-32", "32-16", "0-1", "a-b", "lock.json", "16-32-48", "+1-2"} { |
| 73 | if _, _, err := ParseSpan(bad); err == nil { |
| 74 | t.Errorf("ParseSpan(%q) accepted", bad) |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | |