mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 04:01:34 +00:00
Some checks failed
- Updated database models and repositories to replace uint IDs with UUIDs. - Modified test fixtures to generate and use UUIDs for authors, translations, users, and works. - Adjusted mock implementations to align with the new UUID structure. - Ensured all relevant functions and methods are updated to handle UUIDs correctly. - Added necessary imports for UUID handling in various files.
288 lines
8.6 KiB
Go
288 lines
8.6 KiB
Go
package cache
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"tercul/internal/domain"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// memCache is a tiny in-memory implementation of platform_cache.Cache for tests.
|
|
type memCache struct {
|
|
m map[string][]byte
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func newMemCache() *memCache { return &memCache{m: map[string][]byte{}} }
|
|
|
|
func (c *memCache) Get(_ context.Context, key string, value interface{}) error {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
b, ok := c.m[key]
|
|
if !ok {
|
|
return errors.New("cache miss")
|
|
}
|
|
return json.Unmarshal(b, value)
|
|
}
|
|
|
|
func (c *memCache) Set(_ context.Context, key string, value interface{}, expiration time.Duration) error {
|
|
b, err := json.Marshal(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.m[key] = b
|
|
return nil
|
|
}
|
|
|
|
func (c *memCache) Delete(_ context.Context, key string) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
delete(c.m, key)
|
|
return nil
|
|
}
|
|
|
|
func (c *memCache) Clear(_ context.Context) error {
|
|
c.mu.Lock()
|
|
c.m = map[string][]byte{}
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
func (c *memCache) GetMulti(_ context.Context, keys []string) (map[string][]byte, error) {
|
|
res := map[string][]byte{}
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
for _, k := range keys {
|
|
if v, ok := c.m[k]; ok {
|
|
res[k] = v
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
func (c *memCache) SetMulti(_ context.Context, items map[string]interface{}, expiration time.Duration) error {
|
|
for k, v := range items {
|
|
b, _ := json.Marshal(v)
|
|
c.m[k] = b
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// dummyWorkRepo implements the minimal parts of domain.WorkRepository we need for tests.
|
|
type dummyWorkRepo struct {
|
|
store map[uint]*domain.Work
|
|
calls int
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func newDummyWorkRepo() *dummyWorkRepo { return &dummyWorkRepo{store: map[uint]*domain.Work{}} }
|
|
|
|
func (d *dummyWorkRepo) Create(_ context.Context, entity *domain.Work) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.calls++
|
|
d.store[entity.ID] = entity
|
|
return nil
|
|
}
|
|
func (d *dummyWorkRepo) CreateInTx(_ context.Context, tx *gorm.DB, entity *domain.Work) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
func (d *dummyWorkRepo) GetByID(_ context.Context, id uuid.UUID) (*domain.Work, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.calls++
|
|
w, ok := d.store[id]
|
|
if !ok {
|
|
return nil, errors.New("not found")
|
|
}
|
|
// return a copy to ensure caching serializes
|
|
cp := *w
|
|
return &cp, nil
|
|
}
|
|
func (d *dummyWorkRepo) GetByIDWithOptions(_ context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
|
return d.GetByID(context.Background(), id)
|
|
}
|
|
func (d *dummyWorkRepo) Update(_ context.Context, entity *domain.Work) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.calls++
|
|
if _, ok := d.store[entity.ID]; !ok {
|
|
return errors.New("not found")
|
|
}
|
|
d.store[entity.ID] = entity
|
|
return nil
|
|
}
|
|
func (d *dummyWorkRepo) UpdateInTx(_ context.Context, tx *gorm.DB, entity *domain.Work) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
func (d *dummyWorkRepo) Delete(_ context.Context, id uint) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.calls++
|
|
if _, ok := d.store[id]; !ok {
|
|
return errors.New("not found")
|
|
}
|
|
delete(d.store, id)
|
|
return nil
|
|
}
|
|
func (d *dummyWorkRepo) DeleteInTx(_ context.Context, tx *gorm.DB, id uint) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
func (d *dummyWorkRepo) List(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
|
return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}}, nil
|
|
}
|
|
func (d *dummyWorkRepo) ListWithOptions(_ context.Context, options *domain.QueryOptions) ([]domain.Work, error) {
|
|
return nil, nil
|
|
}
|
|
func (d *dummyWorkRepo) ListAll(_ context.Context) ([]domain.Work, error) { return nil, nil }
|
|
func (d *dummyWorkRepo) Count(_ context.Context) (int64, error) { return 0, nil }
|
|
func (d *dummyWorkRepo) CountWithOptions(_ context.Context, options *domain.QueryOptions) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
func (d *dummyWorkRepo) FindWithPreload(_ context.Context, preloads []string, id uint) (*domain.Work, error) {
|
|
return d.GetByID(context.Background(), id)
|
|
}
|
|
func (d *dummyWorkRepo) GetAllForSync(_ context.Context, batchSize, offset int) ([]domain.Work, error) {
|
|
return nil, nil
|
|
}
|
|
func (d *dummyWorkRepo) Exists(_ context.Context, id uint) (bool, error) {
|
|
_, ok := d.store[id]
|
|
return ok, nil
|
|
}
|
|
func (d *dummyWorkRepo) BeginTx(_ context.Context) (*gorm.DB, error) { return nil, nil }
|
|
func (d *dummyWorkRepo) WithTx(_ context.Context, fn func(tx *gorm.DB) error) error {
|
|
return fn(nil)
|
|
}
|
|
func (d *dummyWorkRepo) FindByTitle(_ context.Context, title string) ([]domain.Work, error) {
|
|
return nil, nil
|
|
}
|
|
func (d *dummyWorkRepo) FindByAuthor(_ context.Context, authorID uint) ([]domain.Work, error) {
|
|
return nil, nil
|
|
}
|
|
func (d *dummyWorkRepo) FindByCategory(_ context.Context, categoryID uint) ([]domain.Work, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (d *dummyWorkRepo) GetWithTranslations(_ context.Context, id uint) (*domain.Work, error) {
|
|
return d.GetByID(context.Background(), id)
|
|
}
|
|
func (d *dummyWorkRepo) GetWithAssociations(_ context.Context, id uint) (*domain.Work, error) {
|
|
return d.GetByID(context.Background(), id)
|
|
}
|
|
func (d *dummyWorkRepo) GetWithAssociationsInTx(_ context.Context, tx *gorm.DB, id uint) (*domain.Work, error) {
|
|
return d.GetByID(context.Background(), id)
|
|
}
|
|
func (d *dummyWorkRepo) ListWithTranslations(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
|
// return a simple paginated result of all works
|
|
items := []domain.Work{}
|
|
for _, w := range d.store {
|
|
items = append(items, *w)
|
|
}
|
|
start := (page - 1) * pageSize
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
end := start + pageSize
|
|
if start >= len(items) {
|
|
return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}}, nil
|
|
}
|
|
if end > len(items) {
|
|
end = len(items)
|
|
}
|
|
return &domain.PaginatedResult[domain.Work]{Items: items[start:end], TotalCount: int64(len(items)), Page: page, PageSize: pageSize}, nil
|
|
}
|
|
|
|
func (d *dummyWorkRepo) FindByLanguage(_ context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
|
items := []domain.Work{}
|
|
for _, w := range d.store {
|
|
if w.Language == language {
|
|
items = append(items, *w)
|
|
}
|
|
}
|
|
start := (page - 1) * pageSize
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
end := start + pageSize
|
|
if start >= len(items) {
|
|
return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}}, nil
|
|
}
|
|
if end > len(items) {
|
|
end = len(items)
|
|
}
|
|
return &domain.PaginatedResult[domain.Work]{Items: items[start:end], TotalCount: int64(len(items)), Page: page, PageSize: pageSize}, nil
|
|
}
|
|
func (d *dummyWorkRepo) IsAuthor(_ context.Context, workID uint, authorID uint) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (d *dummyWorkRepo) ListByCollectionID(_ context.Context, collectionID uint) ([]domain.Work, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestCachedWork_GetByID_Caches(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := newDummyWorkRepo()
|
|
d.store[1] = &domain.Work{Title: "initial", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
|
|
mc := newMemCache()
|
|
cw := NewCachedWorkRepository(d, mc, nil)
|
|
|
|
w, err := cw.GetByID(ctx, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "initial", w.Title)
|
|
require.Equal(t, 1, d.calls)
|
|
|
|
// second read should hit cache and not increment inner calls
|
|
w2, err := cw.GetByID(ctx, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "initial", w2.Title)
|
|
require.Equal(t, 1, d.calls)
|
|
}
|
|
|
|
func TestCachedWork_Update_Invalidates(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := newDummyWorkRepo()
|
|
d.store[1] = &domain.Work{Title: "initial", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
|
|
mc := newMemCache()
|
|
cw := NewCachedWorkRepository(d, mc, nil)
|
|
|
|
_, err := cw.GetByID(ctx, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, d.calls)
|
|
|
|
// Update underlying entity
|
|
d.store[1] = &domain.Work{Title: "updated", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
|
|
err = cw.Update(ctx, d.store[1])
|
|
require.NoError(t, err)
|
|
|
|
// Next GetByID should call inner again (cache invalidated)
|
|
w, err := cw.GetByID(ctx, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "updated", w.Title)
|
|
require.Equal(t, 3, d.calls, "expected inner to be called again for update invalidation")
|
|
}
|
|
|
|
func TestCachedWork_Delete_Invalidates(t *testing.T) {
|
|
ctx := context.Background()
|
|
d := newDummyWorkRepo()
|
|
d.store[1] = &domain.Work{Title: "initial", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
|
|
mc := newMemCache()
|
|
cw := NewCachedWorkRepository(d, mc, nil)
|
|
|
|
_, err := cw.GetByID(ctx, 1)
|
|
require.NoError(t, err)
|
|
// delete
|
|
err = cw.Delete(ctx, 1)
|
|
require.NoError(t, err)
|
|
|
|
_, err = cw.GetByID(ctx, 1)
|
|
require.Error(t, err)
|
|
}
|