| 1 | package dbdoor |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "io" |
| 7 | "net/http" |
| 8 | "strconv" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | |
| 12 | "github.com/superfly/ltx" |
| 13 | ) |
| 14 | |
| 15 | func TestHandleProtocolCompatibility(t *testing.T) { |
| 16 | for _, mode := range []string{"legacy", "pinned", "downgraded-get"} { |
| 17 | t.Run(mode, func(t *testing.T) { |
| 18 | c := NewClient(func(ctx context.Context, tail string, h http.Header) (*http.Response, error) { |
| 19 | out := &http.Response{StatusCode: 200, Header: http.Header{}} |
| 20 | if tail == ListTail(0) { |
| 21 | list := ListResponse{Files: []LTXFile{{Level: 0, MinTXID: 1, MaxTXID: 1, Size: 3}}} |
| 22 | if mode != "legacy" { |
| 23 | list.Protocol = 2 |
| 24 | list.Files[0].Handle = "opaque" |
| 25 | } |
| 26 | b, _ := json.Marshal(list) |
| 27 | out.Body = io.NopCloser(strings.NewReader(string(b))) |
| 28 | } else { |
| 29 | want := "" |
| 30 | if mode != "legacy" { |
| 31 | want = "opaque" |
| 32 | } |
| 33 | if h.Get(HandleHeader) != want || h.Get("Range") != "bytes=1-2" { |
| 34 | t.Fatal(tail, h) |
| 35 | } |
| 36 | if mode == "pinned" { |
| 37 | out.Header.Set(HandleHeader, want) |
| 38 | } |
| 39 | out.Body = io.NopCloser(strings.NewReader("bc")) |
| 40 | } |
| 41 | return out, nil |
| 42 | }) |
| 43 | it, err := c.LTXFiles(t.Context(), 0, 0, false) |
| 44 | if err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | it.Close() |
| 48 | rc, err := c.OpenLTXFile(t.Context(), 0, ltx.TXID(1), ltx.TXID(1), 1, 2) |
| 49 | if mode == "downgraded-get" { |
| 50 | if err == nil { |
| 51 | rc.Close() |
| 52 | t.Fatal("accepted unpinned body") |
| 53 | } |
| 54 | return |
| 55 | } |
| 56 | if err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | b, err := io.ReadAll(rc) |
| 60 | rc.Close() |
| 61 | if err != nil || string(b) != "bc" { |
| 62 | t.Fatal(string(b), err) |
| 63 | } |
| 64 | }) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | func TestHandleProtocolNeverFallsBack(t *testing.T) { |
| 69 | c := NewClient(nil) |
| 70 | first := ListResponse{Protocol: 2, Files: []LTXFile{{Level: 0, MinTXID: 1, MaxTXID: 1, Size: 3, Handle: "first"}}} |
| 71 | if _, err := c.rememberFiles(0, first); err != nil { |
| 72 | t.Fatal(err) |
| 73 | } |
| 74 | again := ListResponse{Protocol: 2, Files: []LTXFile{{Level: 0, MinTXID: 1, MaxTXID: 1, Size: 3, Handle: "second"}}} |
| 75 | files, err := c.rememberFiles(0, again) |
| 76 | if err != nil || files[0].Handle != "first" { |
| 77 | t.Fatal(files, err) |
| 78 | } |
| 79 | again.Files[0].Size = 4 |
| 80 | if _, err := c.rememberFiles(0, again); err == nil { |
| 81 | t.Fatal("metadata changed") |
| 82 | } |
| 83 | if _, err := c.rememberFiles(0, ListResponse{}); err == nil { |
| 84 | t.Fatal("listing downgraded") |
| 85 | } |
| 86 | if _, err := c.fileHandle(0, 2, 2); err == nil { |
| 87 | t.Fatal("missing handle fell back") |
| 88 | } |
| 89 | for _, bad := range []ListResponse{{Protocol: 3}, {Protocol: 2, Files: []LTXFile{{Level: 0, MinTXID: 1, MaxTXID: 1}}}} { |
| 90 | if _, err := NewClient(nil).rememberFiles(0, bad); err == nil { |
| 91 | t.Fatal("bad listing", bad) |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | func TestHandleProtocolCompletePlan(t *testing.T) { |
| 97 | lists := 0 |
| 98 | c := NewClient(func(ctx context.Context, tail string, h http.Header) (*http.Response, error) { |
| 99 | if strings.Count(tail, "/") == 2 { |
| 100 | lists++ |
| 101 | if lists != 1 { |
| 102 | t.Fatal("relisted during one restore") |
| 103 | } |
| 104 | return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(`{"protocol":2,"plan":true,"files":[{"level":9,"min_txid":1,"max_txid":11,"size":100,"handle":"root"},{"level":1,"min_txid":10,"max_txid":13,"size":100,"handle":"continuation"}]}`))}, nil |
| 105 | } |
| 106 | if h.Get(HandleHeader) != "continuation" { |
| 107 | t.Fatal("lost selected handle", h) |
| 108 | } |
| 109 | return &http.Response{StatusCode: 206, Header: h.Clone(), Body: io.NopCloser(strings.NewReader("bytes"))}, nil |
| 110 | }) |
| 111 | for _, level := range []int{9, 8, 1, 0, 9} { |
| 112 | it, err := c.LTXFiles(t.Context(), level, 0, false) |
| 113 | if err != nil { |
| 114 | t.Fatal(err) |
| 115 | } |
| 116 | var count int |
| 117 | for it.Next() { |
| 118 | count++ |
| 119 | if it.Item().Level != level { |
| 120 | t.Fatal(it.Item()) |
| 121 | } |
| 122 | } |
| 123 | if err := it.Close(); err != nil { |
| 124 | t.Fatal(err) |
| 125 | } |
| 126 | want := 0 |
| 127 | if level == 9 || level == 1 { |
| 128 | want = 1 |
| 129 | } |
| 130 | if count != want { |
| 131 | t.Fatalf("level %d: %d files, want %d", level, count, want) |
| 132 | } |
| 133 | } |
| 134 | rc, err := c.OpenLTXFile(t.Context(), 1, 10, 13, 0, 0) |
| 135 | if err != nil { |
| 136 | t.Fatal(err) |
| 137 | } |
| 138 | rc.Close() |
| 139 | } |
| 140 | |
| 141 | func TestHandleProtocolRetainsSnapshotCRC(t *testing.T) { |
| 142 | for _, first := range []string{"0", "12345678901234567890"} { |
| 143 | t.Run(first, func(t *testing.T) { |
| 144 | c := NewClient(nil) |
| 145 | for _, crc := range []string{first, "7", "null"} { |
| 146 | var list ListResponse |
| 147 | raw := `{"protocol":2,"files":[{"level":9,"min_txid":1,"max_txid":11,"size":100,"handle":"` + crc + `","snapshot_crc64":` + crc + `}]}` |
| 148 | if err := json.Unmarshal([]byte(raw), &list); err != nil { |
| 149 | t.Fatal(err) |
| 150 | } |
| 151 | files, err := c.rememberFiles(9, list) |
| 152 | if err != nil { |
| 153 | t.Fatal(err) |
| 154 | } |
| 155 | if files[0].Handle != first || files[0].SnapshotCRC64 == nil { |
| 156 | t.Fatalf("lost first handle metadata: %+v", files[0]) |
| 157 | } |
| 158 | want, err := strconv.ParseUint(first, 10, 64) |
| 159 | if err != nil { |
| 160 | t.Fatal(err) |
| 161 | } |
| 162 | if *files[0].SnapshotCRC64 != want { |
| 163 | t.Fatalf("CRC changed to %d", *files[0].SnapshotCRC64) |
| 164 | } |
| 165 | } |
| 166 | }) |
| 167 | } |
| 168 | } |
| 169 | |