carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package lease_test
2
3import (
4 "context"
5 "errors"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "path/filepath"
11 "testing"
12
13 "github.com/aws/aws-sdk-go-v2/aws"
14 "github.com/aws/aws-sdk-go-v2/credentials"
15 awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
16
17 "github.com/carlosframework/platform/internal/lease"
18 "github.com/carlosframework/platform/internal/lease/leasetest"
19)
20
21func wireClient(t *testing.T, url string) *awss3.Client {
22 t.Helper()
23 return awss3.New(awss3.Options{
24 Region: "eu-west-1", BaseEndpoint: aws.String(url), UsePathStyle: true,
25 Credentials: credentials.NewStaticCredentialsProvider("k", "s", ""),
26 RetryMaxAttempts: 1,
27 })
28}
29
30// wireStore is a versionedS3 whose buckets "data" and "other" are served
31// by two separate servers over one model, so a wrong-region routing is a
32// visible failure.
33func wireStore(t *testing.T) (lease.VersionedStore, *leasetest.VersionedBucket, func() int) {
34 t.Helper()
35 m := leasetest.NewVersionedBucket()
36 m.AddBucket("data")
37 m.AddBucket("other")
38 h1, n1 := leasetest.NewS3Server(m, "data", "nobucket")
39 h2, n2 := leasetest.NewS3Server(m, "other")
40 s1, s2 := httptest.NewServer(h1), httptest.NewServer(h2)
41 t.Cleanup(s1.Close)
42 t.Cleanup(s2.Close)
43 // "nobucket" is bound but absent from the model: the server answers
44 // NoSuchBucket, for the missing-bucket contract case.
45 st := lease.NewVersionedS3WithClients(map[string]*awss3.Client{
46 "data": wireClient(t, s1.URL), "other": wireClient(t, s2.URL),
47 "nobucket": wireClient(t, s1.URL),
48 })
49 return st, m, func() int { return n1() + n2() }
50}
51
52func TestVersionedS3Contract(t *testing.T) {
53 leasetest.RunVersionedStoreContract(t, func(t *testing.T) (lease.VersionedStore, *leasetest.VersionedBucket, string, func() int) {
54 st, m, served := wireStore(t)
55 return st, m, "data", served
56 })
57}
58
59// Each bucket reaches only its own endpoint: a client bound to the wrong
60// endpoint gets 301 and the operation fails.
61func TestVersionedS3RoutesEachBucketToItsEndpoint(t *testing.T) {
62 ctx := context.Background()
63 st, m, _ := wireStore(t)
64 for _, b := range []string{"data", "other"} {
65 v := m.Put(b, "k", []byte(b))
66 if vs, err := st.ListVersions(ctx, b, ""); err != nil || len(vs) != 1 {
67 t.Fatalf("list %s = %v, %v", b, vs, err)
68 }
69 if err := st.DeleteVersion(ctx, b, "k", v); err != nil {
70 t.Fatalf("delete %s = %v", b, err)
71 }
72 }
73 m2 := leasetest.NewVersionedBucket()
74 m2.AddBucket("data")
75 h, _ := leasetest.NewS3Server(m2, "other")
76 srv := httptest.NewServer(h)
77 t.Cleanup(srv.Close)
78 wrong := lease.NewVersionedS3WithClients(map[string]*awss3.Client{"data": wireClient(t, srv.URL)})
79 if _, err := wrong.ListVersions(ctx, "data", ""); err == nil {
80 t.Fatal("a wrong-region endpoint answered")
81 }
82}
83
84func TestVersionedS3RefusesUnboundBucket(t *testing.T) {
85 st, _, _ := wireStore(t)
86 if _, err := st.ListVersions(context.Background(), "unbound", ""); err == nil {
87 t.Fatal("listing an unbound bucket did not fail")
88 }
89}
90
91func TestVersionedS3CopyEscapesLikeAWS(t *testing.T) {
92 st, m, _ := wireStore(t)
93 key := "src/a b+c%?d"
94 v1 := m.Put("data", key, []byte("old"))
95 m.Put("data", key, []byte("new"))
96 if _, _, err := st.CopyVersion(context.Background(), "data", key, v1, "other", "dst/k", true); err != nil {
97 t.Fatal(err)
98 }
99 if cur, ok := m.Current("other", "dst/k"); !ok || string(cur) != "old" {
100 t.Fatalf("copied %q, want the pinned old version", cur)
101 }
102}
103
104func TestVersionedS3RangeWire(t *testing.T) {
105 m := leasetest.NewVersionedBucket()
106 m.AddBucket("data")
107 key := "dir/a b+c%?d"
108 id := m.Put("data", key, []byte("original"))
109 m.Put("data", key, []byte("replacement"))
110 handler, _ := leasetest.NewS3Server(m, "data")
111 for _, tc := range []struct {
112 length int64
113 header, contentRange, body string
114 }{
115 {3, "bytes=2-4", "bytes 2-4/8", "igi"},
116 {-1, "bytes=2-", "bytes 2-7/8", "iginal"},
117 } {
118 t.Run(tc.header, func(t *testing.T) {
119 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
120 if r.Method != http.MethodGet || r.URL.Query().Get("versionId") != id || r.Header.Get("Range") != tc.header {
121 t.Errorf("request = %s %s, Range %q", r.Method, r.URL, r.Header.Get("Range"))
122 }
123 rec := httptest.NewRecorder()
124 handler.ServeHTTP(rec, r)
125 if rec.Code != http.StatusPartialContent || rec.Header().Get("Content-Range") != tc.contentRange || rec.Body.String() != tc.body {
126 t.Errorf("response = %d %v %q", rec.Code, rec.Header(), rec.Body.String())
127 }
128 for k, values := range rec.Header() {
129 w.Header()[k] = values
130 }
131 w.WriteHeader(rec.Code)
132 w.Write(rec.Body.Bytes())
133 }))
134 defer srv.Close()
135 st := lease.NewVersionedS3WithClients(map[string]*awss3.Client{"data": wireClient(t, srv.URL)})
136 ranged, ok := st.(lease.VersionRangeOpener)
137 if !ok {
138 t.Fatal("S3 store lacks ranged version reads")
139 }
140 rc, err := ranged.OpenVersionRange(t.Context(), "data", key, id, 2, tc.length)
141 if err != nil {
142 t.Fatal(err)
143 }
144 body, err := io.ReadAll(rc)
145 rc.Close()
146 if err != nil || string(body) != tc.body {
147 t.Fatalf("read = %q, %v", body, err)
148 }
149 })
150 }
151}
152
153// PutFileIfAbsent streams a file from disk create-only: the object lands
154// with the file's bytes and a version id, and a second put is ErrExists
155// with the first object untouched.
156func TestVersionedS3PutFileIfAbsent(t *testing.T) {
157 ctx := context.Background()
158 st, m, _ := wireStore(t)
159 fp, ok := st.(lease.FilePutIfAbsent)
160 if !ok {
161 t.Fatal("the S3 store cannot put a file create-only")
162 }
163 path := filepath.Join(t.TempDir(), "sealed")
164 if err := os.WriteFile(path, []byte("sealed bytes"), 0o600); err != nil {
165 t.Fatal(err)
166 }
167 v, err := fp.PutFileIfAbsent(ctx, "data", "move-snapshots/h/op/files-ab.sealed", path)
168 if err != nil || v == "" {
169 t.Fatalf("first put = %q, %v", v, err)
170 }
171 if cur, ok := m.Current("data", "move-snapshots/h/op/files-ab.sealed"); !ok || string(cur) != "sealed bytes" {
172 t.Fatalf("stored %q", cur)
173 }
174 if err := os.WriteFile(path, []byte("other bytes"), 0o600); err != nil {
175 t.Fatal(err)
176 }
177 if _, err := fp.PutFileIfAbsent(ctx, "data", "move-snapshots/h/op/files-ab.sealed", path); !errors.Is(err, lease.ErrExists) {
178 t.Fatalf("second put = %v, want ErrExists", err)
179 }
180 if cur, _ := m.Current("data", "move-snapshots/h/op/files-ab.sealed"); string(cur) != "sealed bytes" {
181 t.Fatalf("the second put overwrote: %q", cur)
182 }
183}
184