Files
zeekay 238dd248ed Read the platform model, do not ship it
The model was compiled in with go:embed, so every clone and every binary
carried a 1.9MB artifact this repository does not own, under a licence that
needed its own carve-out in LICENSE, its own section in NOTICE, and a
LICENSE-EPL beside them.

It is now read at start from model.json in the data directory. A deployment
that has the artifact supplies it; this repository redistributes nothing.
Absent is not an error -- loadModel returns nil for a file that is not there
and buildHierarchy already answered an empty hierarchy for input it cannot
parse -- so a server without one starts and serves an empty model rather
than refusing to boot over a file it cannot carry.

The model moves onto the server, which is where a value read at Register
belongs; sessions reach it through the server they already hold. Tests carry
their own small model, the ten classes they exercise, hand-written: they
never depended on its contents, only that the model served is the model
held.

LICENSE is MIT OR Apache-2.0 with no exception. NOTICE lists dependencies
only.
2026-09-01 00:48:49 -07:00

143 lines
5.6 KiB
Go

package transactor
import (
"encoding/json"
"testing"
)
// newTestSession wires a session onto a fresh per-workspace store + the real
// embedded model hierarchy.
func newTestSession(t *testing.T) *session {
t.Helper()
return &session{
server: &server{hub: newHub(), model: testModel, modelHash: hashModel(testModel)},
store: newStore(t.TempDir()),
hier: buildHierarchy(testModel),
org: "test-org",
workspace: "ws-test",
account: "acc-test",
}
}
func TestPersistenceCRUD(t *testing.T) {
s := newTestSession(t)
// create a tracker Project
s.applyTx(json.RawMessage(`{"_class":"core:class:TxCreateDoc","objectId":"proj1",
"objectClass":"tracker:class:Project","objectSpace":"core:space:Space",
"modifiedBy":"acc-test","modifiedOn":1,
"attributes":{"name":"My Project","identifier":"TSK","sequence":0}}`))
if docs := s.queryDocs("tracker:class:Project", nil); len(docs) != 1 {
t.Fatalf("create: want 1 project, got %d", len(docs))
} else if docs[0]["name"] != "My Project" {
t.Fatalf("create: name=%v", docs[0]["name"])
}
// hierarchy: a Project IS a Space IS a Doc — findAll by ancestor must find it
if docs := s.queryDocs("core:class:Space", nil); len(docs) != 1 {
t.Fatalf("hierarchy(Space): want 1, got %d", len(docs))
}
if docs := s.queryDocs("core:class:Doc", map[string]any{"_id": "proj1"}); len(docs) != 1 {
t.Fatalf("hierarchy(Doc) by _id: want 1, got %d", len(docs))
}
// query by field + predicate
if docs := s.queryDocs("tracker:class:Project", map[string]any{"identifier": "TSK"}); len(docs) != 1 {
t.Fatalf("query identifier: want 1, got %d", len(docs))
}
if docs := s.queryDocs("tracker:class:Project", map[string]any{"sequence": map[string]any{"$gte": float64(0)}}); len(docs) != 1 {
t.Fatalf("query $gte: want 1, got %d", len(docs))
}
// update — operator + plain set
s.applyTx(json.RawMessage(`{"_class":"core:class:TxUpdateDoc","objectId":"proj1",
"objectClass":"tracker:class:Project","modifiedBy":"acc-test","modifiedOn":2,
"operations":{"name":"Renamed","$inc":{"sequence":1}}}`))
docs := s.queryDocs("tracker:class:Project", nil)
if docs[0]["name"] != "Renamed" {
t.Fatalf("update set: name=%v", docs[0]["name"])
}
if seq, _ := asFloat(docs[0]["sequence"]); seq != 1 {
t.Fatalf("update $inc: sequence=%v", docs[0]["sequence"])
}
// persistence across a fresh store handle (simulates reconnect/reload)
s2 := &session{server: s.server, store: newStore(s.store.dir), hier: s.hier, org: s.org, workspace: s.workspace, account: s.account}
if docs := s2.queryDocs("tracker:class:Project", nil); len(docs) != 1 {
t.Fatalf("reload: want 1 persisted project, got %d", len(docs))
}
// remove
s.applyTx(json.RawMessage(`{"_class":"core:class:TxRemoveDoc","objectId":"proj1",
"objectClass":"tracker:class:Project","modifiedBy":"acc-test","modifiedOn":3}`))
if docs := s.queryDocs("tracker:class:Project", nil); len(docs) != 0 {
t.Fatalf("remove: want 0, got %d", len(docs))
}
}
func TestMixinAndTrigger(t *testing.T) {
s := newTestSession(t)
// create a Person, then apply the Employee mixin (active) — the trigger must
// materialize a PersonSpace (fixes "Failed to find space for employee").
s.applyTx(json.RawMessage(`{"_class":"core:class:TxCreateDoc","objectId":"person1",
"objectClass":"contact:class:Person","objectSpace":"contact:space:Contacts",
"modifiedBy":"acc-test","modifiedOn":1,"attributes":{"name":"Z","city":""}}`))
s.applyTx(json.RawMessage(`{"_class":"core:class:TxMixin","objectId":"person1",
"objectClass":"contact:class:Person","mixin":"contact:mixin:Employee",
"objectSpace":"contact:space:Contacts","modifiedBy":"acc-test","modifiedOn":2,
"attributes":{"active":true,"role":"USER"}}`))
// mixin data stored under the mixin key; findAll(Employee mixin) filters by it
emps := s.queryDocs("contact:mixin:Employee", nil)
if len(emps) != 1 {
t.Fatalf("mixin query: want 1 employee, got %d", len(emps))
}
if sub, ok := emps[0]["contact:mixin:Employee"].(map[string]any); !ok || sub["active"] != true {
t.Fatalf("mixin payload missing/inactive: %v", emps[0]["contact:mixin:Employee"])
}
// trigger: a PersonSpace for person1 must now exist
sp := s.queryDocs("contact:class:PersonSpace", map[string]any{"person": "person1"})
if len(sp) != 1 {
t.Fatalf("trigger: want 1 PersonSpace, got %d", len(sp))
}
}
func TestSeedSpaces(t *testing.T) {
s := newTestSession(t)
s.seedWorkspace()
if sp := s.queryDocs("core:class:Space", map[string]any{"_id": "contact:space:Contacts"}); len(sp) != 1 {
t.Fatalf("seed: Contacts space not found")
}
// idempotent
s.seedWorkspace()
if n, _ := s.store.count(s.org, s.workspace); n != len(systemSpaces()) {
t.Fatalf("seed not idempotent: count=%d", n)
}
}
// TestTenantIsolation proves two orgs sharing a workspace id never see each
// other's data — the whole point of per-(org,workspace) SQLite files.
func TestTenantIsolation(t *testing.T) {
st := newStore(t.TempDir())
hier := buildHierarchy(testModel)
mk := func(org string) *session {
return &session{server: &server{hub: newHub()}, store: st, hier: hier, org: org, workspace: "shared-ws", account: "u"}
}
a := mk("org-a")
b := mk("org-b")
a.applyTx(json.RawMessage(`{"_class":"core:class:TxCreateDoc","objectId":"p1",
"objectClass":"tracker:class:Project","objectSpace":"core:space:Space",
"modifiedBy":"u","modifiedOn":1,"attributes":{"name":"A only"}}`))
if got := a.queryDocs("tracker:class:Project", nil); len(got) != 1 {
t.Fatalf("org-a should see its project, got %d", len(got))
}
if got := b.queryDocs("tracker:class:Project", nil); len(got) != 0 {
t.Fatalf("org-b must NOT see org-a's project (tenant leak), got %d", len(got))
}
}