package test import ( "RenewCMS/api/pkg/apperr" "net/http" "strconv" "sync" "testing" ) const concurrentSaveRounds = 20 func TestArticleConcurrentSavesHTTP(t *testing.T) { t.Run("Two saves of the same article with the same if_revision: one wins, the other conflicts", func(t *testing.T) { for round := 0; round < concurrentSaveRounds; round++ { w := newStoryWorld(t) revision, _ := revisionsOf(w, 10) bodies := []string{"

Mine

", "

Theirs

"} responses := make([]storyResponse, len(bodies)) start := make(chan struct{}) var saves sync.WaitGroup for i, body := range bodies { saves.Add(1) go func() { defer saves.Done() <-start responses[i] = w.send(storyUpdater, http.MethodPatch, "/v1/article/10", `{"body":"`+body+`","if_revision":`+strconv.FormatUint(uint64(revision), 10)+`}`) }() } close(start) saves.Wait() winners, losers := 0, 0 var winningBody string for i, response := range responses { switch response.Code { case http.StatusOK: winners++ winningBody = bodies[i] case http.StatusConflict: losers++ if response.text() != apperr.RevisionConflictErrorMessage+"\n" { t.Fatalf("conflict body %q", response.text()) } if response.header(apperr.CurrentRevisionHeader) != strconv.FormatUint(uint64(revision+1), 10) { t.Fatalf("current revision header %q", response.header(apperr.CurrentRevisionHeader)) } default: t.Fatalf("unexpected %d %q", response.Code, response.text()) } } if winners != 1 || losers != 1 { t.Fatalf("round %d: %d saved, %d conflicted", round, winners, losers) } if stored, _ := revisionsOf(w, 10); stored != revision+1 || w.stored(10).Body != winningBody { t.Fatalf("round %d: stored revision %d body %q, the winner sent %q", round, stored, w.stored(10).Body, winningBody) } } }) }