feat(server): ghost-session guard — JWTs for deleted users stop authenticating
The JWT middleware validated signature + expiry and fabricated the request user purely from token claims, so a signature-valid token for a DELETED user kept full API access until token expiry — observed live on hardware (a phone session survived the test suite's user-table recreation and kept serving its true empty sets; open handoff bug 1). An ensureUserExistsMiddleware already existed but was only wired on the frontend SSR group (frontend.go) — every /api group used bare createJWTMiddleware. The existence check now lives INSIDE createJWTMiddleware's SuccessHandler so every protected group (SSR and API alike) gets it: - After claim validation, GetUser by the token's user_id; a missing row clears the token cookie and answers 401 user_not_found for API calls (the app's TokenAuthenticator then re-logins with stored credentials) or redirects to /login?session=invalid for page requests — same shape-aware split the error handler already uses. - Cost: one primary-key lookup per authenticated request (~0.2ms local); instant revocation, no TTL semantics. The redundant SSR-only middleware is deleted (its redirect behavior is preserved inside the guard). The guard exposed two processing-issues tests that were unknowingly RELYING on ghost semantics: they nested a second setupTestServer whose bootstrap deleted the first setup's regular user, then asserted 403 from the dead token. De-nested: a createLibraryDirect helper inserts the library under the live setup's user, no second bootstrap. New TestDeletedUserTokenRejected pins the contract: token works while the user exists -> 401 user_not_found after deletion. Full suite: 219 groups, zero failures.
This commit is contained in:
1 parent
4f05a232fe
commit
9cd6356892
5 files changed
+125
-57
No files matched your search
@@ -16,8 +16,7 @@ func TestProcessingIssuesListAuthentication(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
|
||||
// Create a test library
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test Auth Library", "manga")
|
||||
libraryID := createLibraryDirect(t, setup.DB, getTestUserID(t, setup.DB), "Test Auth Library", "manga")
|
||||
|
||||
t.Run("GET /api/libraries/:id/issues/list - No authentication", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
|
||||
@@ -55,8 +54,7 @@ func TestProcessingIssuesListResponseStructure(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
|
||||
// Create a test library
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test Structure Library", "comics")
|
||||
libraryID := createLibraryDirect(t, setup.DB, getTestUserID(t, setup.DB), "Test Structure Library", "comics")
|
||||
|
||||
t.Run("Returns JSON array for empty library", func(t *testing.T) {
|
||||
token := setup.Token
|
||||
@@ -163,8 +161,7 @@ func TestProcessingIssueStatsAuthentication(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
|
||||
// Create a test library
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test Stats Auth Library", "manga")
|
||||
libraryID := createLibraryDirect(t, setup.DB, getTestUserID(t, setup.DB), "Test Stats Auth Library", "manga")
|
||||
|
||||
t.Run("GET /api/libraries/:id/issues/stats - No authentication", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
|
||||
@@ -202,8 +199,7 @@ func TestProcessingIssueStatsResponseStructure(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
|
||||
// Create a test library
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test Stats Structure Library", "comics")
|
||||
libraryID := createLibraryDirect(t, setup.DB, getTestUserID(t, setup.DB), "Test Stats Structure Library", "comics")
|
||||
|
||||
t.Run("Returns correct structure for empty library", func(t *testing.T) {
|
||||
token := setup.Token
|
||||
@@ -343,11 +339,9 @@ func TestProcessingIssuesCrossLibraryIsolation(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
|
||||
// Create two different libraries with different types
|
||||
deviceSetup1 := setupDeviceTest(t)
|
||||
library1ID := deviceSetup1.CreateLibrary(t, "Isolation Test Library 1", "manga")
|
||||
library1ID := createLibraryDirect(t, setup.DB, getTestUserID(t, setup.DB), "Isolation Test Library 1", "manga")
|
||||
|
||||
deviceSetup2 := setupDeviceTest(t)
|
||||
library2ID := deviceSetup2.CreateLibrary(t, "Isolation Test Library 2", "comics")
|
||||
library2ID := createLibraryDirect(t, setup.DB, getTestUserID(t, setup.DB), "Isolation Test Library 2", "comics")
|
||||
|
||||
token := setup.Token
|
||||
|
||||
@@ -431,8 +425,7 @@ func TestProcessingIssuesDifferentLibraryTypes(t *testing.T) {
|
||||
|
||||
for _, lt := range libraryTypes {
|
||||
t.Run(lt.name+" - list endpoint works", func(t *testing.T) {
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test "+lt.name, lt.libraryType)
|
||||
libraryID := createLibraryDirect(t, setup.DB, getTestUserID(t, setup.DB), "Test "+lt.name, lt.libraryType)
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
@@ -448,8 +441,7 @@ func TestProcessingIssuesDifferentLibraryTypes(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run(lt.name+" - stats endpoint works", func(t *testing.T) {
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test "+lt.name, lt.libraryType)
|
||||
libraryID := createLibraryDirect(t, setup.DB, getTestUserID(t, setup.DB), "Test "+lt.name, lt.libraryType)
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
@@ -474,8 +466,7 @@ func TestProcessingIssuesInvalidTokens(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
|
||||
// Create a test library
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test Token Library", "ebooks")
|
||||
libraryID := createLibraryDirect(t, setup.DB, getTestUserID(t, setup.DB), "Test Token Library", "ebooks")
|
||||
|
||||
t.Run("List endpoint rejects invalid token", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
|
||||
|
||||
Reference in new issue
Block a user