271 lines
7.0 KiB
Go
271 lines
7.0 KiB
Go
|
|
package minio
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
miniogo "github.com/minio/minio-go/v7"
|
||
|
|
|
||
|
|
"code.nochebuena.dev/go/health"
|
||
|
|
"code.nochebuena.dev/go/logz"
|
||
|
|
)
|
||
|
|
|
||
|
|
func newLogger() logz.Logger { return logz.New(logz.Options{}) }
|
||
|
|
|
||
|
|
// mockTransport intercepts all HTTP requests and delegates to fn.
|
||
|
|
type mockTransport struct {
|
||
|
|
fn func(*http.Request) (*http.Response, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||
|
|
return m.fn(r)
|
||
|
|
}
|
||
|
|
|
||
|
|
func makeResp(status int) *http.Response {
|
||
|
|
return &http.Response{
|
||
|
|
StatusCode: status,
|
||
|
|
Body: io.NopCloser(strings.NewReader("")),
|
||
|
|
Header: make(http.Header),
|
||
|
|
Proto: "HTTP/1.1",
|
||
|
|
ProtoMajor: 1,
|
||
|
|
ProtoMinor: 1,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testConfig(transport http.RoundTripper) Config {
|
||
|
|
return Config{
|
||
|
|
Endpoint: "localhost:9000",
|
||
|
|
AccessKey: "minioadmin",
|
||
|
|
SecretKey: "minioadmin",
|
||
|
|
Bucket: "test-bucket",
|
||
|
|
Region: "us-east-1",
|
||
|
|
Transport: transport,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- construction ---
|
||
|
|
|
||
|
|
func TestNew(t *testing.T) {
|
||
|
|
if New(newLogger(), Config{}) == nil {
|
||
|
|
t.Fatal("New returned nil")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_Name(t *testing.T) {
|
||
|
|
c := New(newLogger(), Config{}).(health.Checkable)
|
||
|
|
if c.Name() != "minio" {
|
||
|
|
t.Errorf("want minio, got %s", c.Name())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_Priority(t *testing.T) {
|
||
|
|
c := New(newLogger(), Config{}).(health.Checkable)
|
||
|
|
if c.Priority() != health.LevelCritical {
|
||
|
|
t.Error("Priority() != LevelCritical")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- nil client guards ---
|
||
|
|
|
||
|
|
func TestComponent_OnStop_NilClient(t *testing.T) {
|
||
|
|
c := &minioComponent{logger: newLogger()}
|
||
|
|
if err := c.OnStop(); err != nil {
|
||
|
|
t.Errorf("OnStop with nil client: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_HealthCheck_NilClient(t *testing.T) {
|
||
|
|
c := &minioComponent{logger: newLogger()}
|
||
|
|
if err := c.HealthCheck(context.Background()); err == nil {
|
||
|
|
t.Error("HealthCheck with nil client should return error")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- OnInit + Native ---
|
||
|
|
|
||
|
|
func TestComponent_OnInit_And_Native(t *testing.T) {
|
||
|
|
cfg := testConfig(&mockTransport{fn: func(r *http.Request) (*http.Response, error) {
|
||
|
|
return makeResp(http.StatusOK), nil
|
||
|
|
}})
|
||
|
|
c := New(newLogger(), cfg)
|
||
|
|
if err := c.OnInit(); err != nil {
|
||
|
|
t.Fatalf("OnInit: %v", err)
|
||
|
|
}
|
||
|
|
if c.Native() == nil {
|
||
|
|
t.Error("Native() returned nil after OnInit")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- OnStart: bucket already exists ---
|
||
|
|
|
||
|
|
func TestComponent_OnStart_BucketExists(t *testing.T) {
|
||
|
|
putCalled := false
|
||
|
|
cfg := testConfig(&mockTransport{fn: func(r *http.Request) (*http.Response, error) {
|
||
|
|
if r.Method == http.MethodPut {
|
||
|
|
putCalled = true
|
||
|
|
}
|
||
|
|
return makeResp(http.StatusOK), nil
|
||
|
|
}})
|
||
|
|
c := New(newLogger(), cfg)
|
||
|
|
if err := c.OnInit(); err != nil {
|
||
|
|
t.Fatalf("OnInit: %v", err)
|
||
|
|
}
|
||
|
|
if err := c.OnStart(); err != nil {
|
||
|
|
t.Fatalf("OnStart: %v", err)
|
||
|
|
}
|
||
|
|
if putCalled {
|
||
|
|
t.Error("MakeBucket should not be called when bucket already exists")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- OnStart: bucket missing → created ---
|
||
|
|
|
||
|
|
func TestComponent_OnStart_BucketMissing(t *testing.T) {
|
||
|
|
putCalled := false
|
||
|
|
cfg := testConfig(&mockTransport{fn: func(r *http.Request) (*http.Response, error) {
|
||
|
|
if r.Method == http.MethodHead {
|
||
|
|
return makeResp(http.StatusNotFound), nil
|
||
|
|
}
|
||
|
|
if r.Method == http.MethodPut {
|
||
|
|
putCalled = true
|
||
|
|
return makeResp(http.StatusOK), nil
|
||
|
|
}
|
||
|
|
return makeResp(http.StatusOK), nil
|
||
|
|
}})
|
||
|
|
c := New(newLogger(), cfg)
|
||
|
|
if err := c.OnInit(); err != nil {
|
||
|
|
t.Fatalf("OnInit: %v", err)
|
||
|
|
}
|
||
|
|
if err := c.OnStart(); err != nil {
|
||
|
|
t.Fatalf("OnStart: %v", err)
|
||
|
|
}
|
||
|
|
if !putCalled {
|
||
|
|
t.Error("MakeBucket should be called when bucket does not exist")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- OnStart: bucket check error ---
|
||
|
|
|
||
|
|
func TestComponent_OnStart_BucketError(t *testing.T) {
|
||
|
|
cfg := testConfig(&mockTransport{fn: func(r *http.Request) (*http.Response, error) {
|
||
|
|
return makeResp(http.StatusInternalServerError), nil
|
||
|
|
}})
|
||
|
|
c := New(newLogger(), cfg)
|
||
|
|
if err := c.OnInit(); err != nil {
|
||
|
|
t.Fatalf("OnInit: %v", err)
|
||
|
|
}
|
||
|
|
if err := c.OnStart(); err == nil {
|
||
|
|
t.Error("OnStart should return error on 500 response")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- HealthCheck: unreachable ---
|
||
|
|
|
||
|
|
func TestComponent_HealthCheck_Unreachable(t *testing.T) {
|
||
|
|
cfg := testConfig(&mockTransport{fn: func(r *http.Request) (*http.Response, error) {
|
||
|
|
return nil, &mockNetError{}
|
||
|
|
}})
|
||
|
|
c := New(newLogger(), cfg)
|
||
|
|
if err := c.OnInit(); err != nil {
|
||
|
|
t.Fatalf("OnInit: %v", err)
|
||
|
|
}
|
||
|
|
if err := c.HealthCheck(context.Background()); err == nil {
|
||
|
|
t.Error("HealthCheck should return error when endpoint is unreachable")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Client interface methods ---
|
||
|
|
|
||
|
|
func startedComponent(t *testing.T, transport http.RoundTripper) Component {
|
||
|
|
t.Helper()
|
||
|
|
cfg := testConfig(transport)
|
||
|
|
c := New(newLogger(), cfg)
|
||
|
|
if err := c.OnInit(); err != nil {
|
||
|
|
t.Fatalf("OnInit: %v", err)
|
||
|
|
}
|
||
|
|
return c
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_PutObject_Success(t *testing.T) {
|
||
|
|
c := startedComponent(t, &mockTransport{fn: func(r *http.Request) (*http.Response, error) {
|
||
|
|
resp := makeResp(http.StatusOK)
|
||
|
|
resp.Header.Set("ETag", `"abc123"`)
|
||
|
|
return resp, nil
|
||
|
|
}})
|
||
|
|
_, err := c.PutObject(context.Background(), "test-bucket", "products/1/img.jpg",
|
||
|
|
strings.NewReader("data"), 4, miniogo.PutObjectOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Errorf("PutObject: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_RemoveObject_Success(t *testing.T) {
|
||
|
|
c := startedComponent(t, &mockTransport{fn: func(r *http.Request) (*http.Response, error) {
|
||
|
|
return makeResp(http.StatusNoContent), nil
|
||
|
|
}})
|
||
|
|
if err := c.RemoveObject(context.Background(), "test-bucket", "products/1/img.jpg",
|
||
|
|
miniogo.RemoveObjectOptions{}); err != nil {
|
||
|
|
t.Errorf("RemoveObject: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_GetObject_Success(t *testing.T) {
|
||
|
|
c := startedComponent(t, &mockTransport{fn: func(r *http.Request) (*http.Response, error) {
|
||
|
|
resp := makeResp(http.StatusOK)
|
||
|
|
resp.Body = io.NopCloser(strings.NewReader("image-bytes"))
|
||
|
|
resp.ContentLength = 11
|
||
|
|
return resp, nil
|
||
|
|
}})
|
||
|
|
obj, err := c.GetObject(context.Background(), "test-bucket", "products/1/img.jpg",
|
||
|
|
miniogo.GetObjectOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("GetObject: %v", err)
|
||
|
|
}
|
||
|
|
if obj == nil {
|
||
|
|
t.Error("GetObject returned nil object")
|
||
|
|
}
|
||
|
|
obj.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_PresignedGetObject_Success(t *testing.T) {
|
||
|
|
c := startedComponent(t, nil)
|
||
|
|
u, err := c.PresignedGetObject(context.Background(), "test-bucket", "products/1/img.jpg",
|
||
|
|
time.Hour, nil)
|
||
|
|
if err != nil {
|
||
|
|
t.Errorf("PresignedGetObject: %v", err)
|
||
|
|
}
|
||
|
|
if u == nil {
|
||
|
|
t.Error("PresignedGetObject returned nil URL")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_HandleError_Passthrough(t *testing.T) {
|
||
|
|
c := New(newLogger(), Config{})
|
||
|
|
if c.HandleError(nil) != nil {
|
||
|
|
t.Error("HandleError(nil) should return nil")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_OnStop_NilsClient(t *testing.T) {
|
||
|
|
c := startedComponent(t, &mockTransport{fn: func(r *http.Request) (*http.Response, error) {
|
||
|
|
return makeResp(http.StatusOK), nil
|
||
|
|
}})
|
||
|
|
if err := c.OnStop(); err != nil {
|
||
|
|
t.Errorf("OnStop: %v", err)
|
||
|
|
}
|
||
|
|
if c.Native() != nil {
|
||
|
|
t.Error("Native() should return nil after OnStop")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// mockNetError satisfies net.Error for the transport error test.
|
||
|
|
type mockNetError struct{}
|
||
|
|
|
||
|
|
func (e *mockNetError) Error() string { return "mock: connection refused" }
|
||
|
|
func (e *mockNetError) Timeout() bool { return false }
|
||
|
|
func (e *mockNetError) Temporary() bool { return false }
|