Files
base/migrations/pr22_boot_verify_test.go
002d546c94 feat(crm): email + calendar sync data model (connected accounts) (#22)
* feat(crm): email + calendar sync data model (connected accounts)

Phase 0 of email/calendar CRM sync: the 10-collection data model as a schema
migration (same proven pattern as the saved-views collections).

- connectedAccount — OAuth provider + handle + scopes + ENCRYPTED tokens
  (accessTokenEnc/refreshTokenEnc ciphertext only; authFailedAt drives reconnect).
- messaging: messageChannel (sync state machine) / messageThread / message
  (dedup by headerMessageId) / messageParticipant (handle=email → person/user) /
  messageAssociation (per-channel external id + thread id + dedup).
- calendar: calendarChannel / calendarEvent / calendarEventParticipant /
  calendarEventAssociation (recurringEventExternalId links series instances).

Plugin-owned + user-readable: CreateRule/UpdateRule/DeleteRule = nil (only the
sync plugin/superuser writes), owner-scoped List/View so the console shows a
user's own mail/events. syncStateFields = the channel state machine
(status/stage/cursor/throttle) the two-phase list→import engine drives.

Relations are forward-compatible text refs (refField) so the schema applies
before the person/company object graph exists; a later migration upgrades them.

Next phases (plugins): plugins/connect (OAuth + token refresh + email→record
matcher), plugins/messaging + plugins/calendar (Google drivers + the two-phase
cron sync engine). Compiles clean; CI runs the full migration-boot test.

* test(migrations): prove the connect/messaging/calendar model boots

Boots a TestApp (which runs SystemMigrations) and asserts all ten collections
apply with the intended access model: owner-scoped reads and nil create/update/
delete rules (plugin-only writes). Locks the Phase-0 CRM sync data model against
a regression that would leave it world-readable or unwritable by the sync plugin.

* test: account for the 10 CRM collections in collection-count assertions

Migration 1780500000_connect_messaging_calendar adds 10 base-type collections
(connect/messaging/calendar), so the default collection set grows 17 → 27. Update
the enumeration/import tests to match:
- core TestFindAllCollections: all-types total 17 → 27 (auth=4/view=7 unchanged;
  the new collections are all type "base").
- apis TestCollectionsList: "totalItems":17 → 27.
- apis TestCollectionsImport: totalCollections baseline 17 → 27, and the
  deleteMissing scenario now deletes 9 + 10 = 19 collections (its delete-event
  counts 9 → 19). The dynamic systemCollections assertion is unchanged (the CRM
  collections are non-system).

* test(platform): deterministic idle-eviction test (kill sweeper race)

TestPoolManager_IdleEviction flaked as 'expected 3 pools, got 2': the
setup loop released each pool immediately, so pool 0 went idle and the
sweeper could evict it before the Len()==3 assertion on a slow runner.
The #25 timeout bump (300ms) only widened the window, not closed it.

Fix gates on a deterministic signal, not wall-clock:
- Hold all 3 pools IN-USE through the Len()==3 assertion. The sweeper
  skips refCount>0 pools (evictIdleLocked), so setup can never race it.
- After release, poll on IdleEvictions (the sweeper's last, monotonic
  step) instead of Len(): a pool leaves the LRU under lock before its
  IdleEvictions++ runs post-unlock, so Len()==0 can be observed while
  the counter lags. Same hardening applied to _SkipsInUse.
- Replace fixed sleeps with a bounded eventually() poll.

Verified: 50x -race against dbx.PoolManager (incl. a 60ms/open slow
Connect that reproduces the original flake) all green.

---------

Co-authored-by: zeekay <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-04 11:21:41 -07:00

40 lines
1.3 KiB
Go

package migrations_test
import (
"testing"
"github.com/hanzoai/base/tests"
)
// Proves the connect/messaging/calendar migration applies at boot and that all
// ten collections exist with the intended access model: owner-scoped reads and
// plugin-only writes (the sync plugin is the sole writer). Guards against a
// regression that would leave the CRM sync model unwritable-by-plugin or
// world-readable.
func TestConnectMessagingCalendar_BootApplies(t *testing.T) {
app, err := tests.NewTestApp()
if err != nil {
t.Fatal(err)
}
defer app.Cleanup()
want := []string{
"connectedAccount",
"messageChannel", "messageThread", "message", "messageParticipant", "messageAssociation",
"calendarChannel", "calendarEvent", "calendarEventParticipant", "calendarEventAssociation",
}
for _, name := range want {
c, err := app.FindCollectionByNameOrId(name)
if err != nil {
t.Fatalf("collection %q not created by migration: %v", name, err)
}
if c.CreateRule != nil || c.UpdateRule != nil || c.DeleteRule != nil {
t.Errorf("%q must be plugin-only-write (nil create/update/delete rules), got C=%v U=%v D=%v",
name, c.CreateRule, c.UpdateRule, c.DeleteRule)
}
if c.ListRule == nil || *c.ListRule != "owner = @request.auth.id" {
t.Errorf("%q must be owner-scoped readable, got ListRule=%v", name, c.ListRule)
}
}
}