package ledger import ( "encoding/json" "fmt" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/buscalls" ) var owner = buscalls.Author{Account: "ledger.anchor.m"} // 1966/AC13, 1966/AC14 and the checks of an operation against its type (ADR 0597 §2, §5, §6, §8; design 25 § 12.4). func TestApplyAppendsDerivesAndSnapshots(t *testing.T) { cases := []struct { name string run func(t *testing.T, l *Ledger, f fakes) }{ {"1966/AC13 an instance at sequence 4 is taken with 5, its state and snapshot the effect's result", func(t *testing.T, l *Ledger, f fakes) { for i := 1; i <= 4; i++ { applied(t, l, "decision/0290", "note", fmt.Sprintf(`{"says":"n%d"}`, i), owner) } if last, _ := f.history.Last("decision.0290"); last != 4 { t.Fatalf("the instance is at %d, want 4", last) } got := applied(t, l, "decision/0290", "title", `{"title":"A ledger","ignored":"x"}`, owner) if !got.Taken || got.Sequence != 5 || got.StateBehind { t.Fatalf("answered %+v, want taken with 5", got) } raw, _, _ := f.state.Get("decision.0290") state := record(t, raw) want := `{"author":{"account":"ledger.anchor.m"},"notes":[{"says":"n1"},{"says":"n2"},{"says":"n3"},{"says":"n4"}],"sequence":5,"title":"A ledger"}` if state.Sequence != 5 || string(state.State) != want { t.Fatalf("the state is %d %s, want 5 %s", state.Sequence, state.State, want) } snap, _, _ := f.snapshots.Get("decision.0290") if string(snap) != string(raw) { t.Fatalf("the snapshot is %s, the state %s", snap, raw) } }}, {"1966/AC14 a full state bucket answers taken and state behind, and the next apply after room catches up", func(t *testing.T, l *Ledger, f fakes) { applied(t, l, "decision/0290", "note", `{"says":"first"}`, owner) f.state.Full = true got := applied(t, l, "decision/0290", "note", `{"says":"while full"}`, owner) if !got.Taken || got.Sequence != 2 || !got.StateBehind { t.Fatalf("answered %+v, want taken with 2 and state behind", got) } if raw, _, _ := f.state.Get("decision.0290"); record(t, raw).Sequence != 1 { t.Fatalf("the full state moved: %s", raw) } f.state.Full = false got = applied(t, l, "decision/0290", "note", `{"says":"after room"}`, owner) raw, _, _ := f.state.Get("decision.0290") state := record(t, raw) if got.StateBehind || state.Sequence != 3 || string(state.State) != `{"notes":[{"says":"first"},{"says":"while full"},{"says":"after room"}]}` { t.Fatalf("after room: %+v, the state %d %s", got, state.Sequence, state.State) } }}, {"each effect: set an entry, remove it, remove the instance", func(t *testing.T, l *Ledger, f fakes) { text := "the section's text" applied(t, l, "decision/1", "section", fmt.Sprintf(`{"id":"a","text":%q,"digest":%q}`, text, Digest(text)), owner) applied(t, l, "decision/1", "section", fmt.Sprintf(`{"id":"b","text":"b","digest":%q}`, Digest("b")), owner) applied(t, l, "decision/1", "drop-section", `{"id":"a"}`, owner) raw, _, _ := f.state.Get("decision.1") if got := string(record(t, raw).State); got != fmt.Sprintf(`{"sections":{"b":{"digest":%q}}}`, Digest("b")) { t.Fatalf("the state is %s", got) } applied(t, l, "decision/1", "withdraw", `{}`, owner) raw, _, _ = f.state.Get("decision.1") if got := record(t, raw); got.Sequence != 4 || string(got.State) != "null" { t.Fatalf("the removed instance is %d %s", got.Sequence, got.State) } }}, {"an operation outside its type, a body off its schema and a digest off its text are refused", func(t *testing.T, l *Ledger, f fakes) { for _, c := range []struct{ instance, operation, body, refusal string }{ {"listing/a17", "title", `{}`, `no type "listing"`}, {"decision/0290", "accept", `{}`, `no operation "accept"`}, {"decision/0290", "title", `{"title":7}`, "field title is a string"}, {"decision/0290", "section", `{"id":"a","text":"x","digest":"sha256:00"}`, "is not the digest of its text"}, {"decision/0290", "section", `{"text":"x"}`, "is not the digest"}, {"decision/0290", "drop-section", `{}`, "names its entry by its field id"}, {"decision", "title", `{}`, "is not an instance"}, {"decision/a.b", "title", `{}`, "is not an instance"}, } { _, err := l.Apply(buscalls.ApplyRequest{Instance: c.instance, Operation: c.operation, Body: json.RawMessage(c.body)}, owner) if err == nil || !strings.Contains(err.Error(), c.refusal) { t.Errorf("%s %s %s: %v, want %q", c.instance, c.operation, c.body, err, c.refusal) } } if last, _ := f.history.Last("decision.0290"); last != 0 { t.Fatalf("a refused operation was appended: %d", last) } }}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { l, f := newLedger() c.run(t, l, f) }) } // design 47 G §587: a ledger's fill is its history's against the ledger's cap, a store that cannot say it is an error naming // the ledger; a removal takes an instance's operations, snapshot, state and index entries and answers what that was, an // unknown instance answers none, and nothing of another instance goes. t.Run("TheLedgerSaysItsFillAndRemovesAnInstanceWhole", func(t *testing.T) { l, f := newLedger() f.history.MaxBytes = 1 << 20 for _, n := range []string{"a", "b"} { applied(t, l, "decision/0290", "note", `{"says":"`+n+`"}`, owner) } applied(t, l, "decision/0291", "note", `{"says":"other"}`, owner) entry, _ := json.Marshal(buscalls.IndexEntry{Instance: "decision/0290", Sequence: 2}) other, _ := json.Marshal(buscalls.IndexEntry{Instance: "decision/0291", Sequence: 1}) _ = f.index.Put("t.decision.0290", entry) _ = f.index.Put("t.decision.0291", other) fill, err := l.Fill(buscalls.LedgerRequest{Ledger: "records"}) if err != nil || fill.Count != 3 || fill.Bytes == 0 || fill.MaxBytes != 1<<20 { t.Fatalf("the fill is %+v (%v), want 3 messages against 1 MiB", fill, err) } got, err := l.Remove(buscalls.LedgerRemoveRequest{Ledger: "records", Instance: "decision/0290"}) if err != nil || got.Instance != "decision/0290" || got.Operations != 2 || got.Bytes == 0 { t.Fatalf("the removal answered %+v (%v), want 2 operations and their bytes", got, err) } if kept, _ := f.history.Read("decision.0290", 1, 0); len(kept) != 0 { t.Errorf("operations are left: %d", len(kept)) } if raw, _, _ := f.state.Get("decision.0290"); raw != nil { t.Errorf("the state is left: %s", raw) } if raw, _, _ := f.snapshots.Get("decision.0290"); raw != nil { t.Errorf("the snapshot is left: %s", raw) } if held, _ := f.index.Entries("", 0); len(held) != 1 || held[0].Key != "t.decision.0291" { t.Errorf("the index holds %v, want the other instance's entry alone", held) } if kept, _ := f.history.Read("decision.0291", 1, 0); len(kept) != 1 { t.Errorf("another instance lost operations: %d left", len(kept)) } if again, err := l.Remove(buscalls.LedgerRemoveRequest{Ledger: "records", Instance: "decision/0290"}); err != nil || again.Operations != 0 || again.Bytes != 0 { t.Errorf("a removal said twice answered %+v (%v), want none", again, err) } f.history.FillFails = true if _, err := l.Fill(buscalls.LedgerRequest{}); err == nil || !strings.Contains(err.Error(), "ledger records cannot say the fill") { t.Errorf("a history that cannot say its fill answered %v", err) } }) }