Files
RenewCMS/test/story_lock_stability_test.go
T

102 lines
3.6 KiB
Go

package test
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/transaction"
"RenewCMS/internal/infrastructure/useCases"
"context"
"errors"
"slices"
"testing"
)
const storyStudio uint32 = 3
type shiftingStoryRepo struct {
*mockArticleRepo
listings int
shiftFrom int
shift func()
}
func (r *shiftingStoryRepo) ListByStory(storyID uint32) ([]article.Article, error) {
r.listings++
if r.listings >= r.shiftFrom {
r.shift()
}
return r.mockArticleRepo.ListByStory(storyID)
}
type repositoriesTransactor struct {
repositories transaction.Repositories
}
func (r repositoriesTransactor) Run(ctx context.Context, work func(transaction.Repositories) error) error {
return work(r.repositories)
}
func TestMoveStoryRelocksUntilTheSiteSetIsStable(t *testing.T) {
t.Run("A version that appears on another site after the first lock gets that site locked too", func(t *testing.T) {
w := newStoryWorld(t)
w.sites.seed(storyStudio, "Studio", "studio", false)
listing := &shiftingStoryRepo{mockArticleRepo: w.articles, shiftFrom: 2}
listing.shift = func() {
shifted := w.articles.articles[13]
shifted.SiteID = storyStudio
w.articles.articles[13] = shifted
}
mover := useCases.NewMoveStoryUseCase(repositoriesTransactor{transaction.Repositories{Articles: listing, Sites: w.sites, Authors: w.authors, Localization: w.rows}}, listing, discardRecorder)
_, _ = mover.MoveStory(context.Background(), useCases.MoveStoryCommand{StoryID: storyStory, SiteKey: "atelier", Caller: ownerCaller(storyOwner)})
if !slices.Contains(w.sites.lockedSites, storyStudio) {
t.Errorf("site %d must be locked, locked %v", storyStudio, w.sites.lockedSites)
}
})
t.Run("A set that never settles is refused as lock contention", func(t *testing.T) {
w := newStoryWorld(t)
listing := &shiftingStoryRepo{mockArticleRepo: w.articles, shiftFrom: 2}
listing.shift = func() {
shifted := w.articles.articles[13]
shifted.SiteID = 100 + uint32(listing.listings)
w.articles.articles[13] = shifted
}
mover := useCases.NewMoveStoryUseCase(repositoriesTransactor{transaction.Repositories{Articles: listing, Sites: w.sites, Authors: w.authors, Localization: w.rows}}, listing, discardRecorder)
_, err := mover.MoveStory(context.Background(), useCases.MoveStoryCommand{StoryID: storyStory, SiteKey: "atelier", Caller: ownerCaller(storyOwner)})
if !errors.Is(err, transaction.ErrLockContention) {
t.Errorf("expected lock contention, got %v", err)
}
if listing.listings != 7 {
t.Errorf("expected two listings plus five relock rounds, got %d listings", listing.listings)
}
})
}
func TestDeleteStoryRelocksUntilTheSiteSetIsStable(t *testing.T) {
t.Run("A set that never settles is refused as lock contention after the last round", func(t *testing.T) {
w := newStoryWorld(t)
listing := &shiftingStoryRepo{mockArticleRepo: w.articles, shiftFrom: 1}
listing.shift = func() {
shifted := w.articles.articles[13]
shifted.SiteID = 100 + uint32(listing.listings)
w.articles.articles[13] = shifted
}
deleter := useCases.NewDeleteStoryUseCase(repositoriesTransactor{transaction.Repositories{Articles: listing, Sites: w.sites}}, listing, discardRecorder)
err := deleter.DeleteStory(context.Background(), useCases.DeleteStoryCommand{StoryID: storyStory, Caller: ownerCaller(storyOwner)})
if !errors.Is(err, transaction.ErrLockContention) {
t.Errorf("expected lock contention, got %v", err)
}
if listing.listings != 6 {
t.Errorf("expected five lock rounds plus the refusal lookup, got %d listings", listing.listings)
}
if _, kept := w.articles.articles[13]; !kept {
t.Error("nothing may be deleted")
}
})
}