mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
227 lines
7.0 KiB
Go
227 lines
7.0 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tests"
|
|
"github.com/hanzoai/dbx"
|
|
|
|
_ "github.com/hanzoai/base/migrations"
|
|
)
|
|
|
|
func app(t *testing.T) core.App {
|
|
t.Helper()
|
|
built, err := tests.NewTestApp()
|
|
if err != nil {
|
|
t.Fatalf("test app: %v", err)
|
|
}
|
|
t.Cleanup(built.Cleanup)
|
|
return built
|
|
}
|
|
|
|
// thing is a Kind to exercise the convention with. It deliberately does not declare
|
|
// Org: Ensure adds it, and a Kind that could omit it would be a collection whose
|
|
// records belong to nobody.
|
|
var thing = Kind{
|
|
Name: "store_thing",
|
|
Fields: []core.Field{
|
|
&core.TextField{Name: "label", Required: true},
|
|
&core.DateField{Name: "at", Required: true},
|
|
},
|
|
Indexes: []Index{
|
|
{Name: "label", Fields: []string{Org, "label"}},
|
|
},
|
|
}
|
|
|
|
func TestEnsureAddsTheTenantAndTheIndexes(t *testing.T) {
|
|
built := app(t)
|
|
if err := thing.Ensure(built); err != nil {
|
|
t.Fatalf("Ensure: %v", err)
|
|
}
|
|
|
|
collection, err := built.FindCollectionByNameOrId(thing.Name)
|
|
if err != nil {
|
|
t.Fatalf("collection: %v", err)
|
|
}
|
|
if collection.Fields.GetByName(Org) == nil {
|
|
t.Error("the collection has no tenant field, so a record could belong to nobody")
|
|
}
|
|
if !indexed(collection.Indexes, thing.name(thing.Indexes[0])) {
|
|
t.Errorf("index missing: %v", collection.Indexes)
|
|
}
|
|
}
|
|
|
|
// TestEnsureIsIdempotentAndAdditive: Ensure runs on every start. A second run must
|
|
// change nothing, and a run of a declaration that has grown must add what is new
|
|
// without disturbing the records already there — a field the reader expects and the
|
|
// collection lacks reads as empty, which is indistinguishable from a record that
|
|
// never had it.
|
|
func TestEnsureIsIdempotentAndAdditive(t *testing.T) {
|
|
built := app(t)
|
|
if err := thing.Ensure(built); err != nil {
|
|
t.Fatalf("Ensure: %v", err)
|
|
}
|
|
|
|
row, err := thing.New(built, "acme")
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
row.Set("label", "before")
|
|
row.Set("at", time.Now().UTC())
|
|
if err := built.Save(row); err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
|
|
if err := thing.Ensure(built); err != nil {
|
|
t.Fatalf("second Ensure: %v", err)
|
|
}
|
|
|
|
grown := thing
|
|
grown.Fields = append(append([]core.Field{}, thing.Fields...), &core.TextField{Name: "note"})
|
|
grown.Indexes = append(append([]Index{}, thing.Indexes...), Index{Name: "at", Fields: []string{Org, "at"}})
|
|
if err := grown.Ensure(built); err != nil {
|
|
t.Fatalf("Ensure of a grown declaration: %v", err)
|
|
}
|
|
|
|
collection, err := built.FindCollectionByNameOrId(thing.Name)
|
|
if err != nil {
|
|
t.Fatalf("collection: %v", err)
|
|
}
|
|
if collection.Fields.GetByName("note") == nil {
|
|
t.Error("a field added to the declaration did not reach the collection")
|
|
}
|
|
if !indexed(collection.Indexes, grown.name(grown.Indexes[1])) {
|
|
t.Errorf("an index added to the declaration did not reach the collection: %v", collection.Indexes)
|
|
}
|
|
|
|
kept, err := grown.Find(built, "acme", "label = {:label}", "", 0, dbx.Params{"label": "before"})
|
|
if err != nil {
|
|
t.Fatalf("Find: %v", err)
|
|
}
|
|
if len(kept) != 1 {
|
|
t.Fatalf("the record written before the collection grew is gone: %d records", len(kept))
|
|
}
|
|
if kept[0].GetString("note") != "" {
|
|
t.Errorf("note = %q, want empty on a record written before the field existed", kept[0].GetString("note"))
|
|
}
|
|
}
|
|
|
|
// TestQueryIsScopedToOneOrg: the tenant predicate is added by Find and not by the
|
|
// caller's filter, because a filter that forgets it reads another tenant's records
|
|
// and nothing about the result says so.
|
|
func TestQueryIsScopedToOneOrg(t *testing.T) {
|
|
built := app(t)
|
|
if err := thing.Ensure(built); err != nil {
|
|
t.Fatalf("Ensure: %v", err)
|
|
}
|
|
for _, org := range []string{"acme", "beta"} {
|
|
row, err := thing.New(built, org)
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
row.Set("label", "shared")
|
|
row.Set("at", time.Now().UTC())
|
|
if err := built.Save(row); err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
}
|
|
|
|
mine, err := thing.Find(built, "acme", "label = {:label}", "", 0, dbx.Params{"label": "shared"})
|
|
if err != nil {
|
|
t.Fatalf("Find: %v", err)
|
|
}
|
|
if len(mine) != 1 || mine[0].GetString(Org) != "acme" {
|
|
t.Fatalf("a scoped query returned %d records: %v", len(mine), mine)
|
|
}
|
|
|
|
if _, err := thing.Find(built, "", "", "", 0, nil); err == nil {
|
|
t.Error("an unscoped query was answered")
|
|
}
|
|
if _, err := thing.New(built, ""); err == nil {
|
|
t.Error("a record with no organisation was built")
|
|
}
|
|
|
|
// Across is the deliberate exception, and the only one.
|
|
every, err := thing.Across(built, "", "", 0, nil)
|
|
if err != nil {
|
|
t.Fatalf("Across: %v", err)
|
|
}
|
|
if len(every) != 2 {
|
|
t.Errorf("Across saw %d records, want both orgs", len(every))
|
|
}
|
|
|
|
held, err := thing.Count(built)
|
|
if err != nil {
|
|
t.Fatalf("Count: %v", err)
|
|
}
|
|
if held != 2 {
|
|
t.Errorf("Count = %d, want both orgs", held)
|
|
}
|
|
}
|
|
|
|
// TestAMomentComparesAsAMoment: a date is stored in one layout and compared as
|
|
// text, so a filter parameter that is a Go time has to be converted before it is
|
|
// bound. Unconverted it does not error — it answers with the wrong rows, and the
|
|
// direction it is wrong in depends on the layout rather than on the data.
|
|
func TestAMomentComparesAsAMoment(t *testing.T) {
|
|
built := app(t)
|
|
if err := thing.Ensure(built); err != nil {
|
|
t.Fatalf("Ensure: %v", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
for label, at := range map[string]time.Time{
|
|
"old": now.Add(-90 * time.Minute),
|
|
"recent": now.Add(-30 * time.Minute),
|
|
"later": now.Add(30 * time.Minute),
|
|
} {
|
|
row, err := thing.New(built, "acme")
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
row.Set("label", label)
|
|
row.Set("at", at)
|
|
if err := built.Save(row); err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
}
|
|
|
|
since := now.Add(-time.Hour)
|
|
within, err := thing.Find(built, "acme", "at >= {:since} && at <= {:now}", "at", 0, dbx.Params{"since": since, "now": now})
|
|
if err != nil {
|
|
t.Fatalf("Find: %v", err)
|
|
}
|
|
if len(within) != 1 || within[0].GetString("label") != "recent" {
|
|
labels := make([]string, 0, len(within))
|
|
for _, r := range within {
|
|
labels = append(labels, r.GetString("label"))
|
|
}
|
|
t.Fatalf("the window matched %v, want [recent]", labels)
|
|
}
|
|
|
|
// The moment a stored record already carries is the one a walk pages by, so it
|
|
// is the comparison that has to be exact rather than approximately right. A
|
|
// layout that differs only after the seconds compares as greater than the value
|
|
// it means, and the record that the cursor just passed comes back again.
|
|
last, err := thing.Find(built, "acme", "", "-at", 1, nil)
|
|
if err != nil {
|
|
t.Fatalf("Find: %v", err)
|
|
}
|
|
if len(last) != 1 || last[0].GetString("label") != "later" {
|
|
t.Fatalf("the newest record is %v, want later", last)
|
|
}
|
|
at := last[0].GetDateTime("at").Time()
|
|
if past, err := thing.Find(built, "acme", "at > {:at}", "at", 0, dbx.Params{"at": at}); err != nil {
|
|
t.Fatalf("Find: %v", err)
|
|
} else if len(past) != 0 {
|
|
t.Errorf("%d records are after the newest one", len(past))
|
|
}
|
|
if including, err := thing.Find(built, "acme", "at >= {:at}", "at", 0, dbx.Params{"at": at}); err != nil {
|
|
t.Fatalf("Find: %v", err)
|
|
} else if len(including) != 1 {
|
|
t.Errorf("the newest record is not at its own moment: %d records", len(including))
|
|
}
|
|
}
|