Files
TimeTracker/tests/test_activity_feed.py
Dries Peeters 90dde470da style: standardize code formatting and normalize line endings
- Normalize line endings from CRLF to LF across all files to match .editorconfig
- Standardize quote style from single quotes to double quotes
- Normalize whitespace and formatting throughout codebase
- Apply consistent code style across 372 files including:
  * Application code (models, routes, services, utils)
  * Test files
  * Configuration files
  * CI/CD workflows

This ensures consistency with the project's .editorconfig settings and
improves code maintainability.
2025-11-28 20:05:37 +01:00

392 lines
15 KiB
Python

"""Tests for Activity Feed functionality"""
import pytest
from datetime import datetime, timedelta
from app.models import Activity, User, Project, Task, TimeEntry, Client
from app import db
class TestActivityModel:
"""Tests for the Activity model"""
def test_activity_creation(self, app, test_user, test_project):
"""Test creating an activity log entry"""
with app.app_context():
activity = Activity(
user_id=test_user.id,
action="created",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description=f'Created project "{test_project.name}"',
)
db.session.add(activity)
db.session.commit()
assert activity.id is not None
assert activity.user_id == test_user.id
assert activity.action == "created"
assert activity.entity_type == "project"
assert activity.entity_id == test_project.id
assert activity.created_at is not None
def test_activity_log_method(self, app, test_user, test_project):
"""Test the Activity.log() class method"""
with app.app_context():
Activity.log(
user_id=test_user.id,
action="updated",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description=f'Updated project "{test_project.name}"',
extra_data={"field": "name"},
)
activity = Activity.query.filter_by(
user_id=test_user.id, entity_type="project", entity_id=test_project.id
).first()
assert activity is not None
assert activity.action == "updated"
assert activity.extra_data == {"field": "name"}
def test_activity_get_recent(self, app, test_user, test_project):
"""Test getting recent activities"""
with app.app_context():
# Create multiple activities
for i in range(5):
Activity.log(
user_id=test_user.id,
action="updated",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description=f"Action {i}",
)
# Get recent activities
activities = Activity.get_recent(user_id=test_user.id, limit=3)
assert len(activities) == 3
assert activities[0].description == "Action 4" # Most recent first
def test_activity_filter_by_entity_type(self, app, test_user, test_project, test_task):
"""Test filtering activities by entity type"""
with app.app_context():
# Create activities for different entity types
Activity.log(
user_id=test_user.id,
action="created",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description="Project created",
)
Activity.log(
user_id=test_user.id,
action="created",
entity_type="task",
entity_id=test_task.id,
entity_name=test_task.name,
description="Task created",
)
# Filter by entity type
project_activities = Activity.get_recent(user_id=test_user.id, entity_type="project")
task_activities = Activity.get_recent(user_id=test_user.id, entity_type="task")
assert len(project_activities) == 1
assert project_activities[0].entity_type == "project"
assert len(task_activities) == 1
assert task_activities[0].entity_type == "task"
def test_activity_to_dict(self, app, test_user, test_project):
"""Test converting activity to dictionary"""
with app.app_context():
Activity.log(
user_id=test_user.id,
action="created",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description="Test activity",
)
activity = Activity.query.filter_by(user_id=test_user.id).first()
activity_dict = activity.to_dict()
assert activity_dict["id"] == activity.id
assert activity_dict["user_id"] == test_user.id
assert activity_dict["action"] == "created"
assert activity_dict["entity_type"] == "project"
assert activity_dict["entity_id"] == test_project.id
assert activity_dict["description"] == "Test activity"
assert "created_at" in activity_dict
def test_activity_get_icon(self, app, test_user, test_project):
"""Test getting icon for different activity types"""
with app.app_context():
actions = ["created", "updated", "deleted", "started", "stopped"]
for action in actions:
Activity.log(
user_id=test_user.id,
action=action,
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description=f"{action} project",
)
activity = Activity.query.filter_by(action=action).first()
icon = activity.get_icon()
assert icon is not None
assert "fas fa-" in icon
class TestActivityAPIEndpoints:
"""Tests for Activity Feed API endpoints"""
def test_get_activities(self, authenticated_client, test_user, test_project):
"""Test GET /api/activities endpoint"""
# Create some test activities
with authenticated_client.application.app_context():
for i in range(3):
Activity.log(
user_id=test_user.id,
action="updated",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description=f"Activity {i}",
)
response = authenticated_client.get("/api/activities")
assert response.status_code == 200
data = response.get_json()
assert "activities" in data
assert len(data["activities"]) >= 3
assert "total" in data
assert "pages" in data
def test_get_activities_with_entity_type_filter(self, authenticated_client, test_user, test_project, test_task):
"""Test filtering activities by entity type"""
with authenticated_client.application.app_context():
Activity.log(
user_id=test_user.id,
action="created",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description="Project activity",
)
Activity.log(
user_id=test_user.id,
action="created",
entity_type="task",
entity_id=test_task.id,
entity_name=test_task.name,
description="Task activity",
)
# Filter by project entity type
response = authenticated_client.get("/api/activities?entity_type=project")
assert response.status_code == 200
data = response.get_json()
assert all(act["entity_type"] == "project" for act in data["activities"])
def test_get_activities_with_pagination(self, authenticated_client, test_user, test_project):
"""Test pagination of activities"""
with authenticated_client.application.app_context():
# Create 15 activities
for i in range(15):
Activity.log(
user_id=test_user.id,
action="updated",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description=f"Activity {i}",
)
# Get first page
response = authenticated_client.get("/api/activities?limit=5&page=1")
assert response.status_code == 200
data = response.get_json()
assert len(data["activities"]) == 5
assert data["has_next"] is True
# Get second page
response = authenticated_client.get("/api/activities?limit=5&page=2")
assert response.status_code == 200
data = response.get_json()
assert len(data["activities"]) == 5
def test_get_activity_stats(self, authenticated_client, test_user, test_project, test_task):
"""Test GET /api/activities/stats endpoint"""
with authenticated_client.application.app_context():
# Create varied activities
Activity.log(
user_id=test_user.id,
action="created",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description="Project created",
)
Activity.log(
user_id=test_user.id,
action="updated",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description="Project updated",
)
Activity.log(
user_id=test_user.id,
action="created",
entity_type="task",
entity_id=test_task.id,
entity_name=test_task.name,
description="Task created",
)
response = authenticated_client.get("/api/activities/stats")
assert response.status_code == 200
data = response.get_json()
assert "total_activities" in data
assert "entity_counts" in data
assert "action_counts" in data
assert data["total_activities"] >= 3
class TestActivityIntegration:
"""Tests for activity logging integration in routes"""
def test_project_create_logs_activity(self, admin_authenticated_client, test_client):
"""Test that creating a project logs an activity"""
with admin_authenticated_client.application.app_context():
# Count activities before
before_count = Activity.query.count()
response = admin_authenticated_client.post(
"/projects/create",
data={
"name": "Test Activity Project",
"client_id": test_client.id,
"billable": "on",
"description": "Test project for activity",
},
follow_redirects=False,
)
with admin_authenticated_client.application.app_context():
# Check activity was logged
after_count = Activity.query.count()
assert after_count == before_count + 1
activity = Activity.query.order_by(Activity.created_at.desc()).first()
assert activity.action == "created"
assert activity.entity_type == "project"
assert "Test Activity Project" in activity.description
def test_task_create_logs_activity(self, authenticated_client, test_project):
"""Test that creating a task logs an activity"""
with authenticated_client.application.app_context():
before_count = Activity.query.count()
response = authenticated_client.post(
"/tasks/create",
data={
"project_id": test_project.id,
"name": "Test Activity Task",
"priority": "high",
"description": "Test task for activity",
},
follow_redirects=False,
)
with authenticated_client.application.app_context():
after_count = Activity.query.count()
assert after_count == before_count + 1
activity = Activity.query.order_by(Activity.created_at.desc()).first()
assert activity.action == "created"
assert activity.entity_type == "task"
assert "Test Activity Task" in activity.description
def test_timer_start_logs_activity(self, authenticated_client, test_project):
"""Test that starting a timer logs an activity"""
with authenticated_client.application.app_context():
before_count = Activity.query.count()
response = authenticated_client.post(
"/timer/start", data={"project_id": test_project.id, "notes": "Test timer"}, follow_redirects=False
)
with authenticated_client.application.app_context():
after_count = Activity.query.count()
assert after_count == before_count + 1
activity = Activity.query.order_by(Activity.created_at.desc()).first()
assert activity.action == "started"
assert activity.entity_type == "time_entry"
assert test_project.name in activity.description
def test_timer_stop_logs_activity(self, authenticated_client, test_user, test_project):
"""Test that stopping a timer logs an activity"""
with authenticated_client.application.app_context():
# Create an active timer
from app.models.time_entry import local_now
timer = TimeEntry(user_id=test_user.id, project_id=test_project.id, start_time=local_now(), source="auto")
db.session.add(timer)
db.session.commit()
before_count = Activity.query.count()
response = authenticated_client.post("/timer/stop", follow_redirects=False)
with authenticated_client.application.app_context():
after_count = Activity.query.count()
assert after_count == before_count + 1
activity = Activity.query.order_by(Activity.created_at.desc()).first()
assert activity.action == "stopped"
assert activity.entity_type == "time_entry"
assert test_project.name in activity.description
class TestActivityWidget:
"""Tests for the activity feed widget on dashboard"""
def test_dashboard_includes_activities(self, authenticated_client, test_user, test_project):
"""Test that the dashboard includes recent activities"""
with authenticated_client.application.app_context():
# Create some activities
Activity.log(
user_id=test_user.id,
action="created",
entity_type="project",
entity_id=test_project.id,
entity_name=test_project.name,
description="Test activity",
)
response = authenticated_client.get("/dashboard")
assert response.status_code == 200
assert b"Recent Activity" in response.data
assert b"Test activity" in response.data