Files
OpenFlare/backend/plugins/infra/infra_test.go
T
ryan 528240026d chore(lint): unify formatting on golangci-lint fmt (gofumpt), uncap issue reporting, fix gofumpt drift
- make format 现在与 code-check 使用同一格式化器(golangci-lint fmt),消除 goimports -local 与 gofumpt 的格式拉锯
- .golangci.yml 关闭默认 50/3 截断,完整上报所有问题(只增强不弱化)
- 全库 gofumpt 规范化(203 files, 纯格式无行为变更)
2026-08-28 16:31:53 +08:00

323 lines
8.9 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package infra_test
import (
"Wavelet/core"
"Wavelet/core/contracts"
"Wavelet/plugins/infra/cache"
"Wavelet/plugins/infra/database"
"Wavelet/plugins/infra/logger"
"Wavelet/plugins/infra/storage"
"Wavelet/plugins/infra/storage/objectstore"
"bytes"
"context"
"io"
"path/filepath"
"sync"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/glebarez/sqlite"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
type TestUser struct {
ID uint64 `gorm:"primaryKey"`
Name string
}
func setupTestDB(t *testing.T) *gorm.DB {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "test.db")
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&TestUser{}))
return db
}
func TestDatabasePlugin(t *testing.T) {
ctx := core.NewContext(context.Background())
testDB := setupTestDB(t)
p := database.New(database.WithDB(testDB))
require.Equal(t, "database", p.Name())
require.NoError(t, p.Apply(ctx))
dbSvc, err := core.Inject[contracts.DBService](ctx)
require.NoError(t, err)
require.NotNil(t, dbSvc)
// Verify GORM and DB methods
assert.NotNil(t, dbSvc.GORM())
assert.NotNil(t, dbSvc.DB(context.Background()))
// Test CRUD via service
user := TestUser{ID: 1, Name: "Alice"}
require.NoError(t, dbSvc.DB(context.Background()).Create(&user).Error)
var fetched TestUser
require.NoError(t, dbSvc.GORM().First(&fetched, 1).Error)
assert.Equal(t, "Alice", fetched.Name)
// Test NamedDB fallback
assert.NotNil(t, dbSvc.Named("replica"))
}
func TestCachePluginRAMOnly(t *testing.T) {
ctx := core.NewContext(context.Background())
p := cache.New()
require.Equal(t, "cache", p.Name())
require.NoError(t, p.Apply(ctx))
cacheSvc, err := core.Inject[contracts.CacheService](ctx)
require.NoError(t, err)
require.NotNil(t, cacheSvc)
type CacheItem struct {
Name string `json:"name"`
Count int `json:"count"`
}
testCtx := context.Background()
// 1. Get non-existing key
var notFound CacheItem
err = cacheSvc.Get(testCtx, "missing:key", &notFound)
assert.ErrorIs(t, err, contracts.ErrCacheMiss)
// 2. Set and Get
item := CacheItem{Name: "item1", Count: 42}
require.NoError(t, cacheSvc.Set(testCtx, "test:item1", item, time.Minute))
var retrieved CacheItem
require.NoError(t, cacheSvc.Get(testCtx, "test:item1", &retrieved))
assert.Equal(t, item, retrieved)
// 3. GetOrSet
var getOrSetTarget CacheItem
var loaderCalled bool
err = cacheSvc.GetOrSet(testCtx, "test:item1", &getOrSetTarget, time.Minute, func() (any, error) {
loaderCalled = true
return CacheItem{Name: "never_called", Count: 0}, nil
})
require.NoError(t, err)
assert.False(t, loaderCalled)
assert.Equal(t, item, getOrSetTarget)
// GetOrSet with cache miss
var newItem CacheItem
err = cacheSvc.GetOrSet(testCtx, "test:item2", &newItem, time.Minute, func() (any, error) {
loaderCalled = true
return CacheItem{Name: "loaded", Count: 99}, nil
})
require.NoError(t, err)
assert.True(t, loaderCalled)
assert.Equal(t, "loaded", newItem.Name)
assert.Equal(t, 99, newItem.Count)
// 4. Delete
require.NoError(t, cacheSvc.Delete(testCtx, "test:item1"))
err = cacheSvc.Get(testCtx, "test:item1", &retrieved)
assert.ErrorIs(t, err, contracts.ErrCacheMiss)
// 5. Invalidate alias
require.NoError(t, cacheSvc.Invalidate(testCtx, "test:item2"))
err = cacheSvc.Get(testCtx, "test:item2", &retrieved)
assert.ErrorIs(t, err, contracts.ErrCacheMiss)
}
func TestCachePluginWithRedisAndPubSub(t *testing.T) {
mr, err := miniredis.Run()
require.NoError(t, err)
defer mr.Close()
rdb := redis.NewClient(&redis.Options{
Addr: mr.Addr(),
})
defer func() { _ = rdb.Close() }()
ctx1 := core.NewContext(context.Background())
ctx2 := core.NewContext(context.Background())
p1 := cache.New(cache.WithRedis(rdb), cache.WithKeyPrefix("test:"))
p2 := cache.New(cache.WithRedis(rdb), cache.WithKeyPrefix("test:"))
require.NoError(t, p1.Apply(ctx1))
require.NoError(t, p2.Apply(ctx2))
cache1, err := core.Inject[contracts.CacheService](ctx1)
require.NoError(t, err)
cache2, err := core.Inject[contracts.CacheService](ctx2)
require.NoError(t, err)
testCtx := context.Background()
// Node 1 writes to cache
type UserCache struct {
Name string `json:"name"`
}
require.NoError(t, cache1.Set(testCtx, "user:100", UserCache{Name: "Bob"}, 10*time.Minute))
// Node 2 reads from cache (misses Node 2's RAM, hits Redis, backfills Node 2's RAM)
var u2 UserCache
require.NoError(t, cache2.Get(testCtx, "user:100", &u2))
assert.Equal(t, "Bob", u2.Name)
// Node 1 deletes cache (evicts Node 1 RAM, Redis, and broadcasts to Node 2)
require.NoError(t, cache1.Delete(testCtx, "user:100"))
// Verify Redis is deleted
var uRedis UserCache
err = cache1.Get(testCtx, "user:100", &uRedis)
assert.ErrorIs(t, err, contracts.ErrCacheMiss)
// Clean up contexts
require.NoError(t, ctx1.Dispose())
require.NoError(t, ctx2.Dispose())
}
func TestLoggerPlugin(t *testing.T) {
ctx := core.NewContext(context.Background())
p := logger.New()
require.Equal(t, "logger", p.Name())
require.NoError(t, p.Apply(ctx))
logSvc, err := core.Inject[contracts.LoggerService](ctx)
require.NoError(t, err)
require.NotNil(t, logSvc)
testCtx := context.Background()
// Should not panic on any log call
logSvc.Debug(testCtx, "debug message", "key1", "val1")
logSvc.Info(testCtx, "info message", "userID", 123)
logSvc.Warn(testCtx, "warn message", "warning", true)
logSvc.Error(testCtx, "error message", "err", "something broke")
logSvc.Debugf(testCtx, "formatted debug %d", 1)
logSvc.Infof(testCtx, "formatted info %s", "test")
logSvc.Warnf(testCtx, "formatted warn %v", map[string]int{"a": 1})
logSvc.Errorf(testCtx, "formatted error %s", "fatal")
childLog := logSvc.With("module", "test_module")
require.NotNil(t, childLog)
childLog.Info(testCtx, "child log message", "action", "run")
}
type memoryBackend struct {
mu sync.RWMutex
storage map[string][]byte
}
func newMemoryBackend() *memoryBackend {
return &memoryBackend{
storage: make(map[string][]byte),
}
}
func (m *memoryBackend) Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) (objectstore.PutResult, error) {
m.mu.Lock()
defer m.mu.Unlock()
data, err := io.ReadAll(body)
if err != nil {
return objectstore.PutResult{}, err
}
m.storage[key] = data
return objectstore.PutResult{Key: key, Bucket: "test-bucket"}, nil
}
func (m *memoryBackend) Get(ctx context.Context, key string) (*objectstore.Object, error) {
m.mu.RLock()
defer m.mu.RUnlock()
data, ok := m.storage[key]
if !ok {
return nil, gorm.ErrRecordNotFound
}
return &objectstore.Object{
Body: io.NopCloser(bytes.NewReader(data)),
ContentLength: int64(len(data)),
ContentType: "application/octet-stream",
}, nil
}
func (m *memoryBackend) Delete(ctx context.Context, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.storage, key)
return nil
}
func (m *memoryBackend) Test(ctx context.Context) error {
return nil
}
func TestStoragePlugin(t *testing.T) {
ctx := core.NewContext(context.Background())
backend := newMemoryBackend()
p := storage.New(storage.WithBackend(backend))
require.Equal(t, "storage", p.Name())
require.NoError(t, p.Apply(ctx))
storageSvc, err := core.Inject[contracts.StorageService](ctx)
require.NoError(t, err)
require.NotNil(t, storageSvc)
testCtx := context.Background()
// 1. Put
content := []byte("Hello, Wavelet Storage Plugin!")
putRes, err := storageSvc.Put(testCtx, "uploads/hello.txt", bytes.NewReader(content), int64(len(content)), "text/plain")
require.NoError(t, err)
assert.Equal(t, "uploads/hello.txt", putRes.Key)
assert.Equal(t, "test-bucket", putRes.Bucket)
// 2. Get
obj, err := storageSvc.Get(testCtx, "uploads/hello.txt")
require.NoError(t, err)
require.NotNil(t, obj)
data, err := io.ReadAll(obj.Body)
require.NoError(t, err)
assert.Equal(t, content, data)
assert.Equal(t, int64(len(content)), obj.ContentLength)
// 3. Delete
require.NoError(t, storageSvc.Delete(testCtx, "uploads/hello.txt"))
_, err = storageSvc.Get(testCtx, "uploads/hello.txt")
assert.Error(t, err)
}
func TestAllInfraPluginsCombined(t *testing.T) {
ctx := core.NewContext(context.Background())
testDB := setupTestDB(t)
memBackend := newMemoryBackend()
require.NoError(t, database.New(database.WithDB(testDB)).Apply(ctx))
require.NoError(t, cache.New().Apply(ctx))
require.NoError(t, logger.New().Apply(ctx))
require.NoError(t, storage.New(storage.WithBackend(memBackend)).Apply(ctx))
// Using3 to resolve dependencies concurrently
var resolved bool
err := core.Using3(ctx, func(db contracts.DBService, c contracts.CacheService, l contracts.LoggerService) {
resolved = true
assert.NotNil(t, db)
assert.NotNil(t, c)
assert.NotNil(t, l)
})
require.NoError(t, err)
assert.True(t, resolved)
// Using storage
err = core.Using(ctx, func(s contracts.StorageService) {
assert.NotNil(t, s)
})
require.NoError(t, err)
}