--- /dev/null
+source .venv/bin/activate
+unset PS1
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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"}]}],
+ }
+ ]
+ }
--- /dev/null
+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
--- /dev/null
+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"
--- /dev/null
+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