|
| 1 | +# The Okta software accompanied by this notice is provided pursuant to the following terms: |
| 2 | +# Copyright © 2026-Present, Okta, Inc. |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. |
| 4 | +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. |
| 5 | +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 6 | +# See the License for the specific language governing permissions and limitations under the License. |
| 7 | + |
| 8 | +"""Tests for get_application — resilient parsing of a single app record (#48).""" |
| 9 | + |
| 10 | +from __future__ import annotations |
| 11 | + |
| 12 | +import json |
| 13 | +from unittest.mock import AsyncMock, MagicMock, patch |
| 14 | + |
| 15 | +import okta.models as okta_models |
| 16 | +import pytest |
| 17 | + |
| 18 | +from okta_mcp_server.tools.applications.applications import get_application |
| 19 | + |
| 20 | + |
| 21 | +# A bookmark app parses cleanly into a typed model. |
| 22 | +GOOD_BOOKMARK = { |
| 23 | + "id": "0oaBOOKMARK001", |
| 24 | + "label": "Bookmark App", |
| 25 | + "name": "bookmark", |
| 26 | + "signOnMode": "BOOKMARK", |
| 27 | + "settings": {"app": {"url": "https://example.com"}}, |
| 28 | +} |
| 29 | + |
| 30 | +# An App Catalog SAML app with a partial settings.signOn fails strict SDK |
| 31 | +# validation (the model marks ~15 signOn fields required) — the record that |
| 32 | +# previously made get_application fail outright (#48). |
| 33 | +BAD_SPARSE_SAML = { |
| 34 | + "id": "0oaSAML0001", |
| 35 | + "label": "Sparse SAML", |
| 36 | + "name": "sparsesaml", |
| 37 | + "signOnMode": "SAML_2_0", |
| 38 | + "settings": {"signOn": {"defaultRelayState": ""}}, |
| 39 | +} |
| 40 | + |
| 41 | + |
| 42 | +def _make_ctx(): |
| 43 | + manager = MagicMock() |
| 44 | + ctx = MagicMock() |
| 45 | + ctx.request_context.lifespan_context.okta_auth_manager = manager |
| 46 | + return ctx |
| 47 | + |
| 48 | + |
| 49 | +def _client_returning(body, execute_error=None): |
| 50 | + """Build a fake Okta client whose request executor returns the given record.""" |
| 51 | + executor = MagicMock() |
| 52 | + executor.create_request = AsyncMock(return_value=({"method": "GET"}, None)) |
| 53 | + if execute_error is not None: |
| 54 | + executor.execute = AsyncMock(return_value=(None, None, execute_error)) |
| 55 | + else: |
| 56 | + executor.execute = AsyncMock(return_value=(MagicMock(), body, None)) |
| 57 | + client = MagicMock() |
| 58 | + client.get_request_executor = MagicMock(return_value=executor) |
| 59 | + return client |
| 60 | + |
| 61 | + |
| 62 | +class TestGetApplicationResilientParsing: |
| 63 | + @pytest.mark.asyncio |
| 64 | + @patch("okta_mcp_server.tools.applications.applications.get_okta_client") |
| 65 | + async def test_good_app_returns_typed_model(self, mock_get_client): |
| 66 | + mock_get_client.return_value = _client_returning(json.dumps(GOOD_BOOKMARK)) |
| 67 | + |
| 68 | + result = await get_application(_make_ctx(), "0oaBOOKMARK001") |
| 69 | + |
| 70 | + assert isinstance(result, okta_models.BookmarkApplication) |
| 71 | + |
| 72 | + @pytest.mark.asyncio |
| 73 | + @patch("okta_mcp_server.tools.applications.applications.get_okta_client") |
| 74 | + async def test_non_conforming_app_falls_back_to_raw_dict(self, mock_get_client): |
| 75 | + mock_get_client.return_value = _client_returning(json.dumps(BAD_SPARSE_SAML)) |
| 76 | + |
| 77 | + result = await get_application(_make_ctx(), "0oaSAML0001") |
| 78 | + |
| 79 | + assert isinstance(result, dict) |
| 80 | + assert result["id"] == "0oaSAML0001" |
| 81 | + assert "_deserialization_warning" in result |
| 82 | + |
| 83 | + @pytest.mark.asyncio |
| 84 | + @patch("okta_mcp_server.tools.applications.applications.get_okta_client") |
| 85 | + async def test_executor_error_is_returned(self, mock_get_client): |
| 86 | + mock_get_client.return_value = _client_returning(None, execute_error="Error: 404 not found") |
| 87 | + |
| 88 | + result = await get_application(_make_ctx(), "0oaMISSING") |
| 89 | + |
| 90 | + assert result == {"error": "Error: 404 not found"} |
| 91 | + |
| 92 | + @pytest.mark.asyncio |
| 93 | + @patch("okta_mcp_server.tools.applications.applications.get_okta_client") |
| 94 | + async def test_expand_is_sent_as_query_param(self, mock_get_client): |
| 95 | + client = _client_returning(json.dumps(GOOD_BOOKMARK)) |
| 96 | + mock_get_client.return_value = client |
| 97 | + |
| 98 | + await get_application(_make_ctx(), "0oaBOOKMARK001", expand="user/abc") |
| 99 | + |
| 100 | + url = client.get_request_executor.return_value.create_request.call_args.kwargs["url"] |
| 101 | + assert "/api/v1/apps/0oaBOOKMARK001" in url |
| 102 | + assert "expand=user" in url |
0 commit comments