1
0
Fork 0
golang-github-pocketbase-po.../core/base_test.go
Daniel Baumann e28c88ef14
Adding upstream version 0.28.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-22 10:57:38 +02:00

554 lines
14 KiB
Go

package core_test
import (
"context"
"database/sql"
"log/slog"
"os"
"slices"
"testing"
"time"
_ "unsafe"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/logger"
"github.com/pocketbase/pocketbase/tools/mailer"
)
func TestNewBaseApp(t *testing.T) {
const testDataDir = "./pb_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 = "./pb_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 = "./pb_base_app_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 = "./pb_base_app_test_data_dir/"
defer os.RemoveAll(testDataDir)
app := core.NewBaseApp(core.BaseAppConfig{
DataDir: testDataDir,
EncryptionEnv: "pb_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 = "./pb_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 = "./pb_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 = "./pb_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) {}
handler, ok := app.Logger().Handler().(*logger.BatchHandler)
if !ok {
t.Fatalf("Expected BatchHandler, got %v", app.Logger().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)
}
}
}
}