Files
zeekay 6a3066b08e Test apps open a migrated fixture, so core and apis fit the runner
tests/data lacked twelve migrations, and every test app ran them before
its test began: about four seconds an app, which put core and apis past
go test's ten minutes on the runner, and past thirty once the timeout
was raised. The fixture now holds them (go run ./tests/fixture writes
it), and TestTheFixtureIsMigrated fails, naming the migration, while
one is missing. On one host core went from 267 s to 26 s, apis from
432 s to 23 s, and the whole suite runs in 51 s.

The delete tests gave a background goroutine 100 ms to remove a
record's files, which a slow runner did not always meet. BaseApp
counts those deletions and WaitFileDeletes waits for them: the tests
wait on the count instead of the clock, and ResetBootstrapState waits
too, so a Base that stops no longer drops a deletion in flight.

TestSQLRun expected a CREATE TABLE to report one affected row. SQLite
answers a statement that writes no rows with the connection's last
count, and that row was the last migration's _migrations insert; the
handler now reads 0, the number it changed.

go test runs with its default timeout again, and crm_test.go is gofmt'd.
2026-10-19 09:08:39 -07:00

115 lines
4.4 KiB
Go

package engine
import (
"context"
"strings"
"testing"
"time"
"github.com/hanzoai/base/core"
"github.com/hanzoai/base/modules"
_ "github.com/hanzoai/base/modules/crm"
"github.com/hanzoai/base/tools/types"
)
// The CRM module on either engine: it installs, its composite values store and
// read back, a primary email stays unique whatever its case, a where clause and
// a part path filter alike, and a stage change is history.
func TestCRM(t *testing.T) {
app := newApp(t)
growth, err := modules.Install(app, "crm")
if err != nil {
t.Fatalf("install: %v", err)
}
if len(growth.Created) != 13 {
t.Fatalf("created %v", growth.Created)
}
if again, err := modules.Install(app, "crm"); err != nil || len(again.Created)+len(again.Grown) != 0 {
t.Fatalf("a second install changed something: %+v %v", again, err)
}
ctx := core.WithActor(context.Background(), types.Actor{Source: core.ActorManual, Id: "ann", Name: "Ann"})
save := func(coll string, data map[string]any) *core.Record {
t.Helper()
c, err := app.FindCollectionByNameOrId(coll)
if err != nil {
t.Fatal(err)
}
r := core.NewRecord(c)
for k, v := range data {
r.Set(k, v)
}
if err := app.SaveWithContext(ctx, r); err != nil {
t.Fatalf("save %s: %v", coll, err)
}
return r
}
acme := save("companies", map[string]any{"name": "Acme", "annual_revenue": types.Currency{AmountMicros: 9_000_000, CurrencyCode: "USD"}})
ada := save("people", map[string]any{
"name": types.FullName{FirstName: "Ada", LastName: "Lovelace"}, "company": acme.Id, "lifecycle": "lead",
"emails": types.Emails{PrimaryEmail: "[email protected]"}, "phones": types.Phones{PrimaryPhoneNumber: "+1 (415) 555-0100"},
})
save("people", map[string]any{"name": types.FullName{FirstName: "Grace"}, "lifecycle": "customer", "emails": types.Emails{PrimaryEmail: "[email protected]"}})
people, _ := app.FindCollectionByNameOrId("people")
dup := core.NewRecord(people)
dup.Set("emails", types.Emails{PrimaryEmail: "[email protected]"})
if err := app.Validate(dup); err == nil || !strings.Contains(err.Error(), "unique") {
t.Fatalf("a primary email another record holds was accepted: %v", err)
}
got, err := app.FindRecordById(people, ada.Id)
if err != nil {
t.Fatal(err)
}
if got.Get("emails").(types.Emails).PrimaryEmail != "[email protected]" || got.Get("phones").(types.Phones).PrimaryPhoneNumber != "+14155550100" {
t.Fatalf("values: %v %v", got.Get("emails"), got.Get("phones"))
}
if a := got.Get("created_by").(types.Actor); a.Id != "ann" {
t.Fatalf("created_by: %+v", a)
}
for name, s := range map[string]struct {
where string
want int
}{
"select any of": {`{"field":"lifecycle","op":"is","value":["lead"]}`, 1},
"composite contains": {`{"field":"emails","op":"contains","value":"globex"}`, 1},
"phone digits": {`{"field":"phones","op":"contains","value":"415 555"}`, 1},
"a relation's field": {`{"field":"company.name","op":"is","value":"Acme"}`, 1},
"not": {`{"not":{"field":"lifecycle","op":"is","value":"lead"}}`, 1},
"empty composite": {`{"field":"address","op":"empty"}`, 2},
"or": {`{"or":[{"field":"name.firstName","op":"is","value":"Ada"},{"field":"name.firstName","op":"is","value":"Grace"}]}`, 2},
"created today": {`{"field":"created","op":"today"}`, 2},
} {
filter, params, err := core.CompileWhere(app, people, nil, []byte(s.where), time.Now())
if err != nil {
t.Fatalf("%s: %v", name, err)
}
found, err := app.FindRecordsByFilter(people, string(filter), "", 0, 0, params)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if len(found) != s.want {
t.Errorf("%s: %d records, want %d", name, len(found), s.want)
}
}
found, err := app.FindRecordsByFilter("companies", "annual_revenue.amountMicros >= 1000000", "-annual_revenue.amountMicros", 0, 0)
if err != nil || len(found) != 1 {
t.Fatalf("a part filter: %d, %v", len(found), err)
}
opp := save("opportunities", map[string]any{"name": "deal", "stage": "new", "company": acme.Id})
opp.Set("stage", "meeting")
if err := app.SaveWithContext(ctx, opp); err != nil {
t.Fatal(err)
}
rows, err := app.FindRecordsByFilter(core.CollectionNameHistory, "record = {:id}", "created", 0, 0, map[string]any{"id": opp.Id})
if err != nil || len(rows) != 2 || rows[1].GetString("action") != "update" || !strings.Contains(rows[1].GetString("changes"), `"meeting"`) {
t.Fatalf("history: %d rows, %v", len(rows), err)
}
}