fix(realtime): доставлять обложку альбома после готовности вариантов
Карточка альбома не получала ссылку на обложку: она появляется в объекте альбома только после генерации thumb/medium, а событие об этом шаге в персональные потоки не публиковалось — счётчик обновлялся, обложка нет. - albums.Service.NotifyCoverReady публикует album.updated получателям, когда варианты готовы у фото, являющегося текущей обложкой (иначе no-op, чтобы массовая загрузка не рассылала событие на каждый файл) - photos: порт AlbumAccess расширен, worker вызывает публикацию после photo.updated - project.md §11/§12: триггер добавлен в контракт персонального потока
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -52,6 +53,35 @@ func (stubAlbums) Access(ctx context.Context, albumID, userID string) (*models.A
|
||||
|
||||
func (stubAlbums) NotifyAlbumChanged(context.Context, string) {}
|
||||
|
||||
func (stubAlbums) NotifyCoverReady(context.Context, string, string) {}
|
||||
|
||||
// recordingAlbums records cover-readiness announcements so worker tests can
|
||||
// assert the worker reports them.
|
||||
type recordingAlbums struct {
|
||||
stubAlbums
|
||||
|
||||
mu sync.Mutex
|
||||
noticed map[string]struct{}
|
||||
}
|
||||
|
||||
func newRecordingAlbums() *recordingAlbums {
|
||||
return &recordingAlbums{noticed: map[string]struct{}{}}
|
||||
}
|
||||
|
||||
func (r *recordingAlbums) NotifyCoverReady(_ context.Context, albumID, photoID string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.noticed[albumID+"|"+photoID] = struct{}{}
|
||||
}
|
||||
|
||||
// coverReady reports whether the given album/photo pair was announced.
|
||||
func (r *recordingAlbums) coverReady(albumID, photoID string) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
_, ok := r.noticed[albumID+"|"+photoID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func createUser(t *testing.T, ctx context.Context, pool *pgxpool.Pool) string {
|
||||
t.Helper()
|
||||
id := auth.NewID()
|
||||
@@ -126,19 +156,22 @@ func newTestService(t *testing.T, pool *pgxpool.Pool) (*Service, storage.Backend
|
||||
return NewService(NewStore(pool), stubAlbums{}, cl, sse.NewHub(1)), cl
|
||||
}
|
||||
|
||||
// newTestServiceFS builds a service backed by a temporary local file-system
|
||||
// storage backend so the full flow can run without external infrastructure.
|
||||
func newTestServiceFS(t *testing.T, pool *pgxpool.Pool) (*Service, storage.Backend) {
|
||||
// newRecordingServiceFS builds a service backed by a temporary local file-system
|
||||
// storage backend (so the full flow runs without external infrastructure) with an
|
||||
// album port that records cover-readiness announcements, for the worker's
|
||||
// publication assertions.
|
||||
func newRecordingServiceFS(t *testing.T, pool *pgxpool.Pool) (*Service, storage.Backend, *recordingAlbums) {
|
||||
t.Helper()
|
||||
signer, err := storage.NewSigner("test-secret", time.Hour, 10*time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("storage init: %v", err)
|
||||
t.Fatalf("signer init: %v", err)
|
||||
}
|
||||
fs, err := storage.NewFS(t.TempDir(), signer)
|
||||
if err != nil {
|
||||
t.Fatalf("storage init: %v", err)
|
||||
}
|
||||
return NewService(NewStore(pool), stubAlbums{}, fs, sse.NewHub(1)), fs
|
||||
rec := newRecordingAlbums()
|
||||
return NewService(NewStore(pool), rec, fs, sse.NewHub(1)), fs, rec
|
||||
}
|
||||
|
||||
// cleanupPhoto removes a photo, its objects (rows and blobs) and bindings.
|
||||
|
||||
Reference in New Issue
Block a user