Skip to content

Latest commit

 

History

History
216 lines (164 loc) · 6.06 KB

testing.md

File metadata and controls

216 lines (164 loc) · 6.06 KB

Testing

Running Tests

# All unit tests
go test ./...

# Specific package
go test ./internal/manager/

# Verbose output
go test -v ./internal/manager/

# Integration tests (requires Docker services)
go test -tags integration ./...

Test Infrastructure

Fixtures (internal/testutil/fixtures.go)

Pre-built sample data for tests:

testutil.SamplePlugin()  // *repo.Plugin with realistic defaults
testutil.SampleTheme()   // *repo.Theme
testutil.SampleCore()    // *repo.Core
testutil.SampleConfig()  // *config.Config with test-friendly values
testutil.NopLogger()     // *zap.Logger that discards output

Hand-Written Fakes (internal/testutil/mocks.go)

Veloria uses hand-written fakes with function fields instead of code-generated mocks. Each method delegates to its function field when set, falling back to a safe zero-value default.

FakeDataSource

Implements repo.DataSource for testing the Manager without real stores:

ds := &testutil.FakeDataSource{
    TypeVal: repo.TypePlugins,
    SearchFunc: func(term string, opt *index.SearchOptions, fn func(int, int)) ([]*repo.SearchResult, error) {
        return []*repo.SearchResult{
            {Extension: testutil.SamplePlugin(), Matches: []*index.FileMatch{{Filename: "main.php", Matches: []*index.Match{{}}}}},
        }, nil
    },
}

m := newTestManager(t, ds)
resp, err := m.Search("plugins", "test", nil)

Available function fields:

Field Signature
LoadFunc func() error
StatsFunc func() (int, int)
IndexStatusFunc func() map[string]bool
SearchFunc func(term string, opt *index.SearchOptions, fn func(int, int)) ([]*repo.SearchResult, error)
PrepareUpdatesFunc func() ([]repo.IndexTask, error)
ResumeUnindexedFunc func() []repo.IndexTask
GetExtensionFunc func(slug string) (repo.Extension, bool)
MakeReindexTaskFunc func(slug string) (repo.IndexTask, bool)
ResolveSourceDirFunc func(slug string) (string, error)

RecordIndexSuccess and RecordIndexFailure are implemented as no-ops.

Web Interface Fakes

For testing web handlers without a real Manager:

// FakeSearchService implements web.SearchService
svc := &testutil.FakeSearchService{
    SearchFunc: func(repoType, term string, params *manager.SearchParams) (*manager.SearchResponse, error) {
        return &manager.SearchResponse{Total: 0}, nil
    },
}

// FakeReindexService implements web.ReindexService
reindex := &testutil.FakeReindexService{
    SubmitReindexFunc: func(repoType, slug string) error { return nil },
}

// FakeSourceResolver implements web.SourceResolver
resolver := &testutil.FakeSourceResolver{
    ResolveSourceDirFunc: func(repoType, slug string) (string, error) {
        return "/tmp/source/test-plugin", nil
    },
}

// FakeStatsProvider implements web.StatsProvider
stats := &testutil.FakeStatsProvider{
    StatsFunc: func(repoType string) (int, int, bool) { return 100, 50, true },
    IndexStatusFunc: func(repoType string) map[string]bool {
        return map[string]bool{"test-plugin": true}
    },
}

Test Manager

manager.NewTestManager() creates a Manager without loading data or starting the updater goroutine:

func newTestManager(t *testing.T, sources ...repo.DataSource) *manager.Manager {
    t.Helper()
    l := testutil.NopLogger()
    m, err := manager.NewTestManager(l, sources)
    if err != nil {
        t.Fatalf("newTestManager: %v", err)
    }
    return m
}

This is used in internal/manager/manager_test.go for fast, deterministic unit tests.

Writing Tests

Testing Manager Methods

func TestSearch_SortsByActiveInstalls(t *testing.T) {
    p1 := testutil.SamplePlugin()
    p1.Slug = "low-installs"
    p1.ActiveInstalls = 100

    p2 := testutil.SamplePlugin()
    p2.Slug = "high-installs"
    p2.ActiveInstalls = 10000

    ds := &testutil.FakeDataSource{
        TypeVal: repo.TypePlugins,
        SearchFunc: func(_ string, _ *index.SearchOptions, _ func(int, int)) ([]*repo.SearchResult, error) {
            return []*repo.SearchResult{
                {Extension: p1, Matches: []*index.FileMatch{{Filename: "a.php", Matches: []*index.Match{{}}}}},
                {Extension: p2, Matches: []*index.FileMatch{{Filename: "b.php", Matches: []*index.Match{{}}}}},
            }, nil
        },
    }

    m := newTestManager(t, ds)
    resp, err := m.Search("plugins", "test", nil)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if resp.Results[0].Slug != "high-installs" {
        t.Errorf("expected first result to be high-installs, got %s", resp.Results[0].Slug)
    }
}

Testing API Handlers

API handlers accept their dependencies via the service registry, making them straightforward to test with httptest:

func TestViewPluginV1_NotFound(t *testing.T) {
    db := setupTestDB(t) // your DB setup helper

    req := httptest.NewRequest("GET", "/api/v1/plugin/"+uuid.New().String(), nil)
    rr := httptest.NewRecorder()

    handler := plugin.ViewPluginV1(reg)
    handler.ServeHTTP(rr, req)

    if rr.Code != http.StatusOK { // API returns 200 with error payload
        t.Errorf("expected 200, got %d", rr.Code)
    }
    // Check JSON body for error response
}

Testing Web Handlers

Web handlers take *web.Deps, so you can inject fakes for any capability:

deps := &web.Deps{
    Registry: reg,  // service.Registry with test services registered
    Cache:    testCache,
    Config:   testutil.SampleConfig(),
}

Integration Tests

Integration tests use the //go:build integration build tag and testcontainers-go to spin up real PostgreSQL instances:

//go:build integration

func TestPluginStore_Integration(t *testing.T) {
    // Uses testcontainers-go to start PostgreSQL
    // Tests real DB operations
}

Run with:

go test -tags integration ./...

Compile-Time Interface Checks

Each fake includes a compile-time assertion to catch interface drift:

var _ repo.DataSource = (*FakeDataSource)(nil)

If the interface changes, the fake will fail to compile, alerting you to update it.