mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
528240026d
- make format 现在与 code-check 使用同一格式化器(golangci-lint fmt),消除 goimports -local 与 gofumpt 的格式拉锯 - .golangci.yml 关闭默认 50/3 截断,完整上报所有问题(只增强不弱化) - 全库 gofumpt 规范化(203 files, 纯格式无行为变更)
323 lines
8.9 KiB
Go
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", ¬Found)
|
|
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)
|
|
}
|