From 5555950f0315d8f8177e066fd39fefafbd49fa83 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Jun 2026 10:10:53 +0200 Subject: [PATCH] refactor(testing): extract e2e fixture reset/truncate into shared package Pull the HTTP-agnostic table reset and truncate-all logic out of the v1 testing handlers into pkg/routes/api/shared so /api/v2 can reuse it. v1's wire behavior is unchanged; it now delegates to the shared functions. --- pkg/routes/api/shared/testing.go | 92 ++++++++++++++++++++++++++++++++ pkg/routes/api/v1/testing.go | 66 ++--------------------- 2 files changed, 95 insertions(+), 63 deletions(-) create mode 100644 pkg/routes/api/shared/testing.go diff --git a/pkg/routes/api/shared/testing.go b/pkg/routes/api/shared/testing.go new file mode 100644 index 000000000..ba9118e5a --- /dev/null +++ b/pkg/routes/api/shared/testing.go @@ -0,0 +1,92 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package shared + +import ( + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/events" + "code.vikunja.io/api/pkg/license" + "code.vikunja.io/api/pkg/log" +) + +// dependentTestingTables lists tables that reference a reset table by ID and +// must be truncated alongside it. Without foreign key cascades, stale rows +// would persist and pollute subsequent tests that reuse the same +// auto-increment IDs. +var dependentTestingTables = map[string][]string{ + "users": {"notifications"}, +} + +// ReplaceTableContents resets a single table to the provided rows for the e2e +// testing endpoint and returns the table's resulting contents. When truncate is +// true the table (and any dependent tables) is emptied first; otherwise the rows +// are restored on top of existing data. Callers must already have verified the +// testing token. +func ReplaceTableContents(table string, content []map[string]interface{}, truncate bool) ([]map[string]interface{}, error) { + // Wait for all async event handlers from the previous test to complete + // before modifying the database. Without this, handlers hold SQLite + // connections and starve this request's truncate/insert operations. + events.WaitForPendingHandlers() + + var err error + if truncate { + for _, dep := range dependentTestingTables[table] { + if err = db.RestoreAndTruncate(dep, nil); err != nil { + return nil, err + } + } + err = db.RestoreAndTruncate(table, content) + } else { + err = db.Restore(table, content) + } + if err != nil { + return nil, err + } + + // License state is cached at startup; re-apply so tests take effect without a restart. + if table == "license_status" { + if err := license.ReloadFromCache(); err != nil { + return nil, err + } + } + + s := db.NewSession() + defer s.Close() + data := []map[string]interface{}{} + if err := s.Table(table).Find(&data); err != nil { + return nil, err + } + return data, nil +} + +// TruncateAllTestingTables empties every Vikunja table for the e2e testing +// endpoint. Callers must already have verified the testing token. +func TruncateAllTestingTables() error { + events.WaitForPendingHandlers() + + if err := db.TruncateAllTables(); err != nil { + return err + } + + // Reload after truncate; otherwise features enabled by a prior test outlive + // the now-empty license_status table. A reload failure here is non-fatal — + // the truncate already succeeded — so it is logged and swallowed. + if err := license.ReloadFromCache(); err != nil { + log.Errorf("Error reloading license after truncate: %v", err) + } + return nil +} diff --git a/pkg/routes/api/v1/testing.go b/pkg/routes/api/v1/testing.go index 98f5aeca1..62d5f5206 100644 --- a/pkg/routes/api/v1/testing.go +++ b/pkg/routes/api/v1/testing.go @@ -22,10 +22,8 @@ import ( "net/http" "code.vikunja.io/api/pkg/config" - "code.vikunja.io/api/pkg/db" - "code.vikunja.io/api/pkg/events" - "code.vikunja.io/api/pkg/license" "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/routes/api/shared" "github.com/labstack/echo/v5" ) @@ -63,36 +61,8 @@ func HandleTesting(c *echo.Context) error { }) } - // Wait for all async event handlers from the previous test to complete - // before modifying the database. Without this, handlers hold SQLite - // connections and starve this request's truncate/insert operations. - events.WaitForPendingHandlers() - truncate := c.QueryParam("truncate") - if truncate == "true" || truncate == "" { - // When truncating certain tables, also truncate dependent tables - // whose rows reference the truncated table by user/entity ID. - // Without foreign key cascades, stale rows would persist and - // pollute subsequent tests that reuse the same auto-increment IDs. - dependentTables := map[string][]string{ - "users": {"notifications"}, - } - if deps, ok := dependentTables[table]; ok { - for _, dep := range deps { - if err = db.RestoreAndTruncate(dep, nil); err != nil { - log.Errorf("Error truncating dependent table %s: %v", dep, err) - return c.JSON(http.StatusInternalServerError, map[string]interface{}{ - "error": true, - "message": err.Error(), - }) - } - } - } - err = db.RestoreAndTruncate(table, content) - } else { - err = db.Restore(table, content) - } - + data, err := shared.ReplaceTableContents(table, content, truncate == "true" || truncate == "") if err != nil { log.Errorf("Error replacing table data: %v", err) return c.JSON(http.StatusInternalServerError, map[string]interface{}{ @@ -101,29 +71,6 @@ func HandleTesting(c *echo.Context) error { }) } - // License state is cached at startup; re-apply so tests take effect without a restart. - if table == "license_status" { - if err := license.ReloadFromCache(); err != nil { - log.Errorf("Error reloading license from seeded cache: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]interface{}{ - "error": true, - "message": err.Error(), - }) - } - } - - s := db.NewSession() - defer s.Close() - data := []map[string]interface{}{} - err = s.Table(table).Find(&data) - if err != nil { - log.Errorf("Error fetching table data: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]interface{}{ - "error": true, - "message": err.Error(), - }) - } - return c.JSON(http.StatusCreated, data) } @@ -142,9 +89,7 @@ func HandleTestingTruncateAll(c *echo.Context) error { return echo.ErrForbidden } - events.WaitForPendingHandlers() - - if err := db.TruncateAllTables(); err != nil { + if err := shared.TruncateAllTestingTables(); err != nil { log.Errorf("Error truncating all tables: %v", err) return c.JSON(http.StatusInternalServerError, map[string]interface{}{ "error": true, @@ -152,11 +97,6 @@ func HandleTestingTruncateAll(c *echo.Context) error { }) } - // Reload after truncate; otherwise features enabled by a prior test outlive the now-empty license_status table. - if err := license.ReloadFromCache(); err != nil { - log.Errorf("Error reloading license after truncate: %v", err) - } - return c.JSON(http.StatusOK, map[string]string{ "message": "ok", })