package httpapi import ( "bytes" "context" "io" "log/slog" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "calllinesystem/server/internal/config" "calllinesystem/server/internal/database" "calllinesystem/server/internal/model" "calllinesystem/server/internal/security" "github.com/google/uuid" ) func TestDisablingStaffRevokesSessionsAndBlocksLoginPostgresIntegration(t *testing.T) { dsn := strings.TrimSpace(os.Getenv("TEST_DATABASE_URL")) if dsn == "" { t.Skip("TEST_DATABASE_URL is not set") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) db, err := database.Open(ctx, dsn, logger) if err != nil { t.Fatal(err) } defer database.Close(db) sqlDB, err := database.SQLDB(db) if err != nil { t.Fatal(err) } if err := database.Migrate(ctx, sqlDB, logger); err != nil { t.Fatal(err) } server, err := New(db, config.Config{ EncryptionKey: bytes.Repeat([]byte{0x61}, 32), PhoneHMACKey: bytes.Repeat([]byte{0x62}, 32), SessionCookieName: "queue_session", SessionTTL: time.Hour, }, logger) if err != nil { t.Fatal(err) } now := time.Date(2026, 7, 30, 10, 0, 0, 0, time.UTC) server.now = func() time.Time { return now } password := "StaffPassword123!" passwordHash, err := security.HashPassword(password) if err != nil { t.Fatal(err) } admin := model.User{ ID: uuid.NewString(), Username: "auth_admin_" + uuid.NewString()[:8], PasswordHash: passwordHash, Role: model.RoleAdmin, Active: true, CreatedAt: now, UpdatedAt: now, } staff := model.User{ ID: uuid.NewString(), Username: "auth_staff_" + uuid.NewString()[:8], PasswordHash: passwordHash, Role: model.RoleStaff, Active: true, CreatedAt: now, UpdatedAt: now, } if err := db.Create(&admin).Error; err != nil { t.Fatal(err) } if err := db.Create(&staff).Error; err != nil { t.Fatal(err) } login := func() *httptest.ResponseRecorder { recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodPost, "/api/staff/auth/login", strings.NewReader(`{"username":"`+staff.Username+`","password":"`+password+`"}`)) server.Handler().ServeHTTP(recorder, request) return recorder } firstLogin := login() if firstLogin.Code != http.StatusOK { t.Fatalf("initial login status = %d, want 200; body = %s", firstLogin.Code, firstLogin.Body.String()) } var staffCookie *http.Cookie for _, cookie := range firstLogin.Result().Cookies() { if cookie.Name == server.authCookieName(model.RoleStaff) { staffCookie = cookie break } } if staffCookie == nil { t.Fatal("initial login did not set the staff session cookie") } updateRequest := httptest.NewRequest(http.MethodPut, "/api/admin/users/"+staff.ID, strings.NewReader(`{"role":"STAFF","active":false,"project_ids":[]}`)) updateRequest.SetPathValue("id", staff.ID) updateRequest = updateRequest.WithContext(context.WithValue(updateRequest.Context(), principalKey, principal{User: admin})) updateRecorder := httptest.NewRecorder() server.updateAdminUser(updateRecorder, updateRequest) if updateRecorder.Code != http.StatusOK { t.Fatalf("disable status = %d, want 200; body = %s", updateRecorder.Code, updateRecorder.Body.String()) } var disabledStaff model.User if err := db.First(&disabledStaff, "id = ?", staff.ID).Error; err != nil { t.Fatal(err) } if disabledStaff.Active { t.Error("staff remains active after disabling") } var activeSessions int64 if err := db.Model(&model.AuthSession{}). Where("user_id = ? AND revoked_at IS NULL", staff.ID). Count(&activeSessions).Error; err != nil { t.Fatal(err) } if activeSessions != 0 { t.Errorf("active sessions after disabling staff = %d, want 0", activeSessions) } meRecorder := httptest.NewRecorder() meRequest := httptest.NewRequest(http.MethodGet, "/api/staff/auth/me", nil) meRequest.AddCookie(staffCookie) server.Handler().ServeHTTP(meRecorder, meRequest) if meRecorder.Code != http.StatusUnauthorized { t.Errorf("existing session after disabling status = %d, want 401; body = %s", meRecorder.Code, meRecorder.Body.String()) } secondLogin := login() if secondLogin.Code != http.StatusUnauthorized { t.Fatalf("login after disabling status = %d, want 401; body = %s", secondLogin.Code, secondLogin.Body.String()) } }