mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
core.App.Logger() now returns logger.Logger (the new shim interface) instead of *slog.Logger, so every consumer talks to the same backend-agnostic surface and stops pinning itself to log/slog. The raw *slog.Logger is still reachable as App.SlogLogger() for the in-app BatchHandler reload hook and the two tests that type-assert the underlying handler — that's the only sanctioned escape hatch. Plugins and network code that previously held *slog.Logger fields move to luxlog.Logger (kms, vault, zap-transport, base-network/transport_zap, network/archive_writer, tools/tasks/client). luxfi/zap's Logger field was a pure transit dependency; we drop it from base call sites since zaplib has its own default. Remaining "log/slog" imports — 11 files — are confined to the adapter/handler bridge (tools/logger and core/log_printer/base/app) and to two tests that exercise the slog.Handler contract directly. Those are the irreducible boundary where slog is the language of the interface.
572 lines
15 KiB
Go
572 lines
15 KiB
Go
package core_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"log/slog"
|
|
"os"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "unsafe"
|
|
|
|
"github.com/hanzoai/dbx"
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tests"
|
|
"github.com/hanzoai/base/tools/logger"
|
|
"github.com/hanzoai/base/tools/mailer"
|
|
)
|
|
|
|
func TestNewBaseApp(t *testing.T) {
|
|
const testDataDir = "./hz_base_app_test_data_dir/"
|
|
defer os.RemoveAll(testDataDir)
|
|
|
|
app := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: testDataDir,
|
|
EncryptionEnv: "test_env",
|
|
IsDev: true,
|
|
})
|
|
|
|
if app.DataDir() != testDataDir {
|
|
t.Fatalf("expected DataDir %q, got %q", testDataDir, app.DataDir())
|
|
}
|
|
|
|
if app.EncryptionEnv() != "test_env" {
|
|
t.Fatalf("expected EncryptionEnv test_env, got %q", app.EncryptionEnv())
|
|
}
|
|
|
|
if !app.IsDev() {
|
|
t.Fatalf("expected IsDev true, got %v", app.IsDev())
|
|
}
|
|
|
|
if app.Store() == nil {
|
|
t.Fatal("expected Store to be set, got nil")
|
|
}
|
|
|
|
if app.Settings() == nil {
|
|
t.Fatal("expected Settings to be set, got nil")
|
|
}
|
|
|
|
if app.SubscriptionsBroker() == nil {
|
|
t.Fatal("expected SubscriptionsBroker to be set, got nil")
|
|
}
|
|
|
|
if app.Cron() == nil {
|
|
t.Fatal("expected Cron to be set, got nil")
|
|
}
|
|
}
|
|
|
|
func TestBaseAppBootstrap(t *testing.T) {
|
|
const testDataDir = "./hz_base_app_test_data_dir/"
|
|
defer os.RemoveAll(testDataDir)
|
|
|
|
app := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: testDataDir,
|
|
})
|
|
defer app.ResetBootstrapState()
|
|
|
|
if app.IsBootstrapped() {
|
|
t.Fatal("Didn't expect the application to be bootstrapped.")
|
|
}
|
|
|
|
if err := app.Bootstrap(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !app.IsBootstrapped() {
|
|
t.Fatal("Expected the application to be bootstrapped.")
|
|
}
|
|
|
|
if stat, err := os.Stat(testDataDir); err != nil || !stat.IsDir() {
|
|
t.Fatal("Expected test data directory to be created.")
|
|
}
|
|
|
|
type nilCheck struct {
|
|
name string
|
|
value any
|
|
expectNil bool
|
|
}
|
|
|
|
runNilChecks := func(checks []nilCheck) {
|
|
for _, check := range checks {
|
|
t.Run(check.name, func(t *testing.T) {
|
|
isNil := check.value == nil
|
|
if isNil != check.expectNil {
|
|
t.Fatalf("Expected isNil %v, got %v", check.expectNil, isNil)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
nilChecksBeforeReset := []nilCheck{
|
|
{"[before] db", app.DB(), false},
|
|
{"[before] concurrentDB", app.ConcurrentDB(), false},
|
|
{"[before] nonconcurrentDB", app.NonconcurrentDB(), false},
|
|
{"[before] auxDB", app.AuxDB(), false},
|
|
{"[before] auxConcurrentDB", app.AuxConcurrentDB(), false},
|
|
{"[before] auxNonconcurrentDB", app.AuxNonconcurrentDB(), false},
|
|
{"[before] settings", app.Settings(), false},
|
|
{"[before] logger", app.Logger(), false},
|
|
{"[before] cached collections", app.Store().Get(core.StoreKeyCachedCollections), false},
|
|
}
|
|
|
|
runNilChecks(nilChecksBeforeReset)
|
|
|
|
// reset
|
|
if err := app.ResetBootstrapState(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
nilChecksAfterReset := []nilCheck{
|
|
{"[after] db", app.DB(), true},
|
|
{"[after] concurrentDB", app.ConcurrentDB(), true},
|
|
{"[after] nonconcurrentDB", app.NonconcurrentDB(), true},
|
|
{"[after] auxDB", app.AuxDB(), true},
|
|
{"[after] auxConcurrentDB", app.AuxConcurrentDB(), true},
|
|
{"[after] auxNonconcurrentDB", app.AuxNonconcurrentDB(), true},
|
|
{"[after] settings", app.Settings(), false},
|
|
{"[after] logger", app.Logger(), false},
|
|
{"[after] cached collections", app.Store().Get(core.StoreKeyCachedCollections), false},
|
|
}
|
|
|
|
runNilChecks(nilChecksAfterReset)
|
|
}
|
|
|
|
func TestNewBaseAppTx(t *testing.T) {
|
|
const testDataDir = "./test_data_dir/"
|
|
defer os.RemoveAll(testDataDir)
|
|
|
|
app := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: testDataDir,
|
|
})
|
|
defer app.ResetBootstrapState()
|
|
|
|
if err := app.Bootstrap(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mustNotHaveTx := func(app core.App) {
|
|
if app.IsTransactional() {
|
|
t.Fatalf("Didn't expect the app to be transactional")
|
|
}
|
|
|
|
if app.TxInfo() != nil {
|
|
t.Fatalf("Didn't expect the app.txInfo to be loaded")
|
|
}
|
|
}
|
|
|
|
mustHaveTx := func(app core.App) {
|
|
if !app.IsTransactional() {
|
|
t.Fatalf("Expected the app to be transactional")
|
|
}
|
|
|
|
if app.TxInfo() == nil {
|
|
t.Fatalf("Expected the app.txInfo to be loaded")
|
|
}
|
|
}
|
|
|
|
mustNotHaveTx(app)
|
|
|
|
app.RunInTransaction(func(txApp core.App) error {
|
|
mustHaveTx(txApp)
|
|
return nil
|
|
})
|
|
|
|
mustNotHaveTx(app)
|
|
}
|
|
|
|
func TestBaseAppNewMailClient(t *testing.T) {
|
|
const testDataDir = "./hz_base_app_test_data_dir/"
|
|
defer os.RemoveAll(testDataDir)
|
|
|
|
app := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: testDataDir,
|
|
EncryptionEnv: "hz_test_env",
|
|
})
|
|
defer app.ResetBootstrapState()
|
|
|
|
client1 := app.NewMailClient()
|
|
m1, ok := client1.(*mailer.Sendmail)
|
|
if !ok {
|
|
t.Fatalf("Expected mailer.Sendmail instance, got %v", m1)
|
|
}
|
|
if m1.OnSend() == nil || m1.OnSend().Length() == 0 {
|
|
t.Fatal("Expected OnSend hook to be registered")
|
|
}
|
|
|
|
app.Settings().SMTP.Enabled = true
|
|
|
|
client2 := app.NewMailClient()
|
|
m2, ok := client2.(*mailer.SMTPClient)
|
|
if !ok {
|
|
t.Fatalf("Expected mailer.SMTPClient instance, got %v", m2)
|
|
}
|
|
if m2.OnSend() == nil || m2.OnSend().Length() == 0 {
|
|
t.Fatal("Expected OnSend hook to be registered")
|
|
}
|
|
}
|
|
|
|
func TestBaseAppNewFilesystem(t *testing.T) {
|
|
const testDataDir = "./hz_base_app_test_data_dir/"
|
|
defer os.RemoveAll(testDataDir)
|
|
|
|
app := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: testDataDir,
|
|
})
|
|
defer app.ResetBootstrapState()
|
|
|
|
// local
|
|
local, localErr := app.NewFilesystem()
|
|
if localErr != nil {
|
|
t.Fatal(localErr)
|
|
}
|
|
if local == nil {
|
|
t.Fatal("Expected local filesystem instance, got nil")
|
|
}
|
|
|
|
// misconfigured s3
|
|
app.Settings().S3.Enabled = true
|
|
s3, s3Err := app.NewFilesystem()
|
|
if s3Err == nil {
|
|
t.Fatal("Expected S3 error, got nil")
|
|
}
|
|
if s3 != nil {
|
|
t.Fatalf("Expected nil s3 filesystem, got %v", s3)
|
|
}
|
|
}
|
|
|
|
func TestBaseAppNewBackupsFilesystem(t *testing.T) {
|
|
const testDataDir = "./hz_base_app_test_data_dir/"
|
|
defer os.RemoveAll(testDataDir)
|
|
|
|
app := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: testDataDir,
|
|
})
|
|
defer app.ResetBootstrapState()
|
|
|
|
// local
|
|
local, localErr := app.NewBackupsFilesystem()
|
|
if localErr != nil {
|
|
t.Fatal(localErr)
|
|
}
|
|
if local == nil {
|
|
t.Fatal("Expected local backups filesystem instance, got nil")
|
|
}
|
|
|
|
// misconfigured s3
|
|
app.Settings().Backups.S3.Enabled = true
|
|
s3, s3Err := app.NewBackupsFilesystem()
|
|
if s3Err == nil {
|
|
t.Fatal("Expected S3 error, got nil")
|
|
}
|
|
if s3 != nil {
|
|
t.Fatalf("Expected nil s3 backups filesystem, got %v", s3)
|
|
}
|
|
}
|
|
|
|
func TestBaseAppLoggerWrites(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
// reset
|
|
if err := app.DeleteOldLogs(time.Now()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
const logsThreshold = 200
|
|
|
|
totalLogs := func(app core.App, t *testing.T) int {
|
|
var total int
|
|
|
|
err := app.LogQuery().Select("count(*)").Row(&total)
|
|
if err != nil {
|
|
t.Fatalf("Failed to fetch total logs: %v", err)
|
|
}
|
|
|
|
return total
|
|
}
|
|
|
|
t.Run("disabled logs retention", func(t *testing.T) {
|
|
app.Settings().Logs.MaxDays = 0
|
|
|
|
for i := 0; i < logsThreshold+1; i++ {
|
|
app.Logger().Error("test")
|
|
}
|
|
|
|
if total := totalLogs(app, t); total != 0 {
|
|
t.Fatalf("Expected no logs, got %d", total)
|
|
}
|
|
})
|
|
|
|
t.Run("test batch logs writes", func(t *testing.T) {
|
|
app.Settings().Logs.MaxDays = 1
|
|
|
|
for i := 0; i < logsThreshold-1; i++ {
|
|
app.Logger().Error("test")
|
|
}
|
|
|
|
if total := totalLogs(app, t); total != 0 {
|
|
t.Fatalf("Expected no logs, got %d", total)
|
|
}
|
|
|
|
// should trigger batch write
|
|
app.Logger().Error("test")
|
|
|
|
// should be added for the next batch write
|
|
app.Logger().Error("test")
|
|
|
|
if total := totalLogs(app, t); total != logsThreshold {
|
|
t.Fatalf("Expected %d logs, got %d", logsThreshold, total)
|
|
}
|
|
|
|
// wait for ~3 secs to check the timer trigger
|
|
time.Sleep(3200 * time.Millisecond)
|
|
if total := totalLogs(app, t); total != logsThreshold+1 {
|
|
t.Fatalf("Expected %d logs, got %d", logsThreshold+1, total)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBaseAppRefreshSettingsLoggerMinLevelEnabled(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
isDev bool
|
|
level int
|
|
// level->enabled map
|
|
expectations map[int]bool
|
|
}{
|
|
{
|
|
"dev mode",
|
|
true,
|
|
4,
|
|
map[int]bool{
|
|
3: true,
|
|
4: true,
|
|
5: true,
|
|
},
|
|
},
|
|
{
|
|
"nondev mode",
|
|
false,
|
|
4,
|
|
map[int]bool{
|
|
3: false,
|
|
4: true,
|
|
5: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
const testDataDir = "./hz_base_app_test_data_dir/"
|
|
defer os.RemoveAll(testDataDir)
|
|
|
|
app := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: testDataDir,
|
|
IsDev: s.isDev,
|
|
})
|
|
defer app.ResetBootstrapState()
|
|
|
|
if err := app.Bootstrap(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// silence query logs
|
|
app.ConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
|
|
app.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
|
|
app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
|
|
app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
|
|
|
|
sl := app.SlogLogger()
|
|
handler, ok := sl.Handler().(*logger.BatchHandler)
|
|
if !ok {
|
|
t.Fatalf("Expected BatchHandler, got %v", sl.Handler())
|
|
}
|
|
|
|
app.Settings().Logs.MinLevel = s.level
|
|
|
|
if err := app.Save(app.Settings()); err != nil {
|
|
t.Fatalf("Failed to save settings: %v", err)
|
|
}
|
|
|
|
for level, enabled := range s.expectations {
|
|
if v := handler.Enabled(context.Background(), slog.Level(level)); v != enabled {
|
|
t.Fatalf("Expected level %d Enabled() to be %v, got %v", level, enabled, v)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBaseAppDBDualBuilder(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
concurrentQueries := []string{}
|
|
nonconcurrentQueries := []string{}
|
|
app.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
|
concurrentQueries = append(concurrentQueries, sql)
|
|
}
|
|
app.ConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
|
concurrentQueries = append(concurrentQueries, sql)
|
|
}
|
|
app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
|
nonconcurrentQueries = append(nonconcurrentQueries, sql)
|
|
}
|
|
app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
|
nonconcurrentQueries = append(nonconcurrentQueries, sql)
|
|
}
|
|
|
|
type testQuery struct {
|
|
query string
|
|
isConcurrent bool
|
|
}
|
|
|
|
regularTests := []testQuery{
|
|
{" \n sEleCt 1", true},
|
|
{"With abc(x) AS (select 2) SELECT x FROM abc", true},
|
|
{"create table t1(x int)", false},
|
|
{"insert into t1(x) values(1)", false},
|
|
{"update t1 set x = 2", false},
|
|
{"delete from t1", false},
|
|
}
|
|
|
|
txTests := []testQuery{
|
|
{"select 3", false},
|
|
{" \n WITH abc(x) AS (select 4) SELECT x FROM abc", false},
|
|
{"create table t2(x int)", false},
|
|
{"insert into t2(x) values(1)", false},
|
|
{"update t2 set x = 2", false},
|
|
{"delete from t2", false},
|
|
}
|
|
|
|
for _, item := range regularTests {
|
|
_, err := app.DB().NewQuery(item.query).Execute()
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute query %q error: %v", item.query, err)
|
|
}
|
|
}
|
|
|
|
app.RunInTransaction(func(txApp core.App) error {
|
|
for _, item := range txTests {
|
|
_, err := txApp.DB().NewQuery(item.query).Execute()
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute query %q error: %v", item.query, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
allTests := append(regularTests, txTests...)
|
|
for _, item := range allTests {
|
|
if item.isConcurrent {
|
|
if !slices.Contains(concurrentQueries, item.query) {
|
|
t.Fatalf("Expected concurrent query\n%q\ngot\nconcurrent:%v\nnonconcurrent:%v", item.query, concurrentQueries, nonconcurrentQueries)
|
|
}
|
|
} else {
|
|
if !slices.Contains(nonconcurrentQueries, item.query) {
|
|
t.Fatalf("Expected nonconcurrent query\n%q\ngot\nconcurrent:%v\nnonconcurrent:%v", item.query, concurrentQueries, nonconcurrentQueries)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBaseAppAuxDBDualBuilder(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
concurrentQueries := []string{}
|
|
nonconcurrentQueries := []string{}
|
|
app.AuxConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
|
concurrentQueries = append(concurrentQueries, sql)
|
|
}
|
|
app.AuxConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
|
concurrentQueries = append(concurrentQueries, sql)
|
|
}
|
|
app.AuxNonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
|
nonconcurrentQueries = append(nonconcurrentQueries, sql)
|
|
}
|
|
app.AuxNonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
|
nonconcurrentQueries = append(nonconcurrentQueries, sql)
|
|
}
|
|
|
|
type testQuery struct {
|
|
query string
|
|
isConcurrent bool
|
|
}
|
|
|
|
regularTests := []testQuery{
|
|
{" \n sEleCt 1", true},
|
|
{"With abc(x) AS (select 2) SELECT x FROM abc", true},
|
|
{"create table t1(x int)", false},
|
|
{"insert into t1(x) values(1)", false},
|
|
{"update t1 set x = 2", false},
|
|
{"delete from t1", false},
|
|
}
|
|
|
|
txTests := []testQuery{
|
|
{"select 3", false},
|
|
{" \n WITH abc(x) AS (select 4) SELECT x FROM abc", false},
|
|
{"create table t2(x int)", false},
|
|
{"insert into t2(x) values(1)", false},
|
|
{"update t2 set x = 2", false},
|
|
{"delete from t2", false},
|
|
}
|
|
|
|
for _, item := range regularTests {
|
|
_, err := app.AuxDB().NewQuery(item.query).Execute()
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute query %q error: %v", item.query, err)
|
|
}
|
|
}
|
|
|
|
app.AuxRunInTransaction(func(txApp core.App) error {
|
|
for _, item := range txTests {
|
|
_, err := txApp.AuxDB().NewQuery(item.query).Execute()
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute query %q error: %v", item.query, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
allTests := append(regularTests, txTests...)
|
|
for _, item := range allTests {
|
|
if item.isConcurrent {
|
|
if !slices.Contains(concurrentQueries, item.query) {
|
|
t.Fatalf("Expected concurrent query\n%q\ngot\nconcurrent:%v\nnonconcurrent:%v", item.query, concurrentQueries, nonconcurrentQueries)
|
|
}
|
|
} else {
|
|
if !slices.Contains(nonconcurrentQueries, item.query) {
|
|
t.Fatalf("Expected nonconcurrent query\n%q\ngot\nconcurrent:%v\nnonconcurrent:%v", item.query, concurrentQueries, nonconcurrentQueries)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBaseAppTriggerOnTerminate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
event := new(core.TerminateEvent)
|
|
event.App = app
|
|
|
|
// trigger OnTerminate multiple times to ensure that it doesn't deadlock
|
|
// https://github.com/hanzoai/base/pull/7305
|
|
app.OnTerminate().Trigger(event)
|
|
app.OnTerminate().Trigger(event)
|
|
app.OnTerminate().Trigger(event)
|
|
}
|