diff --git a/README.md b/README.md index dac444f..2a0be39 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,12 @@ Available * [`GET /company/employees`](https://developer.personio.de/reference#get_company-employees): list all employees * [`GET /company/employees/{id}`](https://developer.personio.de/reference#get_company-employees-employee-id): get the employee with the specified ID * [`GET /company/employees/{id}/profile-picture/{width}`](https://developer.personio.de/reference#get_company-employees-employee-id-profile-picture-width): get the profile picture of the specified employee -* [`GET /company/time-offs`](https://developer.personio.de/reference#get_company-time-offs): fetch absence data for the company employees * [`GET /company/attendances`](https://developer.personio.de/reference#get_company-attendances): fetch attendance data for the company employees +* [`GET /company/time-off-types`](https://developer.personio.de/reference#get_company-time-off-types): get a list of available absences types +* [`GET /company/time-offs`](https://developer.personio.de/reference#get_company-time-offs): fetch absence data for the company employees +* [`POST /company/time-offs`](https://developer.personio.de/reference#post_company-time-offs): add absence data for the company employees +* [`GET /company/time-offs/{id}`](https://developer.personio.de/reference#get_company-time-offs-id): get the absence entry with the specified ID +* [`DELETE /company/time-offs/{id}`](https://developer.personio.de/reference#delete_company-time-offs-id): delete the absence entry with the specified ID Work in Progress @@ -102,10 +106,6 @@ Work in Progress * [`POST /company/attendances`](https://developer.personio.de/reference#post_company-attendances): add attendance data for the company employees * [`DELETE /company/attendances/{id}`](https://developer.personio.de/reference#delete_company-attendances-id): delete the attendance entry with the specified ID * [`PATCH /company/attendances/{id}`](https://developer.personio.de/reference#patch_company-attendances-id): update the attendance entry with the specified ID -* [`GET /company/time-off-types`](https://developer.personio.de/reference#get_company-time-off-types): get a list of available absences types -* [`POST /company/time-offs`](https://developer.personio.de/reference#post_company-time-offs): add absence data for the company employees -* [`DELETE /company/time-offs/{id}`](https://developer.personio.de/reference#delete_company-time-offs-id): delete the absence entry with the specified ID -* [`GET /company/time-offs/{id}`](https://developer.personio.de/reference#get_company-time-offs-id): get the absence entry with the specified ID ## Contact diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 357b8c5..51d497a 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -169,7 +169,10 @@ def request_paginated( resp_data = response['data'] if resp_data: data_acc.extend(resp_data) - params['offset'] += len(resp_data) + if response['metadata']['current_page'] == response['metadata']['total_pages']: + break + else: + params['offset'] += len(resp_data) else: break # return the accumulated data @@ -341,23 +344,58 @@ def get_absences(self, employees: Union[int, List[int], Employee, List[Employee] return self._get_employee_metadata( 'company/time-offs', Absence, employees, start_date, end_date) - def get_absence(self, absence_id: int) -> Absence: + def get_absence(self, absence: Union[Absence, int]) -> Absence: """ - placeholder; not ready to be used + Get an absence record from a given id. + + :param absence: The absence id to fetch. """ - raise NotImplementedError() + if isinstance(absence, int): + response = self.request_json(f'company/time-offs/{absence}') + return Absence.from_dict(response['data'], self) + else: + if absence.id_: + return self.get_absence(absence.id_) + else: + self.__add_remote_absence_id(absence) + return self.get_absence(absence.id_) - def create_absence(self, absence: Absence): + def create_absence(self, absence: Absence) -> Absence: """ - placeholder; not ready to be used + Creates an absence record on the Personio servers + + :param absence: The absence object to be created + :raises PersonioError: If the absence could not be created on the Personio servers """ - raise NotImplementedError() + data = absence.to_body_params() + response = self.request_json('company/time-offs', method='POST', data=data) + if response['success']: + absence.id_ = response['data']['attributes']['id'] + return absence + raise PersonioError("Could not create absence") - def delete_absence(self, absence_id: int): + def delete_absence(self, absence: Union[Absence, int]): """ - placeholder; not ready to be used + Delete an existing record + + An absence id is required. + + :param absence: The Absence object holding + the new data or an absence record id to delete. + :raises: + ValueError: If a query is required but not allowed + or the query does not provide exactly one result. """ - raise NotImplementedError() + if isinstance(absence, int): + response = self.request_json(path=f'company/time-offs/{absence}', method='DELETE') + return response['success'] + elif isinstance(absence, Absence): + if absence.id_ is not None: + return self.delete_absence(absence.id_) + else: + raise ValueError("Only an absence with an absence id can be deleted.") + else: + raise ValueError("absence must be an Absence object or an integer") def _get_employee_metadata( self, path: str, resource_cls: Type[PersonioResourceType], @@ -408,3 +446,27 @@ def _normalize_timeframe_params( employees = [employees] employee_ids = [(e.id_ if isinstance(e, Employee) else e) for e in employees] return employee_ids, start_date, end_date + + def __add_remote_absence_id(self, absence: Absence) -> Absence: + """ + Queries the API for an absence record matching + the given Absence object and adds the remote id. + + :param absence: The absence object to be updated + :return: The absence object with the absence_id set + """ + if absence.employee is None: + raise ValueError("For a remote query an employee_id is required") + if absence.start_date is None: + raise ValueError("For a remote query a start date is required") + if absence.end_date is None: + raise ValueError("For a remote query an end date is required") + matching_remote_absences = self.get_absences(employees=[absence.employee.id_], + start_date=absence.start_date, + end_date=absence.end_date) + if len(matching_remote_absences) == 0: + raise PersonioError("The absence to patch was not found") + elif len(matching_remote_absences) > 1: + raise PersonioError("More than one absence found.") + absence.id_ = matching_remote_absences[0].id_ + return absence diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 081a879..08d8aee 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -573,8 +573,8 @@ def __init__(self, start_date: datetime = None, end_date: datetime = None, days_count: float = None, - half_day_start: int = None, - half_day_end: int = None, + half_day_start: bool = False, + half_day_end: bool = False, time_off_type: AbsenceType = None, employee: ShortEmployee = None, created_by: str = None, @@ -588,19 +588,32 @@ def __init__(self, self.start_date = start_date self.end_date = end_date self.days_count = days_count - self.half_day_start = half_day_start - self.half_day_end = half_day_end + self.half_day_start = bool(half_day_start) + self.half_day_end = bool(half_day_end) self.time_off_type = time_off_type self.employee = employee self.created_by = created_by self.certificate = certificate self.created_at = created_at - def _create(self, client: 'Personio'): - pass - - def _delete(self, client: 'Personio'): - pass + def _create(self, client: 'Personio' = None): + return get_client(self, client).create_absence(self) + + def _delete(self, client: 'Personio' = None): + return get_client(self, client).delete_absence(self) + + def to_body_params(self): + data = { + 'employee_id': self.employee.id_, + 'time_off_type_id': self.time_off_type.id_, + 'start_date': self.start_date.strftime("%Y-%m-%d"), + 'end_date': self.end_date.strftime("%Y-%m-%d"), + 'half_day_start': self.half_day_start, + 'half_day_end': self.half_day_end + } + if self.comment is not None: + data['comment'] = self.comment + return data class Attendance(WritablePersonioResource): diff --git a/tests/apitest_shared.py b/tests/apitest_shared.py index 354396c..2a980c9 100644 --- a/tests/apitest_shared.py +++ b/tests/apitest_shared.py @@ -1,10 +1,15 @@ import os from functools import lru_cache +from datetime import date import pytest from personio_py import Personio, PersonioError +# Test time. if used on a personio instance, only touch entries during this time range +NOT_BEFORE = date(year=2022, month=1, day=1) +NOT_AFTER = date(year=2022, month=12, day=31) + # Personio client authentication CLIENT_ID = os.getenv('CLIENT_ID') CLIENT_SECRET = os.getenv('CLIENT_SECRET') diff --git a/tests/mock_data.py b/tests/mock_data.py index 3591c25..57df9c1 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -709,36 +709,73 @@ """ json_dict_employee_ada = json.loads(json_string_employee_ada) -json_string_absence_types = """ +json_string_empty_response = """ { "success": true, + "data": [] +} +""" +json_dict_empty_response = json.loads(json_string_empty_response) + +json_string_attendance_rms = """ +{ + "success": true, + "metadata":{ + "current_page":1, + "total_pages":1 + }, "data": [{ - "type": "TimeOffType", + "id": 33479712, + "type": "AttendancePeriod", "attributes": { - "id": 195824, - "name": "vacation" + "employee": 2116366, + "date": "1985-03-20", + "start_time": "11:00", + "end_time": "12:30", + "break": 60, + "comment": "release day! GNU Emacs Version 13 is available as free software now *yay*", + "is_holiday": false, + "is_on_time_off": false } }, { - "type": "TimeOffType", + "id": 33479612, + "type": "AttendancePeriod", "attributes": { - "id": 195825, - "name": "paid leave" + "employee": 2116366, + "date": "1985-03-19", + "start_time": "10:30", + "end_time": "22:00", + "break": 120, + "comment": "just a couple more parentheses...", + "is_holiday": false, + "is_on_time_off": false } }, { - "type": "TimeOffType", + "id": 33479602, + "type": "AttendancePeriod", "attributes": { - "id": 195826, - "name": "sick" + "employee": 2116366, + "date": "1985-03-18", + "start_time": "10:00", + "end_time": "20:00", + "break": 90, + "comment": "working on GNU Emacs", + "is_holiday": false, + "is_on_time_off": false } } ] } """ -json_dict_absence_types = json.loads(json_string_absence_types) +json_dict_attendance_rms = json.loads(json_string_attendance_rms) json_string_absence_alan = """ { "success": true, + "metadata":{ + "current_page":1, + "total_pages":1 + }, "data": [{ "type": "TimeOffPeriod", "attributes": { @@ -880,58 +917,220 @@ """ json_dict_absence_alan = json.loads(json_string_absence_alan) -json_string_empty_response = """ +json_string_absence_alan_single = """ { - "success": true, - "data": [] + "success": true, + "metadata":{ + "current_page":1, + "total_pages":1 + }, + "data": [{ + "type": "TimeOffPeriod", + "attributes": { + "id": 17205942, + "status": "approved", + "comment": "marathon starts at noon", + "start_date": "1944-09-01T00:00:00+02:00", + "end_date": "1944-09-01T00:00:00+02:00", + "days_count": 0.5, + "half_day_start": 0, + "half_day_end": 1, + "time_off_type": { + "type": "TimeOffType", + "attributes": { + "id": 195824, + "name": "vacation" + } + }, + "employee": { + "type": "Employee", + "attributes": { + "id": { + "label": "ID", + "value": 2116365 + }, + "first_name": { + "label": "First name", + "value": "Alan" + }, + "last_name": { + "label": "Last name", + "value": "Turing" + }, + "email": { + "label": "Email", + "value": "alan@example.org" + } + } + }, + "created_by": "Alan Turing", + "certificate": { + "status": "not-required" + }, + "created_at": "2020-08-21T18:07:06+02:00" + } + } + ] } """ -json_dict_empty_response = json.loads(json_string_empty_response) +json_dict_absence_alan_first = json.loads(json_string_absence_alan_single) -json_string_attendance_rms = """ +json_string_absence_types = """ { "success": true, + "metadata":{ + "current_page":1, + "total_pages":1 + }, "data": [{ - "id": 33479712, - "type": "AttendancePeriod", + "type": "TimeOffType", "attributes": { - "employee": 2116366, - "date": "1985-03-20", - "start_time": "11:00", - "end_time": "12:30", - "break": 60, - "comment": "release day! GNU Emacs Version 13 is available as free software now *yay*", - "is_holiday": false, - "is_on_time_off": false + "id": 195824, + "name": "vacation" } }, { - "id": 33479612, - "type": "AttendancePeriod", + "type": "TimeOffType", "attributes": { - "employee": 2116366, - "date": "1985-03-19", - "start_time": "10:30", - "end_time": "22:00", - "break": 120, - "comment": "just a couple more parentheses...", - "is_holiday": false, - "is_on_time_off": false + "id": 195825, + "name": "paid leave" } }, { - "id": 33479602, - "type": "AttendancePeriod", + "type": "TimeOffType", "attributes": { - "employee": 2116366, - "date": "1985-03-18", - "start_time": "10:00", - "end_time": "20:00", - "break": 90, - "comment": "working on GNU Emacs", - "is_holiday": false, - "is_on_time_off": false + "id": 195826, + "name": "sick" } } ] } """ -json_dict_attendance_rms = json.loads(json_string_attendance_rms) +json_dict_absence_types = json.loads(json_string_absence_types) + +json_string_delete_absence = """ +{ + "success": true, + "metadata":{ + "current_page":1, + "total_pages":1 + }, + "data": { + "message": "The absence period was deleted." + } +} +""" +json_dict_delete_absence = json.loads(json_string_delete_absence) + +json_string_absence_create_no_halfdays = """ +{ + "success":true, + "metadata":{ + "current_page":1, + "total_pages":1 + }, + "data":{ + "type":"TimeOffPeriod", + "attributes":{ + "id":22809350, + "status":"approved", + "comment":"", + "start_date":"2021-01-01T00:00:00+01:00", + "end_date":"2021-01-10T00:00:00+01:00", + "days_count":5, + "half_day_start":1, + "half_day_end":1, + "time_off_type":{ + "type":"TimeOffType", + "attributes":{ + "id":243402, + "name":"Unpaid vacation", + "category":"unpaid_vacation" + } + }, + "employee":{ + "type":"Employee", + "attributes":{ + "id":{ + "label":"ID", + "value":2628890 + }, + "first_name":{ + "label":"First name", + "value":"Alan" + }, + "last_name":{ + "label":"Last name", + "value":"Turing" + }, + "email":{ + "label":"Email", + "value":"alan.turing@cetitec.com" + } + } + }, + "created_by":"API", + "certificate":{ + "status":"not-required" + }, + "created_at":"2020-12-01T18:24:11+01:00" + } + } +} +""" +json_dict_absence_create_no_halfdays = json.loads(json_string_absence_create_no_halfdays) + +json_string_get_absence = """ +{ + "success":true, + "metadata":{ + "current_page":1, + "total_pages":1 + }, + "data":{ + "type":"TimeOffPeriod", + "attributes":{ + "id":2628890, + "status":"approved", + "comment":"", + "start_date":"2021-01-01T00:00:00+01:00", + "end_date":"2021-01-10T00:00:00+01:00", + "days_count":5, + "half_day_start":0, + "half_day_end":1, + "time_off_type":{ + "type":"TimeOffType", + "attributes":{ + "id":243402, + "name":"Unpaid vacation", + "category":"unpaid_vacation" + } + }, + "employee":{ + "type":"Employee", + "attributes":{ + "id":{ + "label":"ID", + "value":2628890 + }, + "first_name":{ + "label":"First name", + "value":"Alan" + }, + "last_name":{ + "label":"Last name", + "value":"Turing" + }, + "email":{ + "label":"Email", + "value":"alan.turing@cetitec.com" + } + } + }, + "created_by":"API", + "certificate":{ + "status":"not-required" + }, + "created_at":"2020-12-02T17:28:34+01:00" + } + } +}""" +json_dict_get_absence = json.loads(json_string_get_absence) diff --git a/tests/test_api.py b/tests/test_api.py index 2215493..d6c6a45 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -46,12 +46,6 @@ def test_create_employee(): assert ada_created.status == 'active' -@skip_if_no_auth -def test_get_absences(): - absences = personio.get_absences(2007207) - assert len(absences) > 0 - - @skip_if_no_auth def test_get_attendances(): attendances = personio.get_attendances(2007207) diff --git a/tests/test_api_absences.py b/tests/test_api_absences.py new file mode 100644 index 0000000..d7362e3 --- /dev/null +++ b/tests/test_api_absences.py @@ -0,0 +1,166 @@ +from .apitest_shared import * +from datetime import timedelta, date + +from personio_py import Employee, ShortEmployee, Personio, PersonioError, Absence, AbsenceType + + +@skip_if_no_auth +@pytest.mark.parametrize("half_day_start", [True, False]) +@pytest.mark.parametrize("half_day_end", [True, False]) +def test_create_absences(half_day_start: bool, half_day_end: bool): + """ + Test the creation of absence records on the server. + """ + # Prepare data + test_user = get_test_employee() + start_date = date(year=2022, month=1, day=1) + end_date = date(year=2022, month=1, day=10) + + # Ensure there are no left absences + delete_all_absences_of_employee(test_user) + + # Start test + absence_to_create = create_absence_for_user(test_user, + start_date=start_date, + end_date=end_date, + half_day_start=half_day_start, + half_day_end=half_day_end) + assert absence_to_create.id_ is None + absence_to_create.create(personio) + assert absence_to_create.id_ + remote_absence = personio.get_absence(absence=absence_to_create) + assert remote_absence.half_day_start is half_day_start + assert remote_absence.half_day_end is half_day_end + assert remote_absence.start_date - start_date < timedelta(seconds=1) + assert remote_absence.end_date - end_date < timedelta(seconds=1) + + +@skip_if_no_auth +def test_get_absences_from_id(): + user = prepare_test_get_absences() + absence_id = create_absence_for_user(user, create=True).id_ + absence = personio.get_absence(absence_id) + assert absence.id_ == absence_id + + +@skip_if_no_auth +def test_get_absences_from_absence_object(): + user = prepare_test_get_absences() + remote_absence = create_absence_for_user(user, create=True) + absence = personio.get_absence(remote_absence) + assert absence.id_ == remote_absence.id_ + + +@skip_if_no_auth +def test_get_absences_from_absence_object_without_id(): + user = prepare_test_get_absences() + remote_absence = create_absence_for_user(user, create=True) + absence_id = remote_absence.id_ + remote_absence.id_ = None + absence = personio.get_absence(remote_absence) + assert absence.id_ == absence_id + + +@skip_if_no_auth +def test_delete_absences_from_model_no_client(): + test_user = get_test_employee() + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + with pytest.raises(PersonioError): + absence.delete() + + +@skip_if_no_auth +def test_delete_absences_from_model_passed_client(): + test_user = get_test_employee() + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + assert absence.delete(client=personio) is True + + +@skip_if_no_auth +def test_delete_absences_from_model_with_client(): + test_user = get_test_employee() + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + absence._client = personio + assert absence.delete() is True + + +@skip_if_no_auth +def test_delete_absence_from_absence_id(): + test_user = get_test_employee() + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + assert personio.delete_absence(absence.id_) is True + + +@skip_if_no_auth +def test_delete_absences_from_client_object_with_id(): + test_user = get_test_employee() + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + assert personio.delete_absence(absence) is True + + +@skip_if_no_auth +def test_delete_absences_from_client_object_with_no_id(): + test_user = get_test_employee() + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + absence.id_ = None + with pytest.raises(ValueError): + personio.delete_absence(absence) + + +def delete_absences(client: Personio, absences: [int] or [Absence]): + for absence in absences: + client.delete_absence(absence) + + +def create_absences(client: Personio, absences: [Absence]): + for absence in absences: + client.create_absence(absence) + + +def delete_all_absences_of_employee(employee: Employee): + absences = personio.get_absences(employee, start_date=NOT_BEFORE, end_date=NOT_AFTER) + delete_absences(personio, absences) + + +def create_absence_for_user(employee: Employee, + time_off_type: AbsenceType = None, + start_date: date = None, + end_date: date = None, + half_day_start: bool = False, + half_day_end: bool = False, + comment: str = None, + create: bool = False) -> Absence: + if not time_off_type: + absence_types = personio.get_absence_types() + time_off_type = [absence_type for absence_type in absence_types if absence_type.name == "Unpaid vacation"][0] + if not start_date: + start_date = date(year=2022, month=1, day=1) + if not end_date: + end_date = date(year=2022, month=1, day=10) + + absence_to_create = Absence( + start_date=start_date, + end_date=end_date, + time_off_type=time_off_type, + employee=employee, + half_day_start=half_day_start, + half_day_end=half_day_end, + comment=comment + ) + if create: + absence_to_create.create(personio) + return absence_to_create + + +def prepare_test_get_absences() -> Employee: + test_user = get_test_employee() + + # Be sure there are no leftover absences + delete_all_absences_of_employee(test_user) + return test_user diff --git a/tests/test_mock_api.py b/tests/test_mock_api.py index c983d0f..7bf9134 100644 --- a/tests/test_mock_api.py +++ b/tests/test_mock_api.py @@ -10,6 +10,7 @@ iso_date_match = re.compile(r'\d\d\d\d-\d\d-\d\d') + @responses.activate def test_authenticate_ok(): # mock a successful authentication response @@ -92,61 +93,6 @@ def test_auth_rotation_fail(): assert "authorization header" in str(e.value).lower() -@responses.activate -def test_get_absences(): - # configure personio & get absences for alan - mock_absences() - personio = mock_personio() - absences = personio.get_absences(2116365) - # validate - assert len(absences) == 3 - selection = [a for a in absences if "marathon" in a.comment.lower()] - assert len(selection) == 1 - marathon = selection[0] - assert marathon.start_date == date(1944, 9, 1) - assert marathon.half_day_start == 0 - assert marathon.half_day_end == 1 - assert marathon.status == 'approved' - # validate serialization - source_dict = json_dict_absence_alan['data'][0] - target_dict = marathon.to_dict() - compare_labeled_attributes(source_dict, target_dict) - - -@responses.activate -def test_get_absences_from_employee_objects(): - # mock endpoints & get absences for all employees - mock_employees() - mock_absences() - personio = mock_personio() - employees = personio.get_employees() - assert employees - absences = personio.get_absences(employees) - # the response is not important (it does not match the input), but the function should accept - # a list of Employee objects as parameter and return a result - assert absences - - -@responses.activate -def test_get_absence_types(): - # mock the get absence types endpoint - responses.add( - responses.GET, 'https://api.personio.de/v1/company/time-off-types', status=200, - json=json_dict_absence_types, adding_headers={'Authorization': 'Bearer foo'}) - # configure personio & get absences for alan - personio = mock_personio() - absence_types = personio.get_absence_types() - # non-empty contents - assert len(absence_types) == 3 - for at in absence_types: - assert at.id_ > 0 - assert isinstance(at.name, str) - # serialization matches input - for source_dict, at in zip(json_dict_absence_types['data'], absence_types): - target_dict = at.to_dict() - assert source_dict == target_dict - - @responses.activate def test_get_attendance(): # mock the get absences endpoint (with different array offsets) @@ -190,16 +136,6 @@ def mock_employees(): json=json_dict_employees, adding_headers={'Authorization': 'Bearer rotated_dummy_token'}) -def mock_absences(): - # mock the get absences endpoint (with different array offsets) - responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), - status=200, json=json_dict_absence_alan, adding_headers={'Authorization': 'Bearer foo'}) - responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=3.*'), - status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) - - def compare_labeled_attributes(expected: Dict, actual: Dict): if actual == expected: # fast lane - exact match diff --git a/tests/test_mock_api_absences.py b/tests/test_mock_api_absences.py new file mode 100644 index 0000000..2afb75e --- /dev/null +++ b/tests/test_mock_api_absences.py @@ -0,0 +1,202 @@ +from datetime import date + +import pytest +import responses +import re + +from personio_py import PersonioError, Absence, Employee +from tests.test_mock_api import mock_personio, compare_labeled_attributes, mock_employees +from tests.mock_data import json_dict_absence_alan, json_dict_absence_types, json_dict_empty_response,\ + json_dict_delete_absence, json_dict_absence_alan_first, json_dict_absence_create_no_halfdays, json_dict_get_absence + + +@responses.activate +def test_get_absence_from_id(): + personio = mock_personio() + mock_get_absence() + absence_id_only = Absence(id_=2628890) + absence = personio.get_absence(absence_id_only) + assert absence.employee.first_name == 'Alan' + assert absence.employee.last_name == 'Turing' + assert absence.id_ == 2628890 + assert absence.start_date == date(2021, 1, 1) + assert absence.end_date == date(2021, 1, 10) + + +@responses.activate +def test_get_absence_from_object_without_id(): + personio = mock_personio() + mock_get_absence() + mock_single_absences() + absence_id_only = Absence(id_=2628890) + absence = personio.get_absence(absence_id_only) + absence.id_ = None + personio.get_absence(absence) + + +@responses.activate +def test_create_absence(): + mock_absence_types() + mock_create_absence_no_halfdays() + personio = mock_personio() + absence_type = personio.get_absence_types()[0] + employee = Employee( + first_name="Alan", + last_name='Turing', + email='alan.turing@cetitec.com' + ) + absence = Absence( + client=personio, + employee=employee, + start_date=date(2020, 1, 1), + end_date=date(2020, 1, 10), + half_day_start=False, + half_day_end=False, + time_off_type=absence_type) + absence.create() + assert absence.id_ + + +@responses.activate +def test_delete_absence(): + mock_delete_absence() + personio = mock_personio() + result = personio.delete_absence(2116365) + assert result is True + + +@responses.activate +def test_delete_absence_no_client(): + personio = mock_personio() + mock_absences() + absence = personio.get_absences(2116365)[0] + absence._client = None + with pytest.raises(PersonioError): + absence.delete() + + +@responses.activate +def test_delete_absence_passed_client(): + personio = mock_personio() + mock_absences() + mock_delete_absence() + absence = personio.get_absences(2116365)[0] + absence._client = None + assert absence.delete(client=personio) is True + + +@responses.activate +def test_delete_absence_no_id(): + personio = mock_personio() + mock_absences() + absence = personio.get_absences(2116365)[0] + absence.id_ = None + with pytest.raises(ValueError): + absence.delete() + with pytest.raises(ValueError): + personio.delete_absence(absence) + + +@responses.activate +def test_get_absences(): + # configure personio & get absences for alan + mock_absences() + personio = mock_personio() + absences = personio.get_absences(2116365) + # validate + assert len(absences) == 3 + selection = [a for a in absences if "marathon" in a.comment.lower()] + assert len(selection) == 1 + marathon = selection[0] + assert marathon.start_date == date(1944, 9, 1) + assert marathon.half_day_start == 0 + assert marathon.half_day_end == 1 + assert marathon.status == 'approved' + # validate serialization + source_dict = json_dict_absence_alan['data'][0] + target_dict = marathon.to_dict() + compare_labeled_attributes(source_dict, target_dict) + + +@responses.activate +def test_get_absences_from_employee_objects(): + # mock endpoints & get absences for all employees + mock_employees() + mock_absences() + personio = mock_personio() + employees = personio.get_employees() + assert employees + absences = personio.get_absences(employees) + # the response is not important (it does not match the input), but the function should accept + # a list of Employee objects as parameter and return a result + assert absences + + +@responses.activate +def test_get_absence_types(): + mock_absence_types() + # configure personio & get absences for alan + personio = mock_personio() + absence_types = personio.get_absence_types() + # non-empty contents + assert len(absence_types) == 3 + for at in absence_types: + assert at.id_ > 0 + assert isinstance(at.name, str) + # serialization matches input + for source_dict, at in zip(json_dict_absence_types['data'], absence_types): + target_dict = at.to_dict() + assert source_dict == target_dict + + +def mock_absence_types(): + # mock the get absence types endpoint + responses.add( + responses.GET, 'https://api.personio.de/v1/company/time-off-types', status=200, + json=json_dict_absence_types, adding_headers={'Authorization': 'Bearer foo'}) + + +def mock_absences(): + # mock the get absences endpoint (with different array offsets) + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), + status=200, json=json_dict_absence_alan, adding_headers={'Authorization': 'Bearer foo'}) + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=3.*'), + status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) + + +def mock_single_absences(): + # mock the get absences endpoint (with different array offsets) + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), + status=200, json=json_dict_absence_alan_first, adding_headers={'Authorization': 'Bearer foo'}) + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), + status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) + + +def mock_no_absences(): + # mock the get absences endpoint + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), + status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) + + +def mock_delete_absence(): + # mock the delete endpoint + responses.add( + responses.DELETE, re.compile('https://api.personio.de/v1/company/time-offs/*'), + status=200, json=json_dict_delete_absence, adding_headers={'Authorization': 'Bearer bar'}) + + +def mock_create_absence_no_halfdays(): + responses.add( + responses.POST, 'https://api.personio.de/v1/company/time-offs', + status=200, json=json_dict_absence_create_no_halfdays, adding_headers={'Authorization': 'Bearer bar'}) + + +def mock_get_absence(): + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs/.*'), + status=200, json=json_dict_get_absence, adding_headers={'Authorization': 'Bearer bar'})