Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.2.3] - 2025-08-22

### Added
- Increased test coverage for the `time_log.py` module from 88% to 100%.


## [1.2.2] - 2025-08-22

### Added
- Your new feature here.
- Increased test coverage for the `manual_entry.py` module from 72% to 98%.


## [1.2.1] - 2025-08-22

### Fixed
- Your new fix here.
- Corrected and updated the release comparison links in `CHANGELOG.md`.


## [1.2.0] - 2025-08-07
Expand Down Expand Up @@ -137,8 +147,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Flexible database configuration (SQLite/PostgreSQL).


[Unreleased]: https://github.com/PPeitsch/TimeTrack/compare/v1.0.1...HEAD
[1.1.1]: https://github.com/PPeitsch/TimeTrack/compare/v1.1.0....v1.1.1

[1.2.3]: https://github.com/PPeitsch/TimeTrack/compare/v1.2.2...v1.2.3
[1.2.2]: https://github.com/PPeitsch/TimeTrack/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/PPeitsch/TimeTrack/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/PPeitsch/TimeTrack/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/PPeitsch/TimeTrack/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/PPeitsch/TimeTrack/compare/v1.0.9...v1.1.0
[1.0.9]: https://github.com/PPeitsch/TimeTrack/compare/v1.0.8...v1.0.9
[1.0.8]: https://github.com/PPeitsch/TimeTrack/compare/v1.0.7...v1.0.8
Expand Down
2 changes: 0 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@ requests==2.32.3
psycopg2-binary==2.9.10
python-dotenv==1.0.1
beautifulsoup4==4.13.0b2
black==24.10.0
isort==6.0.0b2
261 changes: 260 additions & 1 deletion tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import json
import unittest
from datetime import date, datetime
from unittest.mock import patch

from flask import jsonify

from app.db.database import db
from app.models.models import Employee, ScheduleEntry
from app.models.models import Employee, ScheduleEntry, Holiday


class TestRoutes(unittest.TestCase):
Expand All @@ -19,6 +22,12 @@ class TestConfig(Config):
self.app = create_app(TestConfig)
self.client = self.app.test_client()

from werkzeug.exceptions import BadRequest

@self.app.errorhandler(BadRequest)
def handle_bad_request(e):
return jsonify(error="Bad Request"), 400

with self.app.app_context():
db.create_all()
# Create a default employee
Expand Down Expand Up @@ -88,6 +97,153 @@ def test_manual_entry_route_post_invalid(self):
data = json.loads(response.data)
self.assertIn("error", data)

def test_manual_entry_post_bad_json(self):
response = self.client.post("/entry", data="{", content_type="application/json")
self.assertEqual(response.status_code, 400)
data = json.loads(response.data)
self.assertEqual(data["error"], "Bad Request")

def test_manual_entry_post_missing_fields(self):
# Test missing date
response = self.client.post(
"/entry",
data=json.dumps({"employee_id": 1}),
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
self.assertEqual(json.loads(response.data)["error"], "Date is required")

# Test missing employee_id
response = self.client.post(
"/entry",
data=json.dumps(
{"date": "2025-03-16", "entries": [{"entry": "09:00", "exit": "17:00"}]}
),
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
self.assertEqual(json.loads(response.data)["error"], "Employee ID is required")

# Test missing entries for work day
response = self.client.post(
"/entry",
data=json.dumps(
{"date": "2025-03-16", "employee_id": 1, "absence_code": None}
),
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
json.loads(response.data)["error"], "Entries are required for work day"
)

def test_manual_entry_post_invalid_date(self):
entry_data = {"date": "invalid-date", "employee_id": 1}
response = self.client.post(
"/entry", data=json.dumps(entry_data), content_type="application/json"
)
self.assertEqual(response.status_code, 400)
self.assertEqual(json.loads(response.data)["error"], "Invalid date format")

def test_manual_entry_post_empty_entries(self):
entry_data = {
"date": "2025-03-16",
"employee_id": 1,
"entries": [],
"absence_code": None,
}
response = self.client.post(
"/entry", data=json.dumps(entry_data), content_type="application/json"
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
json.loads(response.data)["error"], "No time entries provided for work day"
)

def test_manual_entry_update_existing(self):
# First, create an entry
entry_data = {
"date": "2025-03-18",
"employee_id": 1,
"entries": [{"entry": "09:00", "exit": "12:00"}],
"absence_code": None,
}
self.client.post(
"/entry", data=json.dumps(entry_data), content_type="application/json"
)

# Now, update it
update_data = {
"date": "2025-03-18",
"employee_id": 1,
"entries": [{"entry": "09:00", "exit": "13:00"}], # Changed exit time
"absence_code": None,
}
response = self.client.post(
"/entry", data=json.dumps(update_data), content_type="application/json"
)
self.assertEqual(response.status_code, 200)

with self.app.app_context():
entry = ScheduleEntry.query.filter_by(
date=datetime.strptime("2025-03-18", "%Y-%m-%d").date()
).first()
self.assertEqual(len(entry.entries), 1)
self.assertEqual(entry.entries[0]["exit"], "13:00")

def test_get_entry_not_found(self):
response = self.client.get("/entry/2025-01-01")
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.data), {})

def test_get_entry_found(self):
# Create an entry to find
entry_date = "2025-03-19"
entry_data = {
"date": entry_date,
"employee_id": 1,
"entries": [{"entry": "10:00", "exit": "18:00"}],
"absence_code": None,
}
self.client.post(
"/entry", data=json.dumps(entry_data), content_type="application/json"
)

response = self.client.get(f"/entry/{entry_date}")
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(len(data["entries"]), 1)
self.assertEqual(data["entries"][0]["entry"], "10:00")
self.assertEqual(data["hours"], 8.0)

def test_get_entry_invalid_date(self):
response = self.client.get("/entry/invalid-date")
self.assertEqual(response.status_code, 400)
self.assertEqual(json.loads(response.data)["error"], "Invalid date format")

def test_manual_entry_post_absence(self):
entry_data = {
"date": "2025-03-20",
"employee_id": 1,
"entries": [],
"absence_code": "VAC",
}
response = self.client.post(
"/entry", data=json.dumps(entry_data), content_type="application/json"
)
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data["status"], "success")
self.assertNotIn("hours", data) # No hours for absence

with self.app.app_context():
entry = ScheduleEntry.query.filter_by(
date=datetime.strptime("2025-03-20", "%Y-%m-%d").date()
).first()
self.assertIsNotNone(entry)
self.assertEqual(entry.absence_code, "VAC")
self.assertEqual(entry.entries, [])

def test_time_summary_route(self):
# Test the time summary route
response = self.client.get("/summary/")
Expand Down Expand Up @@ -154,6 +310,71 @@ def test_monthly_summary_route(self):
self.assertIn("required", data)
self.assertIn("difference", data)

# More specific check for required hours
# March 2025 has 21 working days. 21 * 8 = 168.
# We added one absence day, so required hours should be 160.
self.assertEqual(data["required"], 160.0)
self.assertEqual(data["total"], 8.0)

def test_daily_summary_for_absence(self):
# Create an absence entry
with self.app.app_context():
entry_date = date(2025, 4, 1) # A Tuesday
absence_entry = ScheduleEntry(
employee_id=1, date=entry_date, entries=[], absence_code="SICK"
)
db.session.add(absence_entry)
db.session.commit()

response = self.client.get("/summary/daily/2025-04-01")
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data["type"], "SICK")
self.assertEqual(data["hours"], 0)
self.assertEqual(data["required"], 0)

def test_daily_summary_for_holiday(self):
# Create a holiday
with self.app.app_context():
holiday_date = date(2025, 5, 1) # A Thursday
holiday = Holiday(date=holiday_date, description="Labor Day")
db.session.add(holiday)
db.session.commit()

response = self.client.get("/summary/daily/2025-05-01")
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data["type"], "Holiday")
self.assertEqual(data["hours"], 0)
self.assertEqual(data["required"], 0)

def test_daily_summary_for_weekend(self):
# A known weekend date
response = self.client.get("/summary/daily/2025-03-16") # A Sunday
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data["type"], "Weekend")
self.assertEqual(data["hours"], 0)
self.assertEqual(data["required"], 0)

def test_daily_summary_exception(self):
with self.app.app_context():
with patch("app.routes.time_summary.ScheduleEntry.query") as mock_query:
mock_query.filter_by.side_effect = Exception("DB Error")
response = self.client.get("/summary/daily/2025-01-01")
self.assertEqual(response.status_code, 500)
data = json.loads(response.data)
self.assertEqual(data["error"], "DB Error")

@patch("app.routes.time_summary.date")
def test_monthly_summary_exception(self, mock_date):
with self.app.app_context():
mock_date.side_effect = Exception("Date Error")
response = self.client.get("/summary/monthly/2025/1")
self.assertEqual(response.status_code, 500)
data = json.loads(response.data)
self.assertEqual(data["error"], "Date Error")

def test_monthly_logs_route(self):
# Create a test entry first
with self.app.app_context():
Expand Down Expand Up @@ -183,6 +404,44 @@ def test_monthly_logs_route(self):
self.assertIn("entries", entry)
self.assertIn("total_hours", entry)

def test_monthly_logs_with_absence(self):
# Create an absence entry
with self.app.app_context():
entry_date = date(2025, 4, 15)
absence_entry = ScheduleEntry(
employee_id=1, date=entry_date, entries=[], absence_code="VAC"
)
db.session.add(absence_entry)
db.session.commit()

# Test getting monthly logs for the month with the absence
response = self.client.get("/logs/monthly/2025/4")
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)

# Find the absence entry in the response
absence_found = False
for entry in data:
if entry["date"] == "2025-04-15":
self.assertEqual(entry["type"], "VAC")
self.assertEqual(entry["total_hours"], 0)
absence_found = True
break
self.assertTrue(absence_found, "Absence entry not found in response")

def test_monthly_logs_exception(self):
with self.app.app_context():
with patch("app.routes.time_log.ScheduleEntry.query") as mock_query:
# Configure the mock to raise an exception when used
mock_query.filter.side_effect = Exception("Database connection failed")

# Test that the exception is handled and a 500 error is returned
response = self.client.get("/logs/monthly/2025/5")
self.assertEqual(response.status_code, 500)
data = json.loads(response.data)
self.assertIn("error", data)
self.assertEqual(data["error"], "Database connection failed")


if __name__ == "__main__":
unittest.main()
Loading