test: storage (MinIO) and cache (Redis) integration tests
storage 0->79%, cache 0->92%, via the testsupport container harness.
This commit is contained in:
Vendored
+133
@@ -0,0 +1,133 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
||||
)
|
||||
|
||||
var testRedis *Redis
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ctx := context.Background()
|
||||
url, terminate, err := testsupport.StartRedis(ctx)
|
||||
if err != nil {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
r, err := NewRedis(url)
|
||||
if err != nil {
|
||||
terminate()
|
||||
panic(err)
|
||||
}
|
||||
testRedis = r
|
||||
code := m.Run()
|
||||
r.Close()
|
||||
terminate()
|
||||
if code != 0 {
|
||||
os.Exit(code)
|
||||
}
|
||||
}
|
||||
|
||||
func requireRedis(t *testing.T) {
|
||||
t.Helper()
|
||||
if testRedis == nil {
|
||||
t.Skip("Docker unavailable; skipping cache integration test")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRedisInvalid(t *testing.T) {
|
||||
if _, err := NewRedis("://bad-url"); err == nil {
|
||||
t.Error("expected error for invalid redis URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTTL(t *testing.T) {
|
||||
requireRedis(t)
|
||||
ctx := context.Background()
|
||||
if fresh, _ := testRedis.CheckTTL(ctx, "r", "missing"); fresh {
|
||||
t.Error("missing key should not be fresh")
|
||||
}
|
||||
if err := testRedis.SetTTL(ctx, "r", "p", time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fresh, err := testRedis.CheckTTL(ctx, "r", "p"); err != nil || !fresh {
|
||||
t.Errorf("expected fresh after SetTTL: %v %v", fresh, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
requireRedis(t)
|
||||
ctx := context.Background()
|
||||
ok, err := testRedis.AcquireLock(ctx, "r", "lockpath", time.Minute)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("first acquire should succeed: %v %v", ok, err)
|
||||
}
|
||||
if ok, _ := testRedis.AcquireLock(ctx, "r", "lockpath", time.Minute); ok {
|
||||
t.Error("second acquire should fail while held")
|
||||
}
|
||||
if err := testRedis.ReleaseLock(ctx, "r", "lockpath"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ok, _ := testRedis.AcquireLock(ctx, "r", "lockpath", time.Minute); !ok {
|
||||
t.Error("acquire should succeed after release")
|
||||
}
|
||||
}
|
||||
|
||||
func TestETagAndToken(t *testing.T) {
|
||||
requireRedis(t)
|
||||
ctx := context.Background()
|
||||
if v, _ := testRedis.GetETag(ctx, "r", "missing"); v != "" {
|
||||
t.Error("missing etag should be empty")
|
||||
}
|
||||
testRedis.SetETag(ctx, "r", "p", `"abc"`, time.Minute)
|
||||
if v, _ := testRedis.GetETag(ctx, "r", "p"); v != `"abc"` {
|
||||
t.Errorf("etag = %q", v)
|
||||
}
|
||||
|
||||
if v, _ := testRedis.GetToken(ctx, "missing"); v != "" {
|
||||
t.Error("missing token should be empty")
|
||||
}
|
||||
testRedis.SetToken(ctx, "key", "tok", time.Minute)
|
||||
if v, _ := testRedis.GetToken(ctx, "key"); v != "tok" {
|
||||
t.Errorf("token = %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircuit(t *testing.T) {
|
||||
requireRedis(t)
|
||||
ctx := context.Background()
|
||||
if n, _ := testRedis.GetCircuitFailures(ctx, "cr"); n != 0 {
|
||||
t.Errorf("initial failures = %d", n)
|
||||
}
|
||||
n1, err := testRedis.IncrCircuitFailure(ctx, "cr", time.Minute)
|
||||
if err != nil || n1 != 1 {
|
||||
t.Fatalf("first incr = %d %v", n1, err)
|
||||
}
|
||||
n2, _ := testRedis.IncrCircuitFailure(ctx, "cr", time.Minute)
|
||||
if n2 != 2 {
|
||||
t.Errorf("second incr = %d", n2)
|
||||
}
|
||||
if n, _ := testRedis.GetCircuitFailures(ctx, "cr"); n != 2 {
|
||||
t.Errorf("get failures = %d", n)
|
||||
}
|
||||
testRedis.ResetCircuit(ctx, "cr")
|
||||
if n, _ := testRedis.GetCircuitFailures(ctx, "cr"); n != 0 {
|
||||
t.Errorf("failures after reset = %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlushRemote(t *testing.T) {
|
||||
requireRedis(t)
|
||||
ctx := context.Background()
|
||||
testRedis.SetTTL(ctx, "flushme", "a", time.Hour)
|
||||
testRedis.SetETag(ctx, "flushme", "a", "x", time.Hour)
|
||||
if err := testRedis.FlushRemote(ctx, "flushme"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fresh, _ := testRedis.CheckTTL(ctx, "flushme", "a"); fresh {
|
||||
t.Error("expected keys flushed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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())
|
||||
}
|
||||
s3, err := NewS3(conn.Endpoint, conn.AccessKey, conn.SecretKey, "test-bucket", false, "")
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user