mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
Rename / namespace: - Finish github.com/pocketbase/* → github.com/hanzoai/base (+ /dbx) sweep across all Go sources; restore the renamed root launcher (Base, base.go). - Drop the upstream-only surfaces ours doesn't ship (admin ui/, the password/OTP/oauth2/impersonate auth files, extensions) re-added by the upstream merge. - Decomplect the on-disk layout to one base_/ namespace: data/, migrations/, hooks/, types.d.ts are siblings under base_/ (default data dir base_/data). Drop the hz_ prefix everywhere; collapse migratecmd's identical JS/Go branches to one path. - Remove the accidentally-committed hz_data/ dev dir (db + iam.key) and gitignore the runtime namespace dirs. Deps / build: - kmsclient now imported from the light github.com/hanzoai/kms/sdk/go module (the root kms module zip exceeds Go's download limit); cloud/pkg/base Mount removed (lives in the fused daemon). - replicate v0.4.0 → v0.8.0; metric.Counter.Get() (no prometheus dto); network/metrics.go import fix; auth gitlab/github tidy. Tests / behavior: - Wire registerNotifyWatcherHooks; debounce notify files past the kqueue rescan window so cross-instance settings/collections reload fires. - migratecmd: exclude createdAt/updatedAt (not created/updated); regenerate Go+JS automigration goldens from canonical output. - Convert the two stale archive red-probes to assert the landed defenses (backlog cap bounds memory; forged-segment signature is rejected). IAM-native (Base = pure IAM client): - Rip the ~1k-line home-grown embedded IAM (iam_embedded*.go) and the iam-user CLI. Base no longer reinvents identity as _iam_users/_superusers rows. IAM_ENDPOINT (hanzo.id, or an in-process iam.Embed in the fused daemon) is required; Base validates IAM JWTs via JWKS only — superuser is the IAM admin claim, resolved to an ephemeral unpersisted record. - Extract the IDV reverse-proxy (/v1/idv/*) into idv_proxy.go — it was mis-colocated in the embedded onboarding file and is independent of IAM. - Bind the SQL console (/v1/sql) and test it through real IAM JWTs against a JWKS stub (no hardcoded record tokens, no persisted rows).
185 lines
4.7 KiB
Go
185 lines
4.7 KiB
Go
package core_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/dbx"
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tools/store"
|
|
"golang.org/x/sync/semaphore"
|
|
)
|
|
|
|
func TestNotifyWatcher_SettingsUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testEvents := store.New[core.App, int](nil)
|
|
|
|
tmpDir, err := os.MkdirTemp("", "hz_notify_test*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
app1 := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: tmpDir,
|
|
})
|
|
if err := app1.Bootstrap(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
app2 := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: tmpDir,
|
|
})
|
|
if err := app2.Bootstrap(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second)
|
|
defer cancelCtx()
|
|
|
|
sem := semaphore.NewWeighted(1)
|
|
sem.Acquire(ctx, 1)
|
|
|
|
app1.OnSettingsReload().BindFunc(func(e *core.SettingsReloadEvent) error {
|
|
testEvents.SetFunc(app1, func(old int) int {
|
|
return old + 1
|
|
})
|
|
return e.Next()
|
|
})
|
|
|
|
app2.OnSettingsReload().BindFunc(func(e *core.SettingsReloadEvent) error {
|
|
testEvents.SetFunc(app2, func(old int) int {
|
|
sem.Release(1)
|
|
|
|
return old + 1
|
|
})
|
|
return e.Next()
|
|
})
|
|
|
|
// updating app1 settings should trigger a reload in app2
|
|
app1.Settings().Meta.AppName = "notify-probe"
|
|
if err := app1.Save(app1.Settings()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// block until released or timeouted
|
|
sem.Acquire(ctx, 1)
|
|
|
|
if app1Total := testEvents.Get(app1); app1Total != 1 {
|
|
t.Fatalf("Expected 1 app1 event, got %d", app1Total)
|
|
}
|
|
|
|
if app2Total := testEvents.Get(app2); app2Total != 1 {
|
|
t.Fatalf("Expected 1 app2 event, got %d", app2Total)
|
|
}
|
|
|
|
if name := app2.Settings().Meta.AppName; name != "notify-probe" {
|
|
t.Fatalf("Expected app2 settings event to carry AppName %q, got %q", "notify-probe", name)
|
|
}
|
|
}
|
|
|
|
func TestNotifyWatcher_CollectionsUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmpDir, err := os.MkdirTemp("", "hz_notify_test*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
app1 := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: tmpDir,
|
|
})
|
|
if err := app1.Bootstrap(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
app2 := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: tmpDir,
|
|
})
|
|
if err := app2.Bootstrap(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testQueries := store.New[string, []string](nil)
|
|
app2.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
|
testQueries.SetFunc("concurrent", func(old []string) []string {
|
|
return append(old, sql)
|
|
})
|
|
}
|
|
app2.ConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
|
testQueries.SetFunc("concurrent", func(old []string) []string {
|
|
return append(old, sql)
|
|
})
|
|
}
|
|
app2.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
|
testQueries.SetFunc("nonconcurrent", func(old []string) []string {
|
|
return append(old, sql)
|
|
})
|
|
}
|
|
app2.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
|
testQueries.SetFunc("nonconcurrent", func(old []string) []string {
|
|
return append(old, sql)
|
|
})
|
|
}
|
|
|
|
ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second)
|
|
defer cancelCtx()
|
|
|
|
sem := semaphore.NewWeighted(1)
|
|
sem.Acquire(ctx, 1)
|
|
|
|
// currently there is no hook for the collections cache reload so we pool instead
|
|
done := make(chan bool, 1)
|
|
ticker := time.NewTicker(100 * time.Millisecond)
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if len(testQueries.Get("concurrent")) == 1 {
|
|
sem.Release(1)
|
|
return
|
|
}
|
|
case <-done:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// create/update/delete app1 collections should trigger a reload in app2
|
|
dummyCollection := core.NewBaseCollection("test")
|
|
if err := app1.Save(dummyCollection); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dummyCollection.Fields.Add(&core.TextField{Name: "test"})
|
|
if err := app1.Save(dummyCollection); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := app1.Delete(dummyCollection); err != nil {
|
|
}
|
|
|
|
// block until released or timeouted
|
|
sem.Acquire(ctx, 1)
|
|
ticker.Stop()
|
|
done <- true
|
|
|
|
nonconcurrentQueries := testQueries.Get("nonconcurrent")
|
|
concurrentQueries := testQueries.Get("concurrent")
|
|
|
|
if len(nonconcurrentQueries) != 0 {
|
|
t.Fatalf("Expected 0 concurrent queries, got %d (%v)", len(nonconcurrentQueries), nonconcurrentQueries)
|
|
}
|
|
if len(concurrentQueries) != 1 {
|
|
t.Fatalf("Expected 1 concurrent query, got %d (%v)", len(concurrentQueries), concurrentQueries)
|
|
}
|
|
|
|
expectedQuery := "SELECT {{_collections}}.* FROM `_collections` ORDER BY `rowid` ASC"
|
|
if concurrentQueries[0] != expectedQuery {
|
|
t.Fatalf("Expected query\n%s\ngot\n%s", expectedQuery, concurrentQueries[0])
|
|
}
|
|
}
|