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 }