Async event handlers (via Watermill) from the previous test can hold
SQLite connections, starving the next test's fixture setup PATCH request.
Three changes fix this:
1. Track in-flight event handler goroutines with a WaitGroup.
2. Call WaitForPendingHandlers() in the test endpoint before
truncating/inserting data.
3. Navigate the browser to about:blank in fixture teardown to stop
notification polling and other frontend requests between tests.
checkUserCaldavTokens called user.GetCaldavTokens which creates its own
db.NewSession(), while the caller (BasicAuth) already holds an open
session. With SQLite this caused a deadlock because the second session
blocks on the write lock held by the first session in the same goroutine.
Add GetCaldavTokensWithSession that accepts an existing session and use
it from checkUserCaldavTokens.
SameSite=None requires Secure=true per browser spec. When running over
plain HTTP (local dev, e2e tests), browsers reject or downgrade the
cookie, breaking session refresh. Fall back to SameSite=Lax for HTTP
while keeping SameSite=None for HTTPS (needed for the Electron desktop
app cross-origin scenario).
Three SQLite connection issues are fixed:
1. The refactoring in 26c0f71 accidentally dropped _busy_timeout from
the file-based SQLite connection string. Without it, concurrent
transactions get instant SQLITE_BUSY errors instead of waiting.
2. _txlock=immediate forced ALL transactions (including reads) to
acquire the write lock at BEGIN, serializing all database access.
WAL mode makes this unnecessary: readers use snapshots and never
block writers, so the SHARED-to-RESERVED deadlock cannot occur.
3. In-memory shared cache (file::memory:?cache=shared) uses table-level
locking where _busy_timeout is ineffective (returns SQLITE_LOCKED,
not SQLITE_BUSY) and concurrent connections deadlock. Replace with a
temp file using WAL mode for proper concurrency.
MaxOpenConns(1) caused Go-level deadlocks: when two goroutines needed
database connections concurrently, the second blocked forever waiting
for the single connection pool slot. This broke CI (sqlite web tests
timed out after 45min, e2e tests hung).
The actual "database is locked" errors were caused by SQLite's default
deferred transaction locking: two connections both acquire SHARED locks,
then deadlock when both try to promote to RESERVED for writing. SQLite
detects this instantly and returns SQLITE_BUSY, bypassing busy_timeout.
_txlock=immediate fixes this by acquiring the write lock at BEGIN time.
The second concurrent transaction waits (up to busy_timeout) instead of
deadlocking. Combined with WAL mode (concurrent readers + single writer),
this handles concurrency correctly without restricting the Go connection
pool.
SameSite=Strict prevents the browser from sending the HttpOnly refresh
token cookie in cross-origin contexts like the Electron desktop app,
where the page runs on localhost but the API is on a remote host. This
caused sessions to expire quickly because refresh requests never
included the cookie.
SameSite=None allows cross-origin sending while HttpOnly still prevents
JavaScript from reading the cookie value (XSS protection).
Resolves#2309
Configure SQLite connections with WAL journal mode, a 5-second busy
timeout, shared cache, and a max of 1 open connection. SQLite only
supports a single writer at a time, so without these settings concurrent
API requests (e.g. bulk task creation) would immediately fail with
"database is locked" instead of waiting and retrying.
calculateNewPositionForTask only checked for lowestPosition == 0 before
triggering a full position recalculation. Extremely small position
values (e.g. 3.16e-285) passed this check, causing new tasks to get
meaningless positions via the index * 2^16 fallback, breaking sort
order.
Use the same < MinPositionSpacing threshold that
createPositionsForTasksInView and the position update handler already
use.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When creating a new view without specifying a position, it defaulted to
0, causing it to always sort before all other views. Apply
calculateDefaultPosition to assign a unique position based on the view
ID, consistent with how projects, tasks, and buckets handle this.
Fixesgo-vikunja/vikunja#2319
The go-datemath lexer panics with "scanner internal error" when given
certain malformed inputs like "no" (it starts recognizing "now" but
hits EOF). Wrap datemath.Parse in a recover so the panic becomes a
regular error, allowing the fallback date parser to handle it gracefully.
Closesgo-vikunja/vikunja#2307
- Add httpCodeGetter interface to handle ValidationHTTPError in test helper
- Add test case for password too short in password reset
- Add test case for password too short in password update
- Fix existing test data to use valid 8+ char passwords
Add bcrypt_password validation to password reset and update endpoints:
- Add validation tag to PasswordReset.NewPassword struct field
- Add validation tag to UserPassword.NewPassword struct field
- Add c.Validate() calls in both handlers
- Fix off-by-one error in bcrypt_password validator (use <= 72 not < 72)
Password requirements: min 8 chars, max 72 bytes (bcrypt limit)
Replace io.LimitReader with a new readZipEntry helper that reads one extra
byte to detect when content exceeds maxZipEntrySize (500MB). This prevents
silent data corruption where partial file bytes would be stored as if the
upload succeeded.
The import now fails with ErrFileTooLarge instead of accepting truncated
content for attachments and background blobs.
Move archive validation (migration file existence and slice bounds
check) before the database wipe. Previously a malformed archive
would first destroy the database and then panic, leaving the
instance in an irrecoverable state with total data loss.
Now the migration data is fully parsed and validated before any
destructive operations occur.
Check that database entries in the zip have a .json suffix and a
non-empty base name before slicing the extension off. This prevents
a panic from index-out-of-range when the filename is too short.
Also use TrimPrefix instead of ReplaceAll for correctness.
Use filepath.Base() on the config file name from the zip archive
before passing it to os.OpenFile, ensuring the config file is
always written to the current directory regardless of what path
the zip entry claims to have.
Validate all zip entry names during restore to reject entries
containing directory traversal sequences (e.g. ../../../pwned.txt).
This prevents a Zip Slip attack where a malicious archive could
write files outside the intended extraction directory.
Typesense was an optional external search backend. This commit fully
removes the integration, leaving the database searcher as the only
search implementation.
Changes:
- Delete pkg/models/typesense.go (core integration)
- Delete pkg/cmd/index.go (CLI command for indexing)
- Simplify task search to always use database searcher
- Remove Typesense event listeners for task sync
- Remove TypesenseSync model registration
- Remove Typesense config keys and defaults
- Remove Typesense doctor health check
- Remove Typesense initialization from startup
- Clean up benchmark test
- Add migration to drop typesense_sync table
- Remove golangci-lint suppression for typesense.go
- Remove typesense-go dependency
RegisterSessionCleanupCron opens a transaction via db.NewSession() but
never calls s.Commit(). The deferred s.Close() auto-rolls-back, making
the DELETE a no-op. Add the missing commit.
- user_export.go: Remove defer s.Close() from checkExportRequest since
it returns the session to callers. Callers now own the session
lifecycle with their own defer s.Close(). Close session on all error
paths within checkExportRequest.
- user_delete.go: Close the read session immediately after Find() before
the per-user deletion loop, avoiding a long-lived transaction holding
locks unnecessarily.
- user/delete.go: Remove double s.Close() in notifyUsersScheduledForDeletion
by closing immediately after Find() instead of using both defer and
explicit close.
- caldav_token.go: Return nil token on Commit() error to prevent callers
from using an unpersisted token.
syncUserGroups created its own db.NewSession() internally while being
called from AuthenticateUserInLDAP which already has an active session
with writes. In SQLite shared-cache mode this causes a lock conflict.
Pass the caller's session through instead, and add s.Commit() before
db.AssertExists calls in LDAP tests.
Add a proper main_test.go for the caldav test package that initializes
the logger, config, test database, and event system. Previously, these
were initialized inline in TestSubTask_Create and TestSubTask_Update
relied on running after it (fragile test ordering).
Fix session handling in TestSubTask_Update: close the read session
before calling UpdateResource (which creates its own internal session)
to avoid SQLite lock conflicts from concurrent transactions.
Two categories of fixes:
1. Use defer s.Close() instead of explicit s.Close() to prevent session
leaks when require.FailNow() triggers runtime.Goexit(), which skips
explicit close calls but runs deferred functions. Leaked sessions
hold SQLite write locks that block all subsequent fixture loading.
2. Add s.Commit() before db.AssertExists/db.AssertMissing calls. These
assertion helpers query via the global engine (not the test session),
so they cannot see uncommitted data from the session's transaction.
For block-scoped sessions (kanban_task_bucket_test.go), wrap each block
in an anonymous function so defer runs at block boundary rather than
deferring to the enclosing test function.
files.Create() and files.CreateWithMime() internally create their own
sessions and transactions. When called from within an existing
transaction (now that db.NewSession() auto-begins), this creates nested
transactions that deadlock on SQLite.
Switch to files.CreateWithSession() and files.CreateWithMimeAndSession()
to participate in the caller's existing transaction instead.
In transaction mode, xorm stores the bean argument as a map key in
afterUpdateBeans. Since Task contains slices and maps (unhashable
types), passing a Task value causes "hash of unhashable type" panic.
Passing a pointer (&ot) fixes this since pointers are always hashable.
With db.NewSession() now starting real transactions, all sessions that
do writes must explicitly commit. These listeners and cron jobs were
previously relying on auto-commit mode where each SQL statement was
committed immediately. Without explicit Commit(), the writes are
silently rolled back on Close(), and the held write locks cause
"database is locked" errors for subsequent requests on SQLite.
On Postgres, a failed operation puts the transaction in an error state
where subsequent operations fail. The previous loop with continue would
keep trying to use a broken transaction. Each user now gets its own
transaction so a single notification failure doesn't affect others.
File.Delete() had s.Commit() and s.Rollback() calls that could
prematurely commit or abort an outer transaction when using a shared
session. The caller is now responsible for transaction management.
Verify that deleting a parent project atomically deletes all child
projects, including archived children and deeply nested hierarchies.
Also add missing defer s.Close() to existing delete test cases.