package storage import ( "bytes" "context" "io" "os" "strings" "testing" "time" "git.unkin.net/unkin/artifactapi/internal/testsupport" ) var testS3 *S3 func TestMain(m *testing.M) { ctx := context.Background() conn, terminate, err := testsupport.StartMinio(ctx) if err != nil { os.Exit(m.Run()) } var s3 *S3 for i := 0; i < 20; i++ { // MinIO can report ready before bucket ops succeed if s3, err = NewS3(conn.Endpoint, conn.AccessKey, conn.SecretKey, "test-bucket", false, ""); err == nil { break } time.Sleep(500 * time.Millisecond) } if err != nil { terminate() panic(err) } testS3 = s3 code := m.Run() terminate() if code != 0 { os.Exit(code) } } func requireS3(t *testing.T) { t.Helper() if testS3 == nil { t.Skip("Docker unavailable; skipping storage integration test") } } func TestKeys(t *testing.T) { if BlobKey("abc") != "blobs/sha256/abc" { t.Error("BlobKey") } if IndexKey("remote", "path/to/x") != "indexes/remote/path/to/x" { t.Error("IndexKey") } } func TestS3RoundTrip(t *testing.T) { requireS3(t) ctx := context.Background() key := "blobs/sha256/test1" content := []byte("hello storage") if err := testS3.Upload(ctx, key, bytes.NewReader(content), int64(len(content)), "text/plain"); err != nil { t.Fatalf("upload: %v", err) } exists, err := testS3.Exists(ctx, key) if err != nil || !exists { t.Fatalf("exists after upload: %v %v", exists, err) } reader, info, err := testS3.Download(ctx, key) if err != nil { t.Fatalf("download: %v", err) } got, _ := io.ReadAll(reader) reader.Close() if !bytes.Equal(got, content) { t.Errorf("content mismatch: %q", got) } if info.Size != int64(len(content)) || info.ContentType != "text/plain" { t.Errorf("stat info wrong: size=%d ct=%s", info.Size, info.ContentType) } if _, err := testS3.Stat(ctx, key); err != nil { t.Errorf("stat: %v", err) } if err := testS3.Delete(ctx, key); err != nil { t.Fatalf("delete: %v", err) } if exists, _ := testS3.Exists(ctx, key); exists { t.Error("expected object gone after delete") } } func TestS3DownloadMissing(t *testing.T) { requireS3(t) if _, _, err := testS3.Download(context.Background(), "does/not/exist"); err == nil { t.Error("expected error downloading missing key") } } func TestCASStore(t *testing.T) { requireS3(t) ctx := context.Background() cas := NewCAS(testS3) content := "content-addressed payload" res, err := cas.Store(ctx, strings.NewReader(content), "text/plain") if err != nil { t.Fatalf("store: %v", err) } if res.AlreadyExists { t.Error("first store should not report AlreadyExists") } if res.SizeBytes != int64(len(content)) || !strings.HasPrefix(res.ContentHash, "sha256:") { t.Errorf("unexpected result: %+v", res) } // Storing identical content again is deduplicated. res2, err := cas.Store(ctx, strings.NewReader(content), "text/plain") if err != nil { t.Fatalf("store again: %v", err) } if !res2.AlreadyExists || res2.ContentHash != res.ContentHash { t.Errorf("second store should dedup: %+v", res2) } // The stored blob is retrievable. reader, _, err := testS3.Download(ctx, res.S3Key) if err != nil { t.Fatalf("download stored blob: %v", err) } got, _ := io.ReadAll(reader) reader.Close() if string(got) != content { t.Errorf("stored content mismatch: %q", got) } }