From b12ebc1fae714e4976c3453e3daf1c0bb0d32519 Mon Sep 17 00:00:00 2001 From: Shane Jaroch Date: Sat, 27 Dec 2025 06:57:05 -0500 Subject: [PATCH] init tests --- .envrc | 2 + .requirements-lint.txt | 8 ++++ Makefile | 14 +++++++ tests/conftest.py | 39 +++++++++++++++++++ tests/test_cli.py | 59 +++++++++++++++++++++++++++++ tests/test_session.py | 85 ++++++++++++++++++++++++++++++++++++++++++ tests/test_tree.py | 68 +++++++++++++++++++++++++++++++++ 7 files changed, 275 insertions(+) create mode 100644 .envrc create mode 100644 .requirements-lint.txt create mode 100644 Makefile create mode 100644 tests/conftest.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_session.py create mode 100644 tests/test_tree.py diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1c21d04 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +source .venv/bin/activate +unset PS1 diff --git a/.requirements-lint.txt b/.requirements-lint.txt new file mode 100644 index 0000000..70606de --- /dev/null +++ b/.requirements-lint.txt @@ -0,0 +1,8 @@ +black==25.1.0 +flake8==7.3.0 +isort==6.0.1 +mypy==1.17.1 +pylint==3.3.8 +types-requests==2.32.4.20250809 +coverage==7.13.0 +pytest==9.0.2 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ac54164 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +LINT_LOCS ?= getmyancestors/ + +.PHONY: lint +lint: + flake8 $(LINT_LOCS) + black $(LINT_LOCS) + isort $(LINT_LOCS) + pylint $(LINT_LOCS) + mypy $(LINT_LOCS) + +.PHONY: test +test: + coverage run -m pytest -svv tests/ + coverage report -m diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f6ae9a2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,39 @@ +from unittest.mock import MagicMock + +import pytest + + +@pytest.fixture +def mock_user_data(): + return { + "users": [ + { + "personId": "KW7V-Y32", + "preferredLanguage": "en", + "displayName": "Test User", + } + ] + } + + +@pytest.fixture +def mock_person_data(): + return { + "persons": [ + { + "id": "KW7V-Y32", + "display": { + "name": "John Doe", + "gender": "Male", + "lifespan": "1900-1980", + }, + "facts": [ + { + "type": "http://gedcomx.org/Birth", + "date": {"original": "1 Jan 1900"}, + } + ], + "names": [{"nameForms": [{"fullText": "John Doe"}]}], + } + ] + } diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..bbea8b9 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,59 @@ +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from getmyancestors.getmyancestors import main + + +class TestCLI: + + @patch("getmyancestors.getmyancestors.Session") + @patch("getmyancestors.getmyancestors.Tree") + @patch( + "sys.argv", + ["getmyancestors", "-u", "testuser", "-p", "testpass", "-i", "KW7V-Y32"], + ) + def test_main_execution(self, mock_tree, mock_session): + """Test that main runs with basic arguments.""" + mock_fs = mock_session.return_value + mock_fs.logged = True + + # Run main + main() + + # Verify Session initialized with args + mock_session.assert_called_with( + username="testuser", + password="testpass", + client_id=None, + redirect_uri=None, + verbose=False, + logfile=False, + timeout=60, + ) + + # Verify Tree operations + mock_tree.return_value.add_indis.assert_called() + mock_tree.return_value.print.assert_called() + + @patch("getmyancestors.getmyancestors.Session") + @patch( + "sys.argv", + ["getmyancestors", "-u", "testuser", "-p", "testpass", "--descend", "2"], + ) + def test_descend_argument(self, mock_session): + """Test that the descend argument is passed to logic.""" + mock_fs = mock_session.return_value + mock_fs.logged = True + + # We need to mock Tree because main interacts with it deeply + with patch("getmyancestors.getmyancestors.Tree") as mock_tree: + mock_tree_instance = mock_tree.return_value + # Return empty sets to stop loops + mock_tree_instance.add_children.return_value = set() + + main() + + # Verify add_children was called (logic inside main triggers this based on args.descend) + assert mock_tree_instance.add_children.called diff --git a/tests/test_session.py b/tests/test_session.py new file mode 100644 index 0000000..edd7757 --- /dev/null +++ b/tests/test_session.py @@ -0,0 +1,85 @@ +from unittest.mock import MagicMock, patch + +import pytest +from requests.exceptions import HTTPError + +from getmyancestors.classes.session import Session + + +class TestSession: + + @patch("requests.Session.get") + @patch("requests.Session.post") + def test_login_success(self, mock_post, mock_get): + """Test the full OAuth2 login flow with successful token retrieval.""" + # Setup the sequence of responses for the login flow + mock_get.side_effect = [ + MagicMock(cookies={"XSRF-TOKEN": "abc"}), # 1. Initial page load + MagicMock(status_code=200), # 3. Redirect URL page + MagicMock( + headers={"location": "http://callback?code=123"} + ), # 4. Auth callback + ] + + # Mock the JSON response for the login POST + mock_post.side_effect = [ + MagicMock(json=lambda: {"redirectUrl": "http://auth.url"}), # 2. Login POST + MagicMock(json=lambda: {"access_token": "fake_token"}), # 5. Token POST + ] + + # Initialize session (triggers login) + session = Session("user", "pass", verbose=True) + + assert session.logged is True + assert session.headers["Authorization"] == "Bearer fake_token" + + @patch("requests.Session.get") + @patch("requests.Session.post") + def test_login_failure_bad_creds(self, mock_post, mock_get): + """Test login failure when credentials are rejected.""" + mock_get.return_value.cookies = {"XSRF-TOKEN": "abc"} + + # Simulate login error response + mock_post.return_value.json.return_value = {"loginError": "Invalid credentials"} + + session = Session("user", "badpass") + + # Should not have session cookie or auth header + assert session.logged is False + assert "Authorization" not in session.headers + + @patch("getmyancestors.classes.session.Session.login") # Prevent auto-login in init + def test_get_url_401_retry(self, mock_login): + """Test that a 401 response triggers a re-login and retry.""" + session = Session("u", "p") + + # Mock Session.get directly on the instance to control responses + with patch.object(session, "get") as mock_request_get: + # First call 401, Second call 200 OK + mock_request_get.side_effect = [ + MagicMock(status_code=401), + MagicMock(status_code=200, json=lambda: {"data": "success"}), + ] + + result = session.get_url("/test-endpoint") + + assert mock_login.call_count == 2 # Once init, Once after 401 + assert result == {"data": "success"} + + @patch("getmyancestors.classes.session.Session.login") + def test_get_url_403_ordinances(self, mock_login): + """Test handling of 403 Forbidden specifically for ordinances.""" + session = Session("u", "p") + + with patch.object(session, "get") as mock_request_get: + response_403 = MagicMock(status_code=403) + response_403.json.return_value = { + "errors": [{"message": "Unable to get ordinances."}] + } + response_403.raise_for_status.side_effect = HTTPError("403 Client Error") + + mock_request_get.return_value = response_403 + + result = session.get_url("/test-ordinances") + + assert result == "error" diff --git a/tests/test_tree.py b/tests/test_tree.py new file mode 100644 index 0000000..f2de14e --- /dev/null +++ b/tests/test_tree.py @@ -0,0 +1,68 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from getmyancestors.classes.tree import Indi, Tree + + +class TestTree: + + @pytest.fixture + def mock_session(self, mock_user_data): + session = MagicMock() + session.fid = "KW7V-Y32" + session.get_url.return_value = mock_user_data + session._ = lambda s: s # Mock translation identity function + return session + + def test_add_indis(self, mock_session, mock_person_data): + """Test adding a list of individuals to the tree.""" + tree = Tree(mock_session) + + # Mock the API call for person details + mock_session.get_url.side_effect = [ + mock_person_data, # For person details + None, # For child relationships (empty for this test) + ] + + tree.add_indis(["KW7V-Y32"]) + + assert "KW7V-Y32" in tree.indi + person = tree.indi["KW7V-Y32"] + assert person.name == "John Doe" + assert person.sex == "M" + + def test_add_parents(self, mock_session): + """Test fetching parents creates family links.""" + tree = Tree(mock_session) + child_id = "KW7V-CHILD" + father_id = "KW7V-DAD" + mother_id = "KW7V-MOM" + + # Seed child in tree + tree.indi[child_id] = Indi(child_id, tree) + + # Mock parent relationship response + mock_session.get_url.return_value = { + "childAndParentsRelationships": [ + { + "father": {"resourceId": father_id}, + "mother": {"resourceId": mother_id}, + "fatherFacts": [{"type": "http://gedcomx.org/BiologicalParent"}], + "motherFacts": [{"type": "http://gedcomx.org/BiologicalParent"}], + } + ] + } + + # Mock fetching the actual parent person objects + # We patch add_indis to avoid the recursive fetch details logic for this unit test + with patch.object(tree, "add_indis") as mock_add_indis: + result = tree.add_parents({child_id}) + + assert father_id in result + assert mother_id in result + + # Verify family object creation + fam_key = (tree.indi[father_id], tree.indi[mother_id]) + assert fam_key in tree.fam + assert tree.indi[child_id] in tree.fam[fam_key].children -- 2.52.0