124 lines
5.2 KiB
Go
124 lines
5.2 KiB
Go
package pgstore
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"textmachine/platform/internal/money"
|
|
)
|
|
|
|
// The rename is scoped by OWNER and moves the revision, which are the two properties a client and a
|
|
// stranger respectively depend on.
|
|
//
|
|
// Mutation caught: dropping `owner_id` from the WHERE (anyone renames anyone's book); leaving the
|
|
// revision alone (a client obeying the contract drops the read that carried the new name, so the
|
|
// screen never shows it); reading the card outside the transaction (the card comes back labelled with
|
|
// a number older than the figures on it).
|
|
func TestRenamingABookIsOwnerScopedAndMovesTheRevision(t *testing.T) {
|
|
s, ctx := testDB(t)
|
|
seedUser(t, s, ctx, "u_owner")
|
|
seedUser(t, s, ctx, "u_stranger")
|
|
seedBook(t, s, ctx, "bk_1", "u_owner", 10)
|
|
|
|
beforeBook, _, err := s.GetBook(ctx, "u_owner", "bk_1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, _, err := s.RenameBook(ctx, "u_owner", "bk_1", "Мастер Гу")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if after.Title != "Мастер Гу" {
|
|
t.Errorf("the card came back with %q", after.Title)
|
|
}
|
|
if after.Revision <= beforeBook.Revision {
|
|
t.Errorf("revision %d did not rise above %d: a conforming client drops this read",
|
|
after.Revision, beforeBook.Revision)
|
|
}
|
|
// The store, not just the answer.
|
|
reread, _, err := s.GetBook(ctx, "u_owner", "bk_1")
|
|
if err != nil || reread.Title != "Мастер Гу" || reread.Revision != after.Revision {
|
|
t.Errorf("the rename did not land: %+v (%v)", reread, err)
|
|
}
|
|
|
|
// ⚠ AND THE PART THAT SEPARATES THE TWO FORMULAS. On a fresh book the account floor and the
|
|
// book's own counter are both 0, so `greatest(max, floor) + 1` and a plain `revision + 1` give
|
|
// the same answer and the test proves nothing about which one is written. Push the ACCOUNT's
|
|
// floor above this book's counter — the state a cancelled upload leaves — and only the correct
|
|
// formula clears it. A client obeying the contract drops any read not ABOVE what it applied, so
|
|
// the wrong one here is a rename the screen never shows.
|
|
exec(t, s, ctx, `update users set library_revision = $1 where id = 'u_owner'`, after.Revision+500)
|
|
cleared, _, err := s.RenameBook(ctx, "u_owner", "bk_1", "Мастер Гу, второе имя")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cleared.Revision <= after.Revision+500 {
|
|
t.Errorf("revision %d did not clear the account's floor %d: a plain `revision + 1` would look exactly like this",
|
|
cleared.Revision, after.Revision+500)
|
|
}
|
|
|
|
// A stranger gets the same answer as for a book that is not there, and writes nothing.
|
|
if _, _, err := s.RenameBook(ctx, "u_stranger", "bk_1", "mine now"); !errors.Is(err, ErrNoBook) {
|
|
t.Errorf("a stranger's rename answered %v, want ErrNoBook", err)
|
|
}
|
|
if _, _, err := s.RenameBook(ctx, "u_owner", "bk_missing", "x"); !errors.Is(err, ErrNoBook) {
|
|
t.Errorf("a missing book answered %v, want ErrNoBook", err)
|
|
}
|
|
still, _, err := s.GetBook(ctx, "u_owner", "bk_1")
|
|
if err != nil || still.Title != "Мастер Гу, второе имя" {
|
|
t.Errorf("a refused rename changed the book anyway: %+v (%v)", still, err)
|
|
}
|
|
}
|
|
|
|
// ⚠ A rename is accepted while a run is LIVE, which the canon states outright («Accepted while a run
|
|
// is live: a rename touches nothing a run reads»). Pinned because the tempting defensive move — a
|
|
// 409 like `deleteBook` has — would be wrong: nothing a run reads is in this row, and a user waiting
|
|
// hours for a translation is exactly the user who notices the name is wrong.
|
|
func TestARenameIsAcceptedWhileARunIsLive(t *testing.T) {
|
|
s, ctx := testDB(t)
|
|
now := fundedAccount(t, s, ctx, "u_owner", "25")
|
|
seedBook(t, s, ctx, "bk_1", "u_owner", 10)
|
|
if _, err := s.StartRun(ctx, StartRunInput{UserID: "u_owner", BookID: "bk_1", OrderedChapters: 2,
|
|
Ceiling: money.MicroUSD(1_000_000), Now: now}, 0, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, run, err := s.RenameBook(ctx, "u_owner", "bk_1", "Переименовано на ходу")
|
|
if err != nil {
|
|
t.Fatalf("a rename during a live run was refused: %v", err)
|
|
}
|
|
if after.Title != "Переименовано на ходу" {
|
|
t.Errorf("title: %q", after.Title)
|
|
}
|
|
// The run comes back beside the card and carries the BOOK's revision, like every other path that
|
|
// hands one out (§Revision: one counter per book).
|
|
if run == nil {
|
|
t.Fatal("the live run vanished from the answer")
|
|
}
|
|
if run.Revision != after.Revision {
|
|
t.Errorf("the run carries revision %d and the card %d", run.Revision, after.Revision)
|
|
}
|
|
}
|
|
|
|
// The backup listing is EVERY book with its directory, in a stable order. A filter here — on status,
|
|
// on a run having happened — is how a book somebody paid for comes to be left out of the copy.
|
|
func TestTheBackupListingHoldsEveryBookAndItsDirectory(t *testing.T) {
|
|
s, ctx := testDB(t)
|
|
seedUser(t, s, ctx, "u_owner")
|
|
seedBook(t, s, ctx, "bk_1", "u_owner", 10)
|
|
seedBook(t, s, ctx, "bk_2", "u_owner", 3)
|
|
exec(t, s, ctx, `update books set status = 'rejected' where id = 'bk_2'`)
|
|
|
|
got, err := s.BooksForBackup(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("the listing holds %d books, want both — including the rejected one, whose "+
|
|
"directory may still be there: %+v", len(got), got)
|
|
}
|
|
for _, b := range got {
|
|
if b.Workdir == "" {
|
|
t.Errorf("%s has no directory, so the backup would not know where to look", b.ID)
|
|
}
|
|
}
|
|
}
|