carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "sync"
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
18type deleteTrap struct {
19 Store
20 called bool
21}
22
23func (s *deleteTrap) Delete(context.Context, string) error { s.called = true; return nil }
24func TestConditionalDeleteUnsupportedAndInvalidTags(t *testing.T) {
25 ctx := context.Background()
26 trap := &deleteTrap{}
27 for _, s := range []Store{trap, NewPrefixStore(trap, "scope"), NewPrefixStore(NewPrefixStore(trap, "outer"), "inner")} {
28 if e := DeleteIfMatch(ctx, s, "key", "etag"); !errors.Is(e, ErrConditionalDeleteUnsupported) {
29 t.Fatal(e)
30 }
31 }
32 if trap.called {
33 t.Fatal("unconditional fallback")
34 }
35 d := NewDirStore(t.TempDir())
36 etag, e := d.Put(ctx, "key", []byte("keep"), "")
37 if e != nil {
38 t.Fatal(e)
39 }
40 for _, bad := range []string{"", " ", "*", `"*"`, `""`, `W/"etag"`, `"one", "two"`, "one,two", "etag\r\nX-Test: bad", " etag", "etag ", "\tetag", `"broken`, `broken"`} {
41 for _, fn := range []func() error{func() error { return DeleteIfMatch(ctx, d, "key", bad) }, func() error { return d.DeleteIfMatch(ctx, "key", bad) }, func() error { return (&S3Store{}).DeleteIfMatch(ctx, "key", bad) }} {
42 if e := fn(); !errors.Is(e, ErrInvalidETag) {
43 t.Fatalf("invalid tag accepted: %q: %v", bad, e)
44 }
45 }
46 }
47 b, got, e := d.Get(ctx, "key")
48 if e != nil || string(b) != "keep" || got != etag {
49 t.Fatal("invalid tag changed object")
50 }
51}
52func TestConditionalDeleteDirRevisionAndPrefix(t *testing.T) {
53 ctx := context.Background()
54 d := NewDirStore(t.TempDir())
55 scoped := NewPrefixStore(NewPrefixStore(d, "outer"), "inner")
56 old, e := scoped.Put(ctx, "key", []byte("old"), "")
57 if e != nil {
58 t.Fatal(e)
59 }
60 current, e := scoped.Put(ctx, "key", []byte("new"), old)
61 if e != nil {
62 t.Fatal(e)
63 }
64 if _, e = d.Put(ctx, "key", []byte("outside"), ""); e != nil {
65 t.Fatal(e)
66 }
67 if e = DeleteIfMatch(ctx, scoped, "key", old); !errors.Is(e, ErrCASConflict) {
68 t.Fatal(e)
69 }
70 b, _, e := d.Get(ctx, "outer/inner/key")
71 if e != nil || string(b) != "new" {
72 t.Fatal("stale tag deleted current object")
73 }
74 if e = DeleteIfMatch(ctx, scoped, "key", current); e != nil {
75 t.Fatal(e)
76 }
77 if _, _, e = d.Get(ctx, "outer/inner/key"); !errors.Is(e, os.ErrNotExist) {
78 t.Fatal(e)
79 }
80 if b, _, e = d.Get(ctx, "key"); e != nil || string(b) != "outside" {
81 t.Fatal("scope escaped")
82 }
83 if e = DeleteIfMatch(ctx, scoped, "key", current); e != nil {
84 t.Fatal("absence not idempotent", e)
85 }
86 if e = DeleteIfMatch(ctx, d, "../escape", current); e == nil {
87 t.Fatal("traversal accepted")
88 }
89}
90func TestConditionalDeleteDirRacesPut(t *testing.T) {
91 ctx := context.Background()
92 d := NewDirStore(t.TempDir())
93 for i := 0; i < 100; i++ {
94 key := fmt.Sprint(i)
95 old, e := d.Put(ctx, key, []byte("old"), "")
96 if e != nil {
97 t.Fatal(e)
98 }
99 start := make(chan struct{})
100 results := make(chan error, 2)
101 go func() { <-start; _, e := d.Put(ctx, key, []byte("new"), old); results <- e }()
102 go func() { <-start; results <- d.DeleteIfMatch(ctx, key, old) }()
103 close(start)
104 a, b := <-results, <-results
105 if (a == nil) == (b == nil) {
106 t.Fatal("both operations won or lost", a, b)
107 }
108 if a != nil && !errors.Is(a, ErrCASConflict) || b != nil && !errors.Is(b, ErrCASConflict) {
109 t.Fatal(a, b)
110 }
111 body, _, e := d.Get(ctx, key)
112 if e != nil && !errors.Is(e, os.ErrNotExist) {
113 t.Fatal(e)
114 }
115 if e == nil && string(body) != "new" {
116 t.Fatal("unexpected surviving revision")
117 }
118 }
119}
120
121func TestConditionalDeleteS3ActualWire(t *testing.T) {
122 for _, absentStatus := range []int{204, 404} {
123 t.Run(fmt.Sprint(absentStatus), func(t *testing.T) {
124 var mu sync.Mutex
125 objects := map[string]string{"/bucket/scope/key": "current", "/bucket/key": "outside"}
126 calls := 0
127 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
128 mu.Lock()
129 defer mu.Unlock()
130 calls++
131 if r.Method != "DELETE" {
132 t.Error("unexpected pre-read or method", r.Method)
133 w.WriteHeader(405)
134 return
135 }
136 if len(r.Header.Values("If-Match")) != 1 || r.Header.Get("If-Match") == "" || r.Header.Get("If-None-Match") != "" {
137 t.Error("missing exact condition")
138 w.WriteHeader(400)
139 return
140 }
141 etag, exists := objects[r.URL.Path]
142 if !exists {
143 if absentStatus == 404 {
144 w.Header().Set("Content-Type", "application/xml")
145 w.WriteHeader(404)
146 fmt.Fprint(w, `<Error><Code>NoSuchKey</Code></Error>`)
147 } else {
148 w.WriteHeader(204)
149 }
150 return
151 }
152 if r.Header.Get("If-Match") != `"`+etag+`"` {
153 w.Header().Set("Content-Type", "application/xml")
154 w.WriteHeader(412)
155 fmt.Fprint(w, `<Error><Code>PreconditionFailed</Code></Error>`)
156 return
157 }
158 delete(objects, r.URL.Path)
159 w.WriteHeader(204)
160 }))
161 defer srv.Close()
162 s := &S3Store{Bucket: "bucket", Client: awss3.New(awss3.Options{Region: "us-east-1", Credentials: credentials.NewStaticCredentialsProvider("fixture", "fixture", ""), BaseEndpoint: aws.String(srv.URL), UsePathStyle: true, RetryMaxAttempts: 1})}
163 ctx := context.Background()
164 scoped := NewPrefixStore(s, "scope")
165 if e := DeleteIfMatch(ctx, scoped, "key", `"stale"`); !errors.Is(e, ErrCASConflict) {
166 t.Fatal(e)
167 }
168 mu.Lock()
169 retained := objects["/bucket/scope/key"]
170 mu.Unlock()
171 if retained != "current" {
172 t.Fatal("wrong tag deleted object")
173 }
174 if e := DeleteIfMatch(ctx, scoped, "key", `"current"`); e != nil {
175 t.Fatal(e)
176 }
177 if e := DeleteIfMatch(ctx, scoped, "key", `"current"`); e != nil {
178 t.Fatal("absence not accepted", e)
179 }
180 mu.Lock()
181 defer mu.Unlock()
182 if _, ok := objects["/bucket/scope/key"]; ok || objects["/bucket/key"] != "outside" || calls != 3 {
183 t.Fatal("conditional scope or request count", objects, calls)
184 }
185 })
186 }
187}
188