package core_test import ( "errors" "testing" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" ) func TestRunInTransaction(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() t.Run("failed nested transaction", func(t *testing.T) { app.RunInTransaction(func(txApp core.App) error { superuser, _ := txApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") return txApp.RunInTransaction(func(tx2Dao core.App) error { if err := tx2Dao.Delete(superuser); err != nil { t.Fatal(err) } return errors.New("test error") }) }) // superuser should still exist superuser, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") if superuser == nil { t.Fatal("Expected superuser test@example.com to not be deleted") } }) t.Run("successful nested transaction", func(t *testing.T) { app.RunInTransaction(func(txApp core.App) error { superuser, _ := txApp.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") return txApp.RunInTransaction(func(tx2Dao core.App) error { return tx2Dao.Delete(superuser) }) }) // superuser should have been deleted superuser, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") if superuser != nil { t.Fatalf("Expected superuser test@example.com to be deleted, found %v", superuser) } }) } func TestTransactionHooksCallsOnFailure(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() createHookCalls := 0 updateHookCalls := 0 deleteHookCalls := 0 afterCreateHookCalls := 0 afterUpdateHookCalls := 0 afterDeleteHookCalls := 0 app.OnModelCreate().BindFunc(func(e *core.ModelEvent) error { createHookCalls++ return e.Next() }) app.OnModelUpdate().BindFunc(func(e *core.ModelEvent) error { updateHookCalls++ return e.Next() }) app.OnModelDelete().BindFunc(func(e *core.ModelEvent) error { deleteHookCalls++ return e.Next() }) app.OnModelAfterCreateSuccess().BindFunc(func(e *core.ModelEvent) error { afterCreateHookCalls++ return e.Next() }) app.OnModelAfterUpdateSuccess().BindFunc(func(e *core.ModelEvent) error { afterUpdateHookCalls++ return e.Next() }) app.OnModelAfterDeleteSuccess().BindFunc(func(e *core.ModelEvent) error { afterDeleteHookCalls++ return e.Next() }) existingModel, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") app.RunInTransaction(func(txApp1 core.App) error { return txApp1.RunInTransaction(func(txApp2 core.App) error { // test create // --- newModel := core.NewRecord(existingModel.Collection()) newModel.SetEmail("test_new1@example.com") newModel.SetPassword("1234567890") if err := txApp2.Save(newModel); err != nil { t.Fatal(err) } // test update (twice) // --- if err := txApp2.Save(existingModel); err != nil { t.Fatal(err) } if err := txApp2.Save(existingModel); err != nil { t.Fatal(err) } // test delete // --- if err := txApp2.Delete(newModel); err != nil { t.Fatal(err) } return errors.New("test_tx_error") }) }) if createHookCalls != 1 { t.Errorf("Expected createHookCalls to be called 1 time, got %d", createHookCalls) } if updateHookCalls != 2 { t.Errorf("Expected updateHookCalls to be called 2 times, got %d", updateHookCalls) } if deleteHookCalls != 1 { t.Errorf("Expected deleteHookCalls to be called 1 time, got %d", deleteHookCalls) } if afterCreateHookCalls != 0 { t.Errorf("Expected afterCreateHookCalls to be called 0 times, got %d", afterCreateHookCalls) } if afterUpdateHookCalls != 0 { t.Errorf("Expected afterUpdateHookCalls to be called 0 times, got %d", afterUpdateHookCalls) } if afterDeleteHookCalls != 0 { t.Errorf("Expected afterDeleteHookCalls to be called 0 times, got %d", afterDeleteHookCalls) } } func TestTransactionHooksCallsOnSuccess(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() createHookCalls := 0 updateHookCalls := 0 deleteHookCalls := 0 afterCreateHookCalls := 0 afterUpdateHookCalls := 0 afterDeleteHookCalls := 0 app.OnModelCreate().BindFunc(func(e *core.ModelEvent) error { createHookCalls++ return e.Next() }) app.OnModelUpdate().BindFunc(func(e *core.ModelEvent) error { updateHookCalls++ return e.Next() }) app.OnModelDelete().BindFunc(func(e *core.ModelEvent) error { deleteHookCalls++ return e.Next() }) app.OnModelAfterCreateSuccess().BindFunc(func(e *core.ModelEvent) error { if e.App.IsTransactional() { t.Fatal("Expected e.App to be non-transactional") } afterCreateHookCalls++ return e.Next() }) app.OnModelAfterUpdateSuccess().BindFunc(func(e *core.ModelEvent) error { if e.App.IsTransactional() { t.Fatal("Expected e.App to be non-transactional") } afterUpdateHookCalls++ return e.Next() }) app.OnModelAfterDeleteSuccess().BindFunc(func(e *core.ModelEvent) error { if e.App.IsTransactional() { t.Fatal("Expected e.App to be non-transactional") } afterDeleteHookCalls++ return e.Next() }) existingModel, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com") app.RunInTransaction(func(txApp1 core.App) error { return txApp1.RunInTransaction(func(txApp2 core.App) error { // test create // --- newModel := core.NewRecord(existingModel.Collection()) newModel.SetEmail("test_new1@example.com") newModel.SetPassword("1234567890") if err := txApp2.Save(newModel); err != nil { t.Fatal(err) } // test update (twice) // --- if err := txApp2.Save(existingModel); err != nil { t.Fatal(err) } if err := txApp2.Save(existingModel); err != nil { t.Fatal(err) } // test delete // --- if err := txApp2.Delete(newModel); err != nil { t.Fatal(err) } return nil }) }) if createHookCalls != 1 { t.Errorf("Expected createHookCalls to be called 1 time, got %d", createHookCalls) } if updateHookCalls != 2 { t.Errorf("Expected updateHookCalls to be called 2 times, got %d", updateHookCalls) } if deleteHookCalls != 1 { t.Errorf("Expected deleteHookCalls to be called 1 time, got %d", deleteHookCalls) } if afterCreateHookCalls != 1 { t.Errorf("Expected afterCreateHookCalls to be called 1 time, got %d", afterCreateHookCalls) } if afterUpdateHookCalls != 2 { t.Errorf("Expected afterUpdateHookCalls to be called 2 times, got %d", afterUpdateHookCalls) } if afterDeleteHookCalls != 1 { t.Errorf("Expected afterDeleteHookCalls to be called 1 time, got %d", afterDeleteHookCalls) } } func TestTransactionFromInnerCreateHook(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() app.OnRecordCreateExecute("demo2").BindFunc(func(e *core.RecordEvent) error { originalApp := e.App return e.App.RunInTransaction(func(txApp core.App) error { e.App = txApp defer func() { e.App = originalApp }() nextErr := e.Next() return nextErr }) }) app.OnRecordAfterCreateSuccess("demo2").BindFunc(func(e *core.RecordEvent) error { if e.App.IsTransactional() { t.Fatal("Expected e.App to be non-transactional") } // perform a db query with the app instance to ensure that it is still valid _, err := e.App.FindFirstRecordByFilter("demo2", "1=1") if err != nil { t.Fatalf("Failed to perform a db query after tx success: %v", err) } return e.Next() }) collection, err := app.FindCollectionByNameOrId("demo2") if err != nil { t.Fatal(err) } record := core.NewRecord(collection) record.Set("title", "test_inner_tx") if err = app.Save(record); err != nil { t.Fatalf("Create failed: %v", err) } expectedHookCalls := map[string]int{ "OnRecordCreateExecute": 1, "OnRecordAfterCreateSuccess": 1, } for k, total := range expectedHookCalls { if found, ok := app.EventCalls[k]; !ok || total != found { t.Fatalf("Expected %q %d calls, got %d", k, total, found) } } } func TestTransactionFromInnerUpdateHook(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() app.OnRecordUpdateExecute("demo2").BindFunc(func(e *core.RecordEvent) error { originalApp := e.App return e.App.RunInTransaction(func(txApp core.App) error { e.App = txApp defer func() { e.App = originalApp }() nextErr := e.Next() return nextErr }) }) app.OnRecordAfterUpdateSuccess("demo2").BindFunc(func(e *core.RecordEvent) error { if e.App.IsTransactional() { t.Fatal("Expected e.App to be non-transactional") } // perform a db query with the app instance to ensure that it is still valid _, err := e.App.FindFirstRecordByFilter("demo2", "1=1") if err != nil { t.Fatalf("Failed to perform a db query after tx success: %v", err) } return e.Next() }) existingModel, err := app.FindFirstRecordByFilter("demo2", "1=1") if err != nil { t.Fatal(err) } if err = app.Save(existingModel); err != nil { t.Fatalf("Update failed: %v", err) } expectedHookCalls := map[string]int{ "OnRecordUpdateExecute": 1, "OnRecordAfterUpdateSuccess": 1, } for k, total := range expectedHookCalls { if found, ok := app.EventCalls[k]; !ok || total != found { t.Fatalf("Expected %q %d calls, got %d", k, total, found) } } } func TestTransactionFromInnerDeleteHook(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() app.OnRecordDeleteExecute("demo2").BindFunc(func(e *core.RecordEvent) error { originalApp := e.App return e.App.RunInTransaction(func(txApp core.App) error { e.App = txApp defer func() { e.App = originalApp }() nextErr := e.Next() return nextErr }) }) app.OnRecordAfterDeleteSuccess("demo2").BindFunc(func(e *core.RecordEvent) error { if e.App.IsTransactional() { t.Fatal("Expected e.App to be non-transactional") } // perform a db query with the app instance to ensure that it is still valid _, err := e.App.FindFirstRecordByFilter("demo2", "1=1") if err != nil { t.Fatalf("Failed to perform a db query after tx success: %v", err) } return e.Next() }) existingModel, err := app.FindFirstRecordByFilter("demo2", "1=1") if err != nil { t.Fatal(err) } if err = app.Delete(existingModel); err != nil { t.Fatalf("Delete failed: %v", err) } expectedHookCalls := map[string]int{ "OnRecordDeleteExecute": 1, "OnRecordAfterDeleteSuccess": 1, } for k, total := range expectedHookCalls { if found, ok := app.EventCalls[k]; !ok || total != found { t.Fatalf("Expected %q %d calls, got %d", k, total, found) } } }