Files
stempelbot/.idea/copilotDiffState.xml
2026-09-25 18:11:14 +02:00

18 lines
18 KiB
XML

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CopilotDiffPersistence">
<option name="pendingDiffs">
<map>
<entry key="/test_weekly_report (snapshot).py">
<value>
<PendingDiffInfo>
<option name="filePath" value="/test_weekly_report (snapshot).py" />
<option name="originalContent" value="import os&#10;import sys&#10;import tempfile&#10;import unittest&#10;from datetime import date&#10;from io import BytesIO&#10;from pathlib import Path&#10;&#10;from openpyxl import load_workbook&#10;&#10;PROJECT_DIR = Path(__file__).resolve().parents[1]&#10;sys.path.insert(0, str(PROJECT_DIR / &quot;stempelbot&quot;))&#10;os.environ.setdefault(&quot;USERNAME&quot;, &quot;test-user&quot;)&#10;os.environ.setdefault(&quot;PASSWORD&quot;, &quot;test-password&quot;)&#10;os.environ.setdefault(&quot;SMART_TIME_PASSWORD&quot;, &quot;smarttime-password&quot;)&#10;os.environ.setdefault(&quot;AUTOSTAMP_FILE&quot;, &quot;autostamps.json&quot;)&#10;os.environ.setdefault(&quot;STAMPHISTORY_FILE&quot;, &quot;timestamp_history.json&quot;)&#10;os.environ.setdefault(&quot;LOG_LEVEL&quot;, &quot;CRITICAL&quot;)&#10;&#10;from excel_export import export_weekly_workbook, load_template_options # noqa: E402&#10;from jira_client import JiraActivity, JiraClient, JiraError # noqa: E402&#10;from weekly_report import calculate_week, iso_week_bounds # noqa: E402&#10;&#10;TEMPLATE = PROJECT_DIR / &quot;TimeTracking Activation_Firstname_Lastname_CW_Version 15.xlsx&quot;&#10;&#10;&#10;class FakeResponse:&#10; def __init__(self, payload, status_code=200):&#10; self.payload = payload&#10; self.status_code = status_code&#10;&#10; def raise_for_status(self):&#10; if self.status_code &gt;= 400:&#10; import requests&#10; response = requests.Response()&#10; response.status_code = self.status_code&#10; raise requests.HTTPError(response=response)&#10;&#10; def json(self):&#10; return self.payload&#10;&#10; @property&#10; def text(self):&#10; return str(self.payload)&#10;&#10; @property&#10; def reason(self):&#10; return &quot;Unauthorized&quot; if self.status_code == 401 else &quot;OK&quot;&#10;&#10;&#10;class FakeSession:&#10; def __init__(self, responses):&#10; self.responses = iter(responses)&#10; self.headers = {}&#10; self.auth = None&#10; self.calls = []&#10;&#10; def get(self, url, **kwargs):&#10; self.calls.append((url, kwargs))&#10; response = next(self.responses)&#10; return response if isinstance(response, FakeResponse) else FakeResponse(response)&#10;&#10;&#10;class WeeklyReportTests(unittest.TestCase):&#10; @classmethod&#10; def setUpClass(cls):&#10; cls.options = load_template_options(TEMPLATE)&#10;&#10; def test_iso_week_can_cross_year_boundary(self):&#10; self.assertEqual(&#10; iso_week_bounds(2026, 1),&#10; (date(2025, 12, 29), date(2026, 1, 4)),&#10; )&#10;&#10; def test_daily_net_time_applies_legal_break_and_accounts_for_minutes(self):&#10; day = date(2026, 8, 24)&#10; rows = calculate_week(&#10; 2026,&#10; 35,&#10; {day: [&quot;08:00&quot;, &quot;12:00&quot;, &quot;12:15&quot;, &quot;16:15&quot;]},&#10; [JiraActivity(day, &quot;ABC-1&quot;, &quot;Implement export&quot;, &quot;Other&quot;, &quot;change&quot;, &quot;code updated&quot;, &quot;url&quot;)],&#10; self.options,&#10; )&#10; self.assertEqual(len(rows), 1)&#10; self.assertEqual(rows[0].minutes, 465)&#10; self.assertEqual(rows[0].issue_keys, &quot;ABC-1&quot;)&#10; self.assertEqual(rows[0].project, &quot;Other&quot;)&#10;&#10; def test_incomplete_historical_stamp_is_not_guessed(self):&#10; day = date(2026, 8, 25)&#10; rows = calculate_week(2026, 35, {day: [&quot;08:00&quot;]}, [], self.options)&#10; self.assertEqual(rows[0].minutes, 0)&#10; self.assertIn(&quot;Incomplete&quot;, rows[0].warning)&#10;&#10; def test_jira_pagination_and_authored_activity_filtering(self):&#10; issue = {&#10; &quot;key&quot;: &quot;ABC-7&quot;,&#10; &quot;fields&quot;: {&#10; &quot;summary&quot;: &quot;Build report&quot;,&#10; &quot;project&quot;: {&quot;name&quot;: &quot;Other&quot;},&#10; &quot;assignee&quot;: {&quot;accountId&quot;: &quot;me&quot;},&#10; &quot;creator&quot;: {&quot;accountId&quot;: &quot;someone&quot;},&#10; &quot;updated&quot;: &quot;2026-08-24T15:00:00+00:00&quot;,&#10; &quot;comment&quot;: {&#10; &quot;comments&quot;: [&#10; {&quot;author&quot;: {&quot;accountId&quot;: &quot;me&quot;}, &quot;created&quot;: &quot;2026-08-24T12:00:00+00:00&quot;},&#10; {&quot;author&quot;: {&quot;accountId&quot;: &quot;other&quot;}, &quot;created&quot;: &quot;2026-08-24T13:00:00+00:00&quot;},&#10; ]&#10; },&#10; &quot;worklog&quot;: {&quot;worklogs&quot;: []},&#10; },&#10; &quot;changelog&quot;: {&quot;histories&quot;: []},&#10; }&#10; session = FakeSession([&#10; {&quot;accountId&quot;: &quot;me&quot;, &quot;displayName&quot;: &quot;Tester&quot;},&#10; {&quot;issues&quot;: [issue], &quot;nextPageToken&quot;: &quot;next&quot;},&#10; {&quot;issues&quot;: [], &quot;isLast&quot;: True},&#10; ])&#10; client = JiraClient(&quot;https://jira.example&quot;, &quot;mail&quot;, &quot;token&quot;, session=session)&#10; activities = client.get_week_activity(date(2026, 8, 24), date(2026, 8, 30))&#10; self.assertEqual([(item.issue_key, item.activity_type) for item in activities], [(&quot;ABC-7&quot;, &quot;comment&quot;)])&#10; self.assertEqual(session.calls[2][1][&quot;params&quot;][&quot;nextPageToken&quot;], &quot;next&quot;)&#10;&#10; def test_jira_http_failure_logs_response_without_token(self):&#10; session = FakeSession([&#10; FakeResponse(&#10; {&quot;errorMessages&quot;: [&quot;Client must be authenticated&quot;]}, status_code=401&#10; )&#10; ])&#10; client = JiraClient(&#10; &quot;https://jira.example&quot;, &quot;mail@example.com&quot;, &quot;super-secret-token&quot;, session=session&#10; )&#10; with self.assertLogs(&quot;jira_client&quot;, level=&quot;ERROR&quot;) as captured:&#10; with self.assertRaisesRegex(JiraError, &quot;HTTP 401&quot;):&#10; client._get(&quot;/rest/api/3/myself&quot;)&#10; output = &quot;\n&quot;.join(captured.output)&#10; self.assertIn(&quot;Client must be authenticated&quot;, output)&#10; self.assertIn(&quot;401 Unauthorized&quot;, output)&#10; self.assertIn(&quot;GET https://jira.example/rest/api/3/myself&quot;, output)&#10; self.assertNotIn(&quot;super-secret-token&quot;, output)&#10;&#10; def test_export_preserves_template_and_exact_total(self):&#10; first = date(2026, 8, 24)&#10; rows = calculate_week(&#10; 2026,&#10; 35,&#10; {&#10; first: [&quot;08:00&quot;, &quot;12:00&quot;],&#10; date(2026, 8, 25): [&quot;09:00&quot;, &quot;10:01&quot;],&#10; },&#10; [],&#10; self.options,&#10; )&#10; data = export_weekly_workbook(TEMPLATE, &quot;Test Person&quot;, 2026, 35, rows)&#10; workbook = load_workbook(BytesIO(data), data_only=False)&#10; sheet = workbook[&quot;weekly time tracking&quot;]&#10; self.assertEqual(workbook.sheetnames, [&quot;weekly time tracking&quot;, &quot;Dropdowns&quot;])&#10; self.assertEqual(sheet[&quot;B3&quot;].value, &quot;Test Person&quot;)&#10; self.assertEqual(sheet[&quot;B6&quot;].value, &quot;KW 35&quot;)&#10; self.assertEqual(sheet[&quot;C6&quot;].value, &quot;24.08. bis 30.08.&quot;)&#10; self.assertAlmostEqual(sheet[&quot;H6&quot;].value + sheet[&quot;H7&quot;].value, 301 / 60)&#10; self.assertEqual(sheet[&quot;H6&quot;].number_format, &quot;0.00&quot;)&#10; self.assertEqual(len(sheet.data_validations.dataValidation), 5)&#10; self.assertTrue(workbook.calculation.fullCalcOnLoad)&#10; self.assertGreater(len(sheet.merged_cells.ranges), 0)&#10; workbook.close()&#10;&#10;&#10;if __name__ == &quot;__main__&quot;:&#10; unittest.main()&#10;&#10;&#10;&#10;" />
<option name="updatedContent" value="import os&#10;import sys&#10;import tempfile&#10;import unittest&#10;from datetime import date&#10;from io import BytesIO&#10;from pathlib import Path&#10;from types import SimpleNamespace&#10;&#10;from openpyxl import load_workbook&#10;&#10;PROJECT_DIR = Path(__file__).resolve().parents[1]&#10;sys.path.insert(0, str(PROJECT_DIR / &quot;stempelbot&quot;))&#10;os.environ.setdefault(&quot;USERNAME&quot;, &quot;test-user&quot;)&#10;os.environ.setdefault(&quot;PASSWORD&quot;, &quot;test-password&quot;)&#10;os.environ.setdefault(&quot;SMART_TIME_PASSWORD&quot;, &quot;smarttime-password&quot;)&#10;os.environ.setdefault(&quot;AUTOSTAMP_FILE&quot;, &quot;autostamps.json&quot;)&#10;os.environ.setdefault(&quot;STAMPHISTORY_FILE&quot;, &quot;timestamp_history.json&quot;)&#10;os.environ.setdefault(&quot;LOG_LEVEL&quot;, &quot;CRITICAL&quot;)&#10;&#10;from excel_export import export_weekly_workbook, load_template_options # noqa: E402&#10;from jira_client import JiraActivity, JiraClient, JiraError # noqa: E402&#10;from weekly_report import calculate_week, iso_week_bounds # noqa: E402&#10;&#10;TEMPLATE = PROJECT_DIR / &quot;TimeTracking Activation_Firstname_Lastname_CW_Version 15.xlsx&quot;&#10;&#10;&#10;class FakeResponse:&#10; def __init__(self, payload, status_code=200):&#10; self.payload = payload&#10; self.status_code = status_code&#10; self.headers = {}&#10; self.request = None&#10;&#10; def raise_for_status(self):&#10; if self.status_code &gt;= 400:&#10; import requests&#10; raise requests.HTTPError(response=self)&#10;&#10; def json(self):&#10; return self.payload&#10;&#10; @property&#10; def text(self):&#10; return str(self.payload)&#10;&#10; @property&#10; def reason(self):&#10; return &quot;Unauthorized&quot; if self.status_code == 401 else &quot;OK&quot;&#10;&#10;&#10;class FakeSession:&#10; def __init__(self, responses):&#10; self.responses = iter(responses)&#10; self.headers = {}&#10; self.auth = None&#10; self.calls = []&#10;&#10; def get(self, url, **kwargs):&#10; self.calls.append((url, kwargs))&#10; response = next(self.responses)&#10; return response if isinstance(response, FakeResponse) else FakeResponse(response)&#10;&#10;&#10;class WeeklyReportTests(unittest.TestCase):&#10; @classmethod&#10; def setUpClass(cls):&#10; cls.options = load_template_options(TEMPLATE)&#10;&#10; def test_iso_week_can_cross_year_boundary(self):&#10; self.assertEqual(&#10; iso_week_bounds(2026, 1),&#10; (date(2025, 12, 29), date(2026, 1, 4)),&#10; )&#10;&#10; def test_daily_net_time_applies_legal_break_and_accounts_for_minutes(self):&#10; day = date(2026, 8, 24)&#10; rows = calculate_week(&#10; 2026,&#10; 35,&#10; {day: [&quot;08:00&quot;, &quot;12:00&quot;, &quot;12:15&quot;, &quot;16:15&quot;]},&#10; [JiraActivity(day, &quot;ABC-1&quot;, &quot;Implement export&quot;, &quot;Other&quot;, &quot;change&quot;, &quot;code updated&quot;, &quot;url&quot;)],&#10; self.options,&#10; )&#10; self.assertEqual(len(rows), 1)&#10; self.assertEqual(rows[0].minutes, 465)&#10; self.assertEqual(rows[0].issue_keys, &quot;ABC-1&quot;)&#10; self.assertEqual(rows[0].project, &quot;Other&quot;)&#10;&#10; def test_incomplete_historical_stamp_is_not_guessed(self):&#10; day = date(2026, 8, 25)&#10; rows = calculate_week(2026, 35, {day: [&quot;08:00&quot;]}, [], self.options)&#10; self.assertEqual(rows[0].minutes, 0)&#10; self.assertIn(&quot;Incomplete&quot;, rows[0].warning)&#10;&#10; def test_jira_pagination_and_authored_activity_filtering(self):&#10; issue = {&#10; &quot;key&quot;: &quot;ABC-7&quot;,&#10; &quot;fields&quot;: {&#10; &quot;summary&quot;: &quot;Build report&quot;,&#10; &quot;project&quot;: {&quot;name&quot;: &quot;Other&quot;},&#10; &quot;assignee&quot;: {&quot;accountId&quot;: &quot;me&quot;},&#10; &quot;creator&quot;: {&quot;accountId&quot;: &quot;someone&quot;},&#10; &quot;updated&quot;: &quot;2026-08-24T15:00:00+00:00&quot;,&#10; &quot;comment&quot;: {&#10; &quot;comments&quot;: [&#10; {&quot;author&quot;: {&quot;accountId&quot;: &quot;me&quot;}, &quot;created&quot;: &quot;2026-08-24T12:00:00+00:00&quot;},&#10; {&quot;author&quot;: {&quot;accountId&quot;: &quot;other&quot;}, &quot;created&quot;: &quot;2026-08-24T13:00:00+00:00&quot;},&#10; ]&#10; },&#10; &quot;worklog&quot;: {&quot;worklogs&quot;: []},&#10; },&#10; &quot;changelog&quot;: {&quot;histories&quot;: []},&#10; }&#10; session = FakeSession([&#10; {&quot;accountId&quot;: &quot;me&quot;, &quot;displayName&quot;: &quot;Tester&quot;},&#10; {&quot;issues&quot;: [issue], &quot;nextPageToken&quot;: &quot;next&quot;},&#10; {&quot;issues&quot;: [], &quot;isLast&quot;: True},&#10; ])&#10; client = JiraClient(&quot;https://jira.example&quot;, &quot;mail&quot;, &quot;token&quot;, session=session)&#10; activities = client.get_week_activity(date(2026, 8, 24), date(2026, 8, 30))&#10; self.assertEqual([(item.issue_key, item.activity_type) for item in activities], [(&quot;ABC-7&quot;, &quot;comment&quot;)])&#10; self.assertEqual(session.calls[2][1][&quot;params&quot;][&quot;nextPageToken&quot;], &quot;next&quot;)&#10;&#10; def test_jira_http_failure_logs_response_without_token(self):&#10; response = FakeResponse(&#10; {&quot;errorMessages&quot;: [&quot;Client must be authenticated&quot;]}, status_code=401&#10; )&#10; response.request = SimpleNamespace(&#10; method=&quot;GET&quot;,&#10; url=&quot;https://jira.example/rest/api/3/myself&quot;,&#10; headers={&quot;Accept&quot;: &quot;application/json&quot;, &quot;Authorization&quot;: &quot;Basic super-secret-token&quot;},&#10; )&#10; response.headers = {&#10; &quot;WWW-Authenticate&quot;: 'Bearer realm=&quot;Atlassian&quot;',&#10; &quot;Set-Cookie&quot;: &quot;secret-session-cookie&quot;,&#10; }&#10; session = FakeSession([&#10; response&#10; ])&#10; client = JiraClient(&#10; &quot;https://jira.example&quot;, &quot;mail@example.com&quot;, &quot;super-secret-token&quot;, session=session&#10; )&#10; with self.assertLogs(&quot;jira_client&quot;, level=&quot;ERROR&quot;) as captured:&#10; with self.assertRaisesRegex(JiraError, &quot;HTTP 401&quot;):&#10; client._get(&quot;/rest/api/3/myself&quot;)&#10; output = &quot;\n&quot;.join(captured.output)&#10; self.assertIn(&quot;Client must be authenticated&quot;, output)&#10; self.assertIn(&quot;401 Unauthorized&quot;, output)&#10; self.assertIn(&quot;GET https://jira.example/rest/api/3/myself&quot;, output)&#10; self.assertIn(&quot;WWW-Authenticate&quot;, output)&#10; self.assertIn(&quot;&lt;redacted&gt;&quot;, output)&#10; self.assertNotIn(&quot;super-secret-token&quot;, output)&#10; self.assertNotIn(&quot;secret-session-cookie&quot;, output)&#10;&#10; def test_export_preserves_template_and_exact_total(self):&#10; first = date(2026, 8, 24)&#10; rows = calculate_week(&#10; 2026,&#10; 35,&#10; {&#10; first: [&quot;08:00&quot;, &quot;12:00&quot;],&#10; date(2026, 8, 25): [&quot;09:00&quot;, &quot;10:01&quot;],&#10; },&#10; [],&#10; self.options,&#10; )&#10; data = export_weekly_workbook(TEMPLATE, &quot;Test Person&quot;, 2026, 35, rows)&#10; workbook = load_workbook(BytesIO(data), data_only=False)&#10; sheet = workbook[&quot;weekly time tracking&quot;]&#10; self.assertEqual(workbook.sheetnames, [&quot;weekly time tracking&quot;, &quot;Dropdowns&quot;])&#10; self.assertEqual(sheet[&quot;B3&quot;].value, &quot;Test Person&quot;)&#10; self.assertEqual(sheet[&quot;B6&quot;].value, &quot;KW 35&quot;)&#10; self.assertEqual(sheet[&quot;C6&quot;].value, &quot;24.08. bis 30.08.&quot;)&#10; self.assertAlmostEqual(sheet[&quot;H6&quot;].value + sheet[&quot;H7&quot;].value, 301 / 60)&#10; self.assertEqual(sheet[&quot;H6&quot;].number_format, &quot;0.00&quot;)&#10; self.assertEqual(len(sheet.data_validations.dataValidation), 5)&#10; self.assertTrue(workbook.calculation.fullCalcOnLoad)&#10; self.assertGreater(len(sheet.merged_cells.ranges), 0)&#10; workbook.close()&#10;&#10;&#10;if __name__ == &quot;__main__&quot;:&#10; unittest.main()&#10;&#10;&#10;&#10;" />
</PendingDiffInfo>
</value>
</entry>
</map>
</option>
</component>
</project>