1// SPDX-License-Identifier: AGPL-3.0-or-later
2package blobs3_test
3
4import (
5 "context"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10
11 "bnema.dev/ermite/internal/adapters/blobs3"
12 "bnema.dev/ermite/internal/adapters/blobtest"
13 "bnema.dev/ermite/internal/ports"
14 "github.com/johannesboyne/gofakes3"
15 "github.com/johannesboyne/gofakes3/backend/s3mem"
16 "github.com/stretchr/testify/require"
17)
18
19func fake(t *testing.T) (*blobs3.Store, *s3mem.Backend) {
20 t.Helper()
21 backend := s3mem.New()
22 require.NoError(t, backend.CreateBucket("b"))
23 handler := gofakes3.New(backend).Server()
24 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25 t.Logf("S3 operation: %s %s", r.Method, r.URL.RequestURI())
26 for _, q := range []string{"policy", "acl", "tagging", "lifecycle", "versioning"} {
27 if r.URL.Query().Has(q) {
28 t.Errorf("unexpected admin operation %s", r.URL)
29 }
30 }
31 if r.Method == http.MethodPut && strings.Trim(r.URL.Path, "/") == "b" {
32 t.Error("unexpected bucket creation")
33 }
34 if r.Method == http.MethodPut && strings.HasPrefix(r.URL.Path, "/b/") {
35 if r.Header.Get("X-Amz-Content-Sha256") == "UNSIGNED-PAYLOAD" || r.Header.Get("X-Amz-Content-Sha256") == "" {
36 t.Error("payload must be signed")
37 }
38 }
39 handler.ServeHTTP(w, r)
40 }))
41 t.Cleanup(srv.Close)
42 s, e := blobs3.New(context.Background(), blobs3.Config{Bucket: "b", Prefix: "/pfx/", Endpoint: srv.URL, Region: "us-east-1", AccessKeyID: "test", SecretAccessKey: "test-secret", ForcePathStyle: true})
43 require.NoError(t, e)
44 return s, backend
45}
46func TestContract(t *testing.T) {
47 blobtest.Run(t, func(t *testing.T) ports.BlobStore { s, _ := fake(t); return s })
48}
49func TestPrefix(t *testing.T) {
50 s, b := fake(t)
51 _, e := s.Put(context.Background(), "ci/one", strings.NewReader("x"), 1)
52 require.NoError(t, e)
53 objects, e := b.ListBucket("b", nil, gofakes3.ListBucketPage{})
54 require.NoError(t, e)
55 require.Len(t, objects.Contents, 1)
56 require.Equal(t, "pfx/ci/one", objects.Contents[0].Key)
57}
58func TestMissingBucket(t *testing.T) {
59 s, b := fake(t)
60 require.NoError(t, b.DeleteBucket("b"))
61 e := s.Check(context.Background())
62 require.ErrorContains(t, e, `bucket "b" is not reachable at http://`)
63 require.NotContains(t, e.Error(), "test-secret")
64}
65
66func TestCheckDeniedHeadFallsBack(t *testing.T) {
67 var listed bool
68 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
69 if r.Method == http.MethodHead {
70 w.WriteHeader(http.StatusForbidden)
71 return
72 }
73 if r.Method != http.MethodGet || r.URL.Query().Get("list-type") != "2" || r.URL.Query().Get("max-keys") != "1" || r.URL.Query().Get("prefix") != "pfx/" {
74 t.Errorf("unexpected probe: %s %s", r.Method, r.URL)
75 }
76 listed = true
77 w.Header().Set("Content-Type", "application/xml")
78 w.Write([]byte(`<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Name>b</Name><IsTruncated>false</IsTruncated></ListBucketResult>`))
79 }))
80 defer srv.Close()
81 s, e := blobs3.New(context.Background(), blobs3.Config{Bucket: "b", Prefix: "pfx", Endpoint: srv.URL, Region: "test-region", AccessKeyID: "test", SecretAccessKey: "test-secret", ForcePathStyle: true})
82 require.NoError(t, e)
83 require.NoError(t, s.Check(context.Background()))
84 require.True(t, listed)
85}