From ea268aaf8288a93f328392307451753a80eee2af Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 14:06:45 -0400 Subject: [PATCH 01/11] adding settings tests --- tests/conftest.py | 14 ++++++++++++-- tests/test_settings.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/test_settings.py diff --git a/tests/conftest.py b/tests/conftest.py index 296bf77..57b3cd0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,11 +11,21 @@ def runner(): def mock_env_vars(monkeypatch): monkeypatch.setenv("SN_USER_NAME", "user") monkeypatch.setenv("SN_PASSWORD", "password") - monkeypatch.setenv("SN_SET_USE_OAUTH", "False") + monkeypatch.setenv("SN_SET_USE_OAUTH", "false") @pytest.fixture def mock_empty_env_vars(monkeypatch): monkeypatch.setenv("SN_USER_NAME", "user") monkeypatch.setenv("SN_PASSWORD", "") - monkeypatch.setenv("SN_SET_USE_OAUTH", "False") + monkeypatch.setenv("SN_SET_USE_OAUTH", "false") + + +@pytest.fixture +def mock_oauth_env_vars(monkeypatch): + monkeypatch.setenv("SN_USER_NAME", "abc123") + monkeypatch.setenv("SN_PASSWORD", "super-secret") + monkeypatch.setenv("SN_SET_USE_OAUTH", "true") + monkeypatch.setenv("SN_SET_CLIENT_ID", "client_id") + monkeypatch.setenv("SN_SET_CLIENT_SECRET", "super-secure") + monkeypatch.setenv("SN_SET_GRANT_TYPE", "invalid") diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..bd65144 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,13 @@ +from sn_set.settings import Settings + + +class TestSettings: + def test_oauth_settings(self, mock_oauth_env_vars): + test_settings = Settings() + + assert test_settings.get_user() == "abc123" + assert test_settings.get_password() == "super-secret" + assert test_settings.get_use_oauth() + assert test_settings.get_client_id() == "client_id" + assert test_settings.get_client_secret() == "super-secure" + assert test_settings.get_grant_type() == "password" From 0e60d68dd8472953df6b83df4038a59edb85fe40 Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 14:10:30 -0400 Subject: [PATCH 02/11] added basic tests --- tests/test_settings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_settings.py b/tests/test_settings.py index bd65144..9193e82 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -11,3 +11,13 @@ def test_oauth_settings(self, mock_oauth_env_vars): assert test_settings.get_client_id() == "client_id" assert test_settings.get_client_secret() == "super-secure" assert test_settings.get_grant_type() == "password" + + def test_basic_settings(self, mock_env_vars): + test_settings = Settings() + + assert test_settings.get_user() == "user" + assert test_settings.get_password() == "password" + assert not test_settings.get_use_oauth() + assert test_settings.get_client_id() is None + assert test_settings.get_client_secret() is None + assert test_settings.get_grant_type() is None From fb1f16486b071ee02d3cda982f3e432a36f265e0 Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 14:21:54 -0400 Subject: [PATCH 03/11] added short-circuit test --- tests/test_cli.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 1c64340..f34b851 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -280,3 +280,50 @@ def test_to_excel_valid(test_value, expected_value, runner): @pytest.mark.parametrize("test_value", [None, {}, [], 1]) def test_to_excel_invalid(test_value): assert cli.to_excel(test_value, None) is False + + +@mock.patch("sn_set.cli.get_set_diff") +@mock.patch("sn_set.cli.get_install_order_new") +@mock.patch("sn_set.cli.get_install_order") +@mock.patch("sn_set.cli.get_update_sets") +def test_cli_short_circuit( + mock_get_update_sets, + mock_get_install_order, + mock_new_install_order, + mock_set_diff, + runner, +): + mock_set1 = [ + {"name": "a set", "sys_id": "12345"}, + {"name": "b set", "sys_id": "54321"}, + ] + mock_set2 = [{"name": "a set", "sys_id": "54321"}] + mock_install_order = [{"name": "b set", "sys_id": "12345"}] + + mock_get_update_sets.side_effect = [mock_set1, mock_set2] + mock_get_install_order.return_value = mock_install_order + mock_set_diff.side_effect = [["b set"], []] + + test_source = "nyudev" + test_target = "nyuqa" + result = runner.invoke( + cli.main, ["--source", test_source, "--target", test_target, "--short"] + ) + + mock_get_update_sets.assert_any_call("nyudev") + mock_get_update_sets.assert_any_call("nyuqa") + mock_set_diff.assert_has_calls( + [ + mock.call(["a set", "b set"], ["a set"], debug=False), + mock.call(["b set"], ["b set"]), + ] + ) + mock_new_install_order.assert_not_called() + + assert result.exit_code == 0 + assert not result.exception + assert ( + f"Begin retrieving update sets from " + f"source: {test_source} and target: {test_target}" + ) in result.output + assert "Short circuiting" in result.output From c7d722c00e4075fc8ed22801c5037ad7a36dacfd Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 14:33:55 -0400 Subject: [PATCH 04/11] added client_factory test for required base_url --- tests/test_requests_lib.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_requests_lib.py b/tests/test_requests_lib.py index 05494c7..cdc83ed 100644 --- a/tests/test_requests_lib.py +++ b/tests/test_requests_lib.py @@ -285,3 +285,10 @@ def mock_make_request(instance_name, path_params): monkeypatch.setattr(requests_lib, "get_install_order_new", mock_make_request) r = requests_lib.get_install_order_new("nyudev", test_input) assert r == expected + + +def test_client_factory_no_base_url(mock_oauth_env_vars): + from sn_set.requests_lib import client_factory + + with pytest.raises(ValueError, match="base_url must be specified"): + client_factory() From 800a3060547370717f86ff7c8573562a6a5dbd97 Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 14:35:25 -0400 Subject: [PATCH 05/11] added test for missing password --- tests/test_requests_lib.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_requests_lib.py b/tests/test_requests_lib.py index cdc83ed..2fd43bc 100644 --- a/tests/test_requests_lib.py +++ b/tests/test_requests_lib.py @@ -292,3 +292,10 @@ def test_client_factory_no_base_url(mock_oauth_env_vars): with pytest.raises(ValueError, match="base_url must be specified"): client_factory() + + +def test_client_factory_missing_password(mock_empty_env_vars): + from sn_set.requests_lib import client_factory + + with pytest.raises(ValueError, match="Username or Password is empty"): + client_factory(base_url="https://test.com") From fb3a41bcf3922a3f8fe9404e1ac7e4634ad041c8 Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 14:38:30 -0400 Subject: [PATCH 06/11] added test for missing username --- tests/test_requests_lib.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_requests_lib.py b/tests/test_requests_lib.py index 2fd43bc..003294a 100644 --- a/tests/test_requests_lib.py +++ b/tests/test_requests_lib.py @@ -299,3 +299,13 @@ def test_client_factory_missing_password(mock_empty_env_vars): with pytest.raises(ValueError, match="Username or Password is empty"): client_factory(base_url="https://test.com") + + +def test_client_factory_missing_username(monkeypatch): + monkeypatch.setenv("SN_USER_NAME", "") + monkeypatch.setenv("SN_PASSWORD", "password") + monkeypatch.setenv("SN_SET_USE_OAUTH", "false") + from sn_set.requests_lib import client_factory + + with pytest.raises(ValueError, match="Username or Password is empty"): + client_factory(base_url="https://test.com") From 7af18d5b93e4289ceb0fbc7252efb37a7687283e Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 14:47:05 -0400 Subject: [PATCH 07/11] added tests for invalid oauth2 values --- tests/test_requests_lib.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_requests_lib.py b/tests/test_requests_lib.py index 003294a..1867a6f 100644 --- a/tests/test_requests_lib.py +++ b/tests/test_requests_lib.py @@ -309,3 +309,24 @@ def test_client_factory_missing_username(monkeypatch): with pytest.raises(ValueError, match="Username or Password is empty"): client_factory(base_url="https://test.com") + + +@pytest.mark.parametrize( + "test_client_id,test_client_secret", {("", "client-secret"), ("client_id", "")} +) +def test_client_factory_oauth_settings_check( + test_client_id, test_client_secret, monkeypatch +): + monkeypatch.setenv("SN_USER_NAME", "abc123") + monkeypatch.setenv("SN_PASSWORD", "PASSWORD") + monkeypatch.setenv("SN_SET_USE_OAUTH", "true") + monkeypatch.setenv("SN_SET_CLIENT_ID", test_client_id) + monkeypatch.setenv("SN_SET_CLIENT_SECRET", test_client_secret) + + from sn_set.requests_lib import client_factory + + with pytest.raises( + ValueError, + match="Client ID, Client Secret, and Grant Type are required to use OAuth2", + ): + client_factory(base_url="https://test.com") From c979d03b105b5b63385094925500593bcf456c27 Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 16:30:15 -0400 Subject: [PATCH 08/11] added test for happy path for client_factory and oauth --- tests/test_requests_lib.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_requests_lib.py b/tests/test_requests_lib.py index 1867a6f..ffde101 100644 --- a/tests/test_requests_lib.py +++ b/tests/test_requests_lib.py @@ -330,3 +330,27 @@ def test_client_factory_oauth_settings_check( match="Client ID, Client Secret, and Grant Type are required to use OAuth2", ): client_factory(base_url="https://test.com") + + +def test_client_factory_oauth_client(mock_oauth_env_vars, monkeypatch): + from authlib.integrations.requests_client import OAuth2Session + + def mock_fetch_token(*args, **kwargs): + return None + + monkeypatch.setattr(OAuth2Session, "fetch_token", mock_fetch_token) + + from sn_set.requests_lib import client_factory + + test_base_url: str = "https://test.com" + + client, auth = client_factory(base_url=test_base_url) + + assert client is not None + assert auth is None + assert isinstance(client, OAuth2Session) + + from sn_set.requests_lib import context + + assert context[test_base_url] is not None + assert context[test_base_url]["client"] == client From 618da9c4e15de2c71265be00af23a4e675e56195 Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 16:41:34 -0400 Subject: [PATCH 09/11] added happy-path test for client_factory and basic auth --- tests/test_requests_lib.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_requests_lib.py b/tests/test_requests_lib.py index ffde101..2a2b158 100644 --- a/tests/test_requests_lib.py +++ b/tests/test_requests_lib.py @@ -354,3 +354,27 @@ def mock_fetch_token(*args, **kwargs): assert context[test_base_url] is not None assert context[test_base_url]["client"] == client + + del context[test_base_url] + + +def test_client_factory_basic_auth_client(mock_env_vars): + import requests + + from sn_set.requests_lib import client_factory + + test_base_url: str = "https://test.com" + + client, auth = client_factory(base_url=test_base_url) + + assert client is not None + assert auth is not None + assert isinstance(auth, requests.auth.HTTPBasicAuth) + + from sn_set.requests_lib import context + + assert context[test_base_url] is not None + assert context[test_base_url]["client"] == client + assert context[test_base_url]["auth"] == auth + + del context[test_base_url] From 00edac4b35eff488ae8682eb00fbe586f4df3419 Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 16:43:50 -0400 Subject: [PATCH 10/11] comments --- tests/test_requests_lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_requests_lib.py b/tests/test_requests_lib.py index 2a2b158..d554967 100644 --- a/tests/test_requests_lib.py +++ b/tests/test_requests_lib.py @@ -355,6 +355,7 @@ def mock_fetch_token(*args, **kwargs): assert context[test_base_url] is not None assert context[test_base_url]["client"] == client + # clean up after test del context[test_base_url] @@ -377,4 +378,5 @@ def test_client_factory_basic_auth_client(mock_env_vars): assert context[test_base_url]["client"] == client assert context[test_base_url]["auth"] == auth + # clean up after test del context[test_base_url] From 9f83b6b365a6424c49ab2d2ed4e693e228955c2a Mon Sep 17 00:00:00 2001 From: Alex Biehl Date: Tue, 12 May 2026 16:51:58 -0400 Subject: [PATCH 11/11] added AI-generated tests for get_install_order_new --- tests/test_requests_lib.py | 88 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/test_requests_lib.py b/tests/test_requests_lib.py index d554967..945abb9 100644 --- a/tests/test_requests_lib.py +++ b/tests/test_requests_lib.py @@ -380,3 +380,91 @@ def test_client_factory_basic_auth_client(mock_env_vars): # clean up after test del context[test_base_url] + + +@mock.patch("sn_set.requests_lib.make_request") +def test_get_install_order_new_400_fallback(mock_make_request): + """ + Tests that get_install_order_new handles 400/414 errors by + splitting the request into individual calls. + """ + # Payload with different dates to verify sorting in fallback + payload_a = [{"name": "set_a", "sys_updated_on": "2021-05-10 12:00:00"}] + payload_b = [{"name": "set_b", "sys_updated_on": "2021-05-05 12:00:00"}] + + mocked_error = HTTPError(response=mock.Mock(status_code=400)) + + # Side effect: 1. Fail initial bulk call, 2. Return set_a, 3. Return set_b + mock_make_request.side_effect = [mocked_error, payload_a, payload_b] + + from sn_set.requests_lib import get_install_order_new + + # We expect the result to be sorted by sys_updated_on (set_b comes first) + result = get_install_order_new("nyudev", ["set_a", "set_b"]) + + assert len(result) == 2 + assert result[0]["name"] == "set_b" + assert result[1]["name"] == "set_a" + + # Verify we made exactly 3 calls + assert mock_make_request.call_count == 3 + + # Verify the fallback query structure for the first individual item + args, kwargs = mock_make_request.call_args_list[1] + assert "name=set_a" in kwargs["path_params"]["sysparm_query"] + assert "installed_fromISEMPTY" in kwargs["path_params"]["sysparm_query"] + + +@pytest.mark.parametrize("status_code", [401, 404, 500]) +@mock.patch("sn_set.requests_lib.make_request") +def test_get_install_order_new_other_errors(mock_make_request, status_code): + """ + Tests that non-400/414 errors are not caught by the fallback + and are re-raised. + """ + mock_error = HTTPError(response=mock.Mock(status_code=status_code)) + mock_make_request.side_effect = mock_error + + from sn_set.requests_lib import get_install_order_new + + with pytest.raises(HTTPError): + get_install_order_new("nyudev", ["set_a"]) + + +def test_get_install_order_new_ordering_logic(): + """ + Directly tests that the order_sets call inside get_install_order_new + uses the correct field (sys_updated_on) compared to the old function. + """ + mock_payload = [ + {"name": "Latest", "sys_updated_on": "2021-12-31 23:59:59"}, + {"name": "Earliest", "sys_updated_on": "2021-01-01 00:00:00"}, + ] + + from sn_set.requests_lib import order_sets + + result = order_sets(mock_payload, order_by_field="sys_updated_on") + + assert result[0]["name"] == "Earliest" + assert result[1]["name"] == "Latest" + + +@mock.patch("sn_set.requests_lib.make_request") +def test_get_install_order_new_fields_verification(mock_make_request): + """ + Verifies that the function requests the specific fields + required for new update sets. + """ + mock_make_request.return_value = [] + from sn_set.requests_lib import get_install_order_new + + get_install_order_new("nyudev", ["test_set"]) + + args, kwargs = mock_make_request.call_args + requested_fields = kwargs["path_params"]["sysparm_fields"].split(",") + + # Should NOT have commit_date (as it's commented out in your source) + assert "commit_date" not in requested_fields + # Should have core fields + assert "sys_updated_on" in requested_fields + assert "name" in requested_fields