package s3 import ( "context" "errors" "fmt" "git.yuntuo.hk/k2safe/go-omni/internal/storagefixture" "git.yuntuo.hk/k2safe/go-omni/storage" "git.yuntuo.hk/k2safe/go-omni/storage/storagetest" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "io" "net/http" "net/http/httptest" "os" "strings" "testing" "time" ) func TestS3ProtocolAndR2Config(t *testing.T) { server := httptest.NewTLSServer(storagefixture.Handler()) defer server.Close() creds := credentials.NewStaticV4("fixtureAccess", "fixtureSecret", "") s, err := Open(Config{Endpoint: strings.TrimPrefix(server.URL, "https://"), Region: "us-east-1", Bucket: "testbucket", Secure: true, MaxBytes: 64, Credentials: creds, Transport: server.Client().Transport}) if err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if _, err = s.Get(ctx, "open-config-missing"); !errors.Is(err, storage.ErrNotFound) { t.Fatal("configured client did not reach protocol fixture", err) } // Malformed-body cases intentionally cause SDK errors. Disable retries on // this test-owned client so randomized backoff cannot exhaust the shared // contract deadline. Open above still checks the production client config. client, err := minio.New(strings.TrimPrefix(server.URL, "https://"), &minio.Options{ Creds: creds, Region: "us-east-1", Secure: true, Transport: server.Client().Transport, MaxRetries: 1, }) if err != nil { t.Fatal(err) } contractStore, err := New(client, "testbucket", 64) if err != nil { t.Fatal(err) } storagetest.Run(t, contractStore) cfg, err := R2Config(strings.Repeat("a", 32), "bucket", creds) if err != nil { t.Fatal(err) } r2, err := Open(cfg) if err != nil { t.Fatal(err) } u, err := r2.PresignGet(context.Background(), "path/file", time.Minute) if err != nil || u.Host != strings.Repeat("a", 32)+".r2.cloudflarestorage.com" || u.Query().Get("X-Amz-Signature") == "" || !strings.Contains(u.Query().Get("X-Amz-Credential"), "/auto/s3/") { t.Fatal("R2 presign settings", err) } } func TestS3Integration(t *testing.T) { address := os.Getenv("GO_OMNI_S3_TEST_ADDR") if address == "" { t.Skip("requires isolated MinIO") } ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) defer cancel() ready := false for n := 0; n < 100; n++ { req, _ := http.NewRequestWithContext(ctx, "GET", "http://"+address+"/minio/health/ready", nil) r, err := http.DefaultClient.Do(req) if err == nil { r.Body.Close() if r.StatusCode == 200 { ready = true break } } if ctx.Err() != nil { break } time.Sleep(100 * time.Millisecond) } if !ready { t.Fatal("isolated S3 server did not start") } // The contract deliberately sends malformed bodies. Avoid retry backoff // exhausting its deadline; these are expected failures, not network retries. client, err := minio.New(address, &minio.Options{Creds: credentials.NewStaticV4("goOmniTest", "goOmniTestSecret", ""), Region: "us-east-1", MaxRetries: 1}) if err != nil { t.Fatal(err) } bucket := fmt.Sprintf("test-%d", time.Now().UnixNano()) if err = client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}); err != nil { t.Fatal(err) } defer client.RemoveBucket(context.Background(), bucket) uploads, _ := New(client, bucket, 6<<20) httpClient := &http.Client{Timeout: 10 * time.Second} storagetest.RunUploads(t, uploads, httpClient) link, err := uploads.PresignPut(ctx, "constrained", 5, "text/plain", time.Minute) if err != nil { t.Fatal(err) } for _, tc := range []struct{ body, contentType string }{{"longer", "text/plain"}, {"hello", "image/png"}} { req, _ := http.NewRequestWithContext(ctx, link.Method, link.URL, strings.NewReader(tc.body)) req.Header = link.Headers.Clone() req.Header.Set("Content-Type", tc.contentType) response, requestErr := httpClient.Do(req) if requestErr != nil { t.Fatal(requestErr) } response.Body.Close() if response.StatusCode != 403 { t.Fatalf("tampered upload constraints accepted: %d", response.StatusCode) } } s, _ := New(client, bucket, 64) checkReadOptionsIntegration(t, s) storagetest.Run(t, s) s.Put(ctx, "signed", strings.NewReader("hello"), 5, "text/plain") defer s.Delete(context.Background(), "signed") u, err := s.PresignGet(ctx, "signed", time.Minute) if err != nil { t.Fatal(err) } response, err := http.Get(u.String()) if err != nil { t.Fatal(err) } raw, _ := io.ReadAll(response.Body) response.Body.Close() if response.StatusCode != 200 || string(raw) != "hello" { t.Fatal(response.StatusCode, string(raw)) } q := u.Query() q.Set("X-Amz-Signature", strings.Repeat("0", 64)) u.RawQuery = q.Encode() response, err = http.Get(u.String()) if err != nil { t.Fatal(err) } response.Body.Close() if response.StatusCode != 403 { t.Fatal("tampered signature", response.StatusCode) } }