Unit Tests / test (push) Successful in 16s
## Summary - Adds `UserProfile` model with mutually exclusive **Employee** / **Client** types - Replaces auto-Employee signal with auto-Client profile on user creation - Data migration: users with time log entries → Employee; others → Client (orphan Employee rows removed) - Admin UI at `/financial/manage_users` to set any user's type; profile page shows current type - **Employees** can log time; **Clients** get read-only access to reports and time logs - Time logs, reports, and dashboard filter to employees only - 14 new tests covering signals, type switching, access control, and filtering ## Design decisions (from issue Q&A) 1. Client login = read-only financial access (reports + time logs, no edit/log time) 2. Employee and Client are strictly mutually exclusive 3. Admins (superusers) can change type via Manage Users 4. Bulk migration applied for existing users ## Test plan - [x] `python manage.py test financial.tests` (14 tests pass) - [x] `python manage.py test public.tests` (21 tests pass) - [ ] Run migration on staging: `python manage.py migrate` - [ ] Verify admin can set user types at `/financial/manage_users` - [ ] Verify employee can log time at `/financial/timekeeping` - [ ] Verify client sees reports/time logs read-only, cannot log time - [ ] Verify employee filter dropdown excludes clients Closes #14 Reviewed-on: #15
160 lines
6.9 KiB
Python
160 lines
6.9 KiB
Python
from django.contrib.auth.models import User
|
|
from django.test import Client, TestCase
|
|
from django.urls import reverse
|
|
|
|
from financial.models import (
|
|
AddressModel,
|
|
ChargeNumber,
|
|
Contract,
|
|
Employee,
|
|
TimeCard,
|
|
TimeCardCell,
|
|
UserProfile,
|
|
set_user_type,
|
|
)
|
|
from financial.permissions import get_employees, is_client_user, is_employee_user
|
|
|
|
|
|
class UserProfileSignalTests(TestCase):
|
|
def test_new_user_gets_client_profile_not_employee(self):
|
|
user = User.objects.create_user(username="newbie", password="pass")
|
|
self.assertTrue(UserProfile.objects.filter(user=user, user_type=UserProfile.UserType.CLIENT).exists())
|
|
self.assertFalse(Employee.objects.filter(user=user).exists())
|
|
|
|
|
|
class SetUserTypeTests(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(username="worker", password="pass")
|
|
UserProfile.objects.filter(user=self.user).delete()
|
|
|
|
def test_set_employee_creates_employee_record(self):
|
|
set_user_type(self.user, UserProfile.UserType.EMPLOYEE)
|
|
self.assertTrue(Employee.objects.filter(user=self.user).exists())
|
|
self.assertEqual(self.user.profile.user_type, UserProfile.UserType.EMPLOYEE)
|
|
|
|
def test_set_client_removes_employee_without_time_entries(self):
|
|
set_user_type(self.user, UserProfile.UserType.EMPLOYEE)
|
|
set_user_type(self.user, UserProfile.UserType.CLIENT)
|
|
self.assertFalse(Employee.objects.filter(user=self.user).exists())
|
|
self.assertEqual(
|
|
UserProfile.objects.get(user=self.user).user_type,
|
|
UserProfile.UserType.CLIENT,
|
|
)
|
|
|
|
def test_cannot_set_client_with_time_entries(self):
|
|
set_user_type(self.user, UserProfile.UserType.EMPLOYEE)
|
|
employee = Employee.objects.get(user=self.user)
|
|
contract = Contract.objects.create(
|
|
contract_type=Contract.ContractTypeEnum.FIRM_FIX_PRICED,
|
|
name="Test Contract",
|
|
)
|
|
charge = ChargeNumber.objects.create(
|
|
charge_number_type=ChargeNumber.ChargeNumberTypeEnum.LEVEL_OF_EFFORT,
|
|
contract=contract,
|
|
)
|
|
time_card = TimeCard.objects.create(employee=employee)
|
|
TimeCardCell.objects.create(timeCard=time_card, charge_number=charge, hour=2.0)
|
|
|
|
with self.assertRaises(ValueError):
|
|
set_user_type(self.user, UserProfile.UserType.CLIENT)
|
|
|
|
|
|
class EmployeeFilterTests(TestCase):
|
|
def setUp(self):
|
|
self.employee_user = User.objects.create_user(username="emp", password="pass")
|
|
self.client_user = User.objects.create_user(username="cli", password="pass")
|
|
set_user_type(self.employee_user, UserProfile.UserType.EMPLOYEE)
|
|
set_user_type(self.client_user, UserProfile.UserType.CLIENT)
|
|
|
|
def test_get_employees_excludes_clients(self):
|
|
employees = list(get_employees())
|
|
self.assertEqual(len(employees), 1)
|
|
self.assertEqual(employees[0].user, self.employee_user)
|
|
|
|
def test_permission_helpers(self):
|
|
self.assertTrue(is_employee_user(self.employee_user))
|
|
self.assertFalse(is_employee_user(self.client_user))
|
|
self.assertTrue(is_client_user(self.client_user))
|
|
self.assertFalse(is_client_user(self.employee_user))
|
|
|
|
|
|
class FinancialAccessTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.admin = User.objects.create_superuser(username="admin", password="pass", email="a@test.com")
|
|
self.employee = User.objects.create_user(username="employee", password="pass")
|
|
self.client_user = User.objects.create_user(username="client", password="pass")
|
|
set_user_type(self.employee, UserProfile.UserType.EMPLOYEE)
|
|
set_user_type(self.client_user, UserProfile.UserType.CLIENT)
|
|
|
|
contract = Contract.objects.create(
|
|
contract_type=Contract.ContractTypeEnum.FIRM_FIX_PRICED,
|
|
name="C1",
|
|
budget_hours=100,
|
|
)
|
|
charge = ChargeNumber.objects.create(
|
|
charge_number_type=ChargeNumber.ChargeNumberTypeEnum.LEVEL_OF_EFFORT,
|
|
contract=contract,
|
|
)
|
|
emp_record = Employee.objects.get(user=self.employee)
|
|
time_card = TimeCard.objects.create(employee=emp_record)
|
|
TimeCardCell.objects.create(timeCard=time_card, charge_number=charge, hour=4.0)
|
|
|
|
def test_client_can_view_reports_readonly(self):
|
|
self.client.login(username="client", password="pass")
|
|
response = self.client.get(reverse("client_reports"))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "C1")
|
|
|
|
def test_client_cannot_log_time(self):
|
|
self.client.login(username="client", password="pass")
|
|
response = self.client.get(reverse("Timekeeping"))
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
def test_client_can_view_time_logs_without_edit(self):
|
|
self.client.login(username="client", password="pass")
|
|
response = self.client.get(reverse("time_logs"))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Read only")
|
|
self.assertNotContains(response, 'href="/financial/time_logs/')
|
|
|
|
def test_employee_can_access_timekeeping(self):
|
|
self.client.login(username="employee", password="pass")
|
|
response = self.client.get(reverse("Timekeeping"))
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_client_redirected_from_financial_home_to_reports(self):
|
|
self.client.login(username="client", password="pass")
|
|
response = self.client.get(reverse("financial_home"))
|
|
self.assertRedirects(response, reverse("client_reports"))
|
|
|
|
def test_admin_can_manage_users(self):
|
|
self.client.login(username="admin", password="pass")
|
|
response = self.client.get(reverse("manage_users"))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(reverse("manage_users"), {
|
|
"user": self.client_user.id,
|
|
"user_type": UserProfile.UserType.EMPLOYEE,
|
|
})
|
|
self.assertRedirects(response, reverse("manage_users"))
|
|
self.client_user.refresh_from_db()
|
|
self.assertEqual(self.client_user.profile.user_type, UserProfile.UserType.EMPLOYEE)
|
|
|
|
def test_time_logs_only_lists_employees(self):
|
|
self.client.login(username="admin", password="pass")
|
|
response = self.client.get(reverse("time_logs"))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "employee")
|
|
employees = response.context["employees"]
|
|
self.assertEqual(employees.count(), 1)
|
|
|
|
def test_new_user_not_in_employee_dropdown(self):
|
|
extra = User.objects.create_user(username="extra", password="pass")
|
|
set_user_type(extra, UserProfile.UserType.CLIENT)
|
|
self.client.login(username="admin", password="pass")
|
|
response = self.client.get(reverse("time_logs"))
|
|
employees = response.context["employees"]
|
|
usernames = [e.user.username for e in employees]
|
|
self.assertIn("employee", usernames)
|
|
self.assertNotIn("extra", usernames)
|