package handler import ( "net/http/httptest" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/internal/service" ) func TestLogoutRevokesOnlyCurrentSession(t *testing.T) { mock := contextMock(t) expectContextPrincipal(mock, "warehouse_admin") mock.ExpectExec("INSERT INTO revoked_tokens").WithArgs(sqlmock.AnyArg(), int64(7), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(0, 1)) auth := service.NewAuthService() user := &model.User{ID: 7, Username: "session-user"} first, err := auth.GenerateToken(user) if err != nil { t.Fatal(err) } second, err := auth.GenerateToken(user) if err != nil { t.Fatal(err) } principal, err := auth.ValidateToken(first) if err != nil { t.Fatal(err) } tickets := service.NewWebSocketTicketService() ticket, err := tickets.Issue(7, 2, first) if err != nil { t.Fatal(err) } hub := newHub(newFakeVerifier()) go hub.run() client := testClient(hub, 7, 2) client.sessionID = service.TokenSessionID(first) client.sessionExpiresAt = principal.Exp hub.register <- client hub.BroadcastStockUpdate(2, "inbound", 1, nil) expectMessage(t, client, time.Second) router := gin.New() NewHandler().RegisterRoutes(router) request := httptest.NewRequest("POST", "/api/auth/logout", nil) request.Header.Set("Authorization", "Bearer "+first) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != 200 { t.Fatalf("logout: %d %s", recorder.Code, recorder.Body.String()) } if _, err := auth.ValidateToken(first); err == nil { t.Fatal("logged-out session still authenticates") } if _, err := auth.ValidateToken(second); err != nil { t.Fatalf("other session revoked: %v", err) } if _, err := tickets.Consume(ticket.Ticket); err == nil { t.Fatal("pending ticket survived logout") } hub.BroadcastStockUpdate(2, "inbound", 2, nil) expectClosed(t, client, time.Second) // A process restart reloads persisted fingerprints, retaining revocation. mock.ExpectQuery("SELECT jti, expires_at FROM revoked_tokens").WithArgs(sqlmock.AnyArg()).WillReturnRows(sqlmock.NewRows([]string{"jti", "expires_at"})) if err := service.LoadRevokedSessions(); err != nil { t.Fatal(err) } if _, err := auth.ValidateToken(first); err != nil { t.Fatalf("restart simulation did not clear cache: %v", err) } mock.ExpectQuery("SELECT jti, expires_at FROM revoked_tokens").WithArgs(sqlmock.AnyArg()).WillReturnRows(sqlmock.NewRows([]string{"jti", "expires_at"}).AddRow(service.TokenSessionID(first), time.Unix(principal.Exp, 0))) if err := service.LoadRevokedSessions(); err != nil { t.Fatal(err) } if _, err := auth.ValidateToken(first); err == nil { t.Fatal("session reactivated after reload") } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }