From 375fdbdceb006c2e24737507ea77c22240e6140f Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 15 Apr 2026 15:39:43 -0400 Subject: [PATCH 01/33] adding token dispenser to get harmony client --- bignbit/utils.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/bignbit/utils.py b/bignbit/utils.py index 5e5f8e6..ff9d236 100644 --- a/bignbit/utils.py +++ b/bignbit/utils.py @@ -4,8 +4,8 @@ import os import pathlib import re +import base64 from datetime import datetime, timedelta - from typing import Any import boto3 import requests @@ -325,9 +325,39 @@ def get_harmony_client(environment_str: str) -> Client: HARMONY_CLIENT = None if not HARMONY_CLIENT: + # Create a boto3 Lambda client for token retrieval + lambda_client = boto3.client('lambda') + + # Retrieve EDL credentials (username/password) + edl_user, edl_pass = get_edl_creds() + + # Prepare payload to request an access token from the Lambda "token-dispenser" + payload = { + "action": "edl", + "edl_user": edl_user, + "edl_pass": edl_pass, + "edl_env": environment_str, + "minimum_alive_secs": 300 # keep token valid for at least 5 minutes + } + encoded_payload = base64.b64encode( + json.dumps(payload).encode("utf-8") + ).decode("utf-8") + + # Invoke the Lambda synchronously and get the token + response = lambda_client.invoke( + FunctionName='sndbx-launchpad_token_dispenser', + InvocationType='RequestResponse', # wait for response + Payload=encoded_payload + ) + + # Read the payload from Lambda response and parse JSON + response_payload = response['Payload'].read() + token = json.loads(response_payload)['access-token'] + + # Instantiate Harmony client with retrieved token HARMONY_CLIENT = Client( env=harmony_environ, - auth=get_edl_creds(), + token=token, should_validate_auth=HARMONY_SHOULD_VALIDATE_AUTH ) From 6474a30acd98443eb22d6374431949957819da75 Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 15 Apr 2026 15:50:40 -0400 Subject: [PATCH 02/33] pull back 1 rev to test develop base in CICD --- bignbit/utils.py | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/bignbit/utils.py b/bignbit/utils.py index ff9d236..5e5f8e6 100644 --- a/bignbit/utils.py +++ b/bignbit/utils.py @@ -4,8 +4,8 @@ import os import pathlib import re -import base64 from datetime import datetime, timedelta + from typing import Any import boto3 import requests @@ -325,39 +325,9 @@ def get_harmony_client(environment_str: str) -> Client: HARMONY_CLIENT = None if not HARMONY_CLIENT: - # Create a boto3 Lambda client for token retrieval - lambda_client = boto3.client('lambda') - - # Retrieve EDL credentials (username/password) - edl_user, edl_pass = get_edl_creds() - - # Prepare payload to request an access token from the Lambda "token-dispenser" - payload = { - "action": "edl", - "edl_user": edl_user, - "edl_pass": edl_pass, - "edl_env": environment_str, - "minimum_alive_secs": 300 # keep token valid for at least 5 minutes - } - encoded_payload = base64.b64encode( - json.dumps(payload).encode("utf-8") - ).decode("utf-8") - - # Invoke the Lambda synchronously and get the token - response = lambda_client.invoke( - FunctionName='sndbx-launchpad_token_dispenser', - InvocationType='RequestResponse', # wait for response - Payload=encoded_payload - ) - - # Read the payload from Lambda response and parse JSON - response_payload = response['Payload'].read() - token = json.loads(response_payload)['access-token'] - - # Instantiate Harmony client with retrieved token HARMONY_CLIENT = Client( env=harmony_environ, - token=token, + auth=get_edl_creds(), should_validate_auth=HARMONY_SHOULD_VALIDATE_AUTH ) From 2290c228565be116bff7e0c4d91dc6af65a3f552 Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 15 Apr 2026 16:07:36 -0400 Subject: [PATCH 03/33] add current region to boto3.client call --- bignbit/utils.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/bignbit/utils.py b/bignbit/utils.py index 5e5f8e6..ae5906b 100644 --- a/bignbit/utils.py +++ b/bignbit/utils.py @@ -4,6 +4,7 @@ import os import pathlib import re +import base64 from datetime import datetime, timedelta from typing import Any @@ -325,9 +326,42 @@ def get_harmony_client(environment_str: str) -> Client: HARMONY_CLIENT = None if not HARMONY_CLIENT: + # Create a boto3 Lambda client for token retrieval + session = boto3.session.Session() + region = session.region_name + + lambda_client = boto3.client('lambda', region_name=region) + + # Retrieve EDL credentials (username/password) + edl_user, edl_pass = get_edl_creds() + + # Prepare payload to request an access token from the Lambda "token-dispenser" + payload = { + "action": "edl", + "edl_user": edl_user, + "edl_pass": edl_pass, + "edl_env": environment_str, + "minimum_alive_secs": 300 # keep token valid for at least 5 minutes + } + encoded_payload = base64.b64encode( + json.dumps(payload).encode("utf-8") + ).decode("utf-8") + + # Invoke the Lambda synchronously and get the token + response = lambda_client.invoke( + FunctionName='sndbx-launchpad_token_dispenser', + InvocationType='RequestResponse', # wait for response + Payload=encoded_payload + ) + + # Read the payload from Lambda response and parse JSON + response_payload = json.loads(response["Payload"].read()) + token = json.loads(response_payload)['access-token'] + + # Instantiate Harmony client with retrieved token HARMONY_CLIENT = Client( env=harmony_environ, - auth=get_edl_creds(), + token=token, should_validate_auth=HARMONY_SHOULD_VALIDATE_AUTH ) From 50ba51c86bd1def8b3fe4368504c1496a8ef7e2b Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 15 Apr 2026 16:15:46 -0400 Subject: [PATCH 04/33] add definition for region --- bignbit/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bignbit/utils.py b/bignbit/utils.py index ae5906b..fcae7de 100644 --- a/bignbit/utils.py +++ b/bignbit/utils.py @@ -328,7 +328,11 @@ def get_harmony_client(environment_str: str) -> Client: if not HARMONY_CLIENT: # Create a boto3 Lambda client for token retrieval session = boto3.session.Session() - region = session.region_name + region = ( + os.environ.get("AWS_REGION") or + os.environ.get("AWS_DEFAULT_REGION") or + "us-west-2" + ) lambda_client = boto3.client('lambda', region_name=region) From 1d0f862f3e3bcbeec90bf28dd64b51836af9fc1c Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 15 Apr 2026 16:20:57 -0400 Subject: [PATCH 05/33] add additional region definitions --- bignbit/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bignbit/utils.py b/bignbit/utils.py index fcae7de..ba15af2 100644 --- a/bignbit/utils.py +++ b/bignbit/utils.py @@ -331,6 +331,7 @@ def get_harmony_client(environment_str: str) -> Client: region = ( os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") or + session.region_name or "us-west-2" ) From 4c56094df102249d0b9f3845365c0b64b7c6ac4d Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 15 Apr 2026 16:36:14 -0400 Subject: [PATCH 06/33] adding mock for boto3.client tests --- tests/test_utils.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 85461b0..68390f1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -468,14 +468,18 @@ def test_parse_doy_leap_day(): # Tests for get_harmony_client() # --------------------------------------------------------------------------- +@patch('boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_uat(mock_get_edl_creds, mock_client): +def test_get_harmony_client_uat(mock_get_edl_creds, mock_client, mock_boto): """Test getting Harmony client for UAT environment.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') mock_client_instance = MagicMock() mock_client.return_value = mock_client_instance + mock_client = MagicMock() + mock_boto.return_value = mock_client + # Reset global client import bignbit.utils bignbit.utils.HARMONY_CLIENT = None @@ -486,14 +490,18 @@ def test_get_harmony_client_uat(mock_get_edl_creds, mock_client): mock_client.assert_called_once() +@patch('boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_prod(mock_get_edl_creds, mock_client): +def test_get_harmony_client_prod(mock_get_edl_creds, mock_client, mock_boto): """Test getting Harmony client for PROD environment.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') mock_client_instance = MagicMock() mock_client.return_value = mock_client_instance + mock_client = MagicMock() + mock_boto.return_value = mock_client + # Reset global client import bignbit.utils bignbit.utils.HARMONY_CLIENT = None @@ -503,14 +511,18 @@ def test_get_harmony_client_prod(mock_get_edl_creds, mock_client): assert result == mock_client_instance +@patch('boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_client): +def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_client, mock_boto): """Test that SIT environment defaults to UAT.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') mock_client_instance = MagicMock() mock_client.return_value = mock_client_instance + mock_client = MagicMock() + mock_boto.return_value = mock_client + # Reset global client import bignbit.utils bignbit.utils.HARMONY_CLIENT = None From 4936481c49ed8d1baf767f77b7cef265c2ec9003 Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 15 Apr 2026 16:44:32 -0400 Subject: [PATCH 07/33] adjusting mock for boto3.client --- tests/test_utils.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 68390f1..0132c48 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -477,8 +477,14 @@ def test_get_harmony_client_uat(mock_get_edl_creds, mock_client, mock_boto): mock_client_instance = MagicMock() mock_client.return_value = mock_client_instance - mock_client = MagicMock() - mock_boto.return_value = mock_client + mock_client.get_parameter.return_value = { + "Parameter": { + "Value": json.dumps({ + "username": "test_user", + "password": "test_pass" + }) + } + } # Reset global client import bignbit.utils @@ -499,8 +505,14 @@ def test_get_harmony_client_prod(mock_get_edl_creds, mock_client, mock_boto): mock_client_instance = MagicMock() mock_client.return_value = mock_client_instance - mock_client = MagicMock() - mock_boto.return_value = mock_client + mock_client.get_parameter.return_value = { + "Parameter": { + "Value": json.dumps({ + "username": "test_user", + "password": "test_pass" + }) + } + } # Reset global client import bignbit.utils @@ -520,8 +532,14 @@ def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_client, mock_client_instance = MagicMock() mock_client.return_value = mock_client_instance - mock_client = MagicMock() - mock_boto.return_value = mock_client + mock_client.get_parameter.return_value = { + "Parameter": { + "Value": json.dumps({ + "username": "test_user", + "password": "test_pass" + }) + } + } # Reset global client import bignbit.utils From 923eb3ce3578b29a97cb6b2df4fabb6373cc53c9 Mon Sep 17 00:00:00 2001 From: David Wood Date: Wed, 15 Apr 2026 17:00:14 -0400 Subject: [PATCH 08/33] adjusting mock for boto3.client --- tests/test_utils.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 0132c48..956f8ca 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -471,13 +471,19 @@ def test_parse_doy_leap_day(): @patch('boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_uat(mock_get_edl_creds, mock_client, mock_boto): +def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): """Test getting Harmony client for UAT environment.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') + + # --- Harmony client mock --- mock_client_instance = MagicMock() - mock_client.return_value = mock_client_instance + mock_harmony_client.return_value = mock_client_instance + + # --- boto3 SSM mock --- + mock_ssm = MagicMock() + mock_boto.return_value = mock_ssm - mock_client.get_parameter.return_value = { + mock_ssm.get_parameter.return_value = { "Parameter": { "Value": json.dumps({ "username": "test_user", @@ -486,26 +492,31 @@ def test_get_harmony_client_uat(mock_get_edl_creds, mock_client, mock_boto): } } - # Reset global client import bignbit.utils bignbit.utils.HARMONY_CLIENT = None result = get_harmony_client('UAT') assert result == mock_client_instance - mock_client.assert_called_once() + mock_harmony_client.assert_called_once() @patch('boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_prod(mock_get_edl_creds, mock_client, mock_boto): +def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_boto): """Test getting Harmony client for PROD environment.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') + + # --- Harmony client mock --- mock_client_instance = MagicMock() - mock_client.return_value = mock_client_instance + mock_harmony_client.return_value = mock_client_instance - mock_client.get_parameter.return_value = { + # --- boto3 SSM mock --- + mock_ssm = MagicMock() + mock_boto.return_value = mock_ssm + + mock_ssm.get_parameter.return_value = { "Parameter": { "Value": json.dumps({ "username": "test_user", @@ -514,25 +525,31 @@ def test_get_harmony_client_prod(mock_get_edl_creds, mock_client, mock_boto): } } - # Reset global client import bignbit.utils bignbit.utils.HARMONY_CLIENT = None result = get_harmony_client('PROD') assert result == mock_client_instance + mock_harmony_client.assert_called_once() @patch('boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_client, mock_boto): +def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): """Test that SIT environment defaults to UAT.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') + + # --- Harmony client mock --- mock_client_instance = MagicMock() - mock_client.return_value = mock_client_instance + mock_harmony_client.return_value = mock_client_instance + + # --- boto3 SSM mock --- + mock_ssm = MagicMock() + mock_boto.return_value = mock_ssm - mock_client.get_parameter.return_value = { + mock_ssm.get_parameter.return_value = { "Parameter": { "Value": json.dumps({ "username": "test_user", @@ -541,7 +558,6 @@ def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_client, } } - # Reset global client import bignbit.utils bignbit.utils.HARMONY_CLIENT = None From ba6696bf9e133373808dff83e0715d2df6d5d894 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 08:36:12 -0400 Subject: [PATCH 09/33] addinging boto mock for testing --- tests/test_handle_big_result.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/test_handle_big_result.py b/tests/test_handle_big_result.py index b4617dd..efdb97e 100644 --- a/tests/test_handle_big_result.py +++ b/tests/test_handle_big_result.py @@ -5,6 +5,7 @@ import boto3 import pytest from moto import mock_s3 +from unittest.mock import patch, MagicMock import bignbit.utils from bignbit.handle_big_result import ( @@ -19,8 +20,16 @@ @pytest.mark.vcr @mock_s3 -def test_process_harmony_results(): +@patch("boto3.client") +def test_process_harmony_results(mock_boto): """Test pulling results of a harmony job from s3.""" + mock_lambda = MagicMock() + mock_boto.return_value = mock_lambda + + mock_lambda.invoke.return_value = { + "Payload": MagicMock(read=lambda: b'{"token": "fake-token"}') + } + bignbit.utils.ED_USER = 'test' bignbit.utils.ED_PASS = 'test' job_id = '3d276f84-56e2-4f0a-acb2-35b9fcaaa317' @@ -63,8 +72,16 @@ def test_process_harmony_results(): assert file['variable'] == 'flx' @pytest.mark.vcr -def test_process_harmony_results_no_data(): +@patch("boto3.client") +def test_process_harmony_results_no_data(mock_boto): """Test case where a harmony job returned no data and was passed empty.""" + mock_lambda = MagicMock() + mock_boto.return_value = mock_lambda + + mock_lambda.invoke.return_value = { + "Payload": MagicMock(read=lambda: b'{"token": "fake-token"}') + } + bignbit.utils.ED_USER = 'test' bignbit.utils.ED_PASS = 'test' From 901e069b2c3bb29899a053a4347eee7ea099de82 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 09:40:23 -0400 Subject: [PATCH 10/33] remove cassette --- .../test_process_results_no_data.yaml | 278 ------------------ tests/test_handle_big_result.py | 4 +- 2 files changed, 3 insertions(+), 279 deletions(-) delete mode 100644 tests/cassettes/test_get_harmony_job_status/test_process_results_no_data.yaml diff --git a/tests/cassettes/test_get_harmony_job_status/test_process_results_no_data.yaml b/tests/cassettes/test_get_harmony_job_status/test_process_results_no_data.yaml deleted file mode 100644 index 67b421d..0000000 --- a/tests/cassettes/test_get_harmony_job_status/test_process_results_no_data.yaml +++ /dev/null @@ -1,278 +0,0 @@ -interactions: -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-requests/2.32.4 - method: GET - uri: https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=https - response: - body: - string: See Other. Redirecting to https://uat.urs.earthdata.nasa.gov/oauth/authorize?response_type=code&client_id=Wll59b2ShcZGg87xl3fRAg&redirect_uri=https%3A%2F%2Fharmony.uat.earthdata.nasa.gov%2Foauth2%2Fredirect&state=c75528d8dae0d2200e065b5e0e92e778 - headers: - Connection: - - keep-alive - Content-Length: - - '245' - Content-Type: - - text/plain; charset=utf-8 - Date: - - Fri, 06 Feb 2026 01:14:13 GMT - Location: - - https://uat.urs.earthdata.nasa.gov/oauth/authorize?response_type=code&client_id=Wll59b2ShcZGg87xl3fRAg&redirect_uri=https%3A%2F%2Fharmony.uat.earthdata.nasa.gov%2Foauth2%2Fredirect&state=c75528d8dae0d2200e065b5e0e92e778 - Server: - - CloudFront - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - Vary: - - Accept - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Powered-By: - - Express - X-XSS-Protection: - - 1; mode=block - status: - code: 303 - message: See Other -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-requests/2.32.4 - method: GET - uri: https://uat.urs.earthdata.nasa.gov/oauth/authorize?response_type=code&client_id=Wll59b2ShcZGg87xl3fRAg&redirect_uri=https%3A%2F%2Fharmony.uat.earthdata.nasa.gov%2Foauth2%2Fredirect&state=c75528d8dae0d2200e065b5e0e92e778 - response: - body: - string: You are being redirected to https://harmony.uat.earthdata.nasa.gov/oauth2/redirect?code=g72rnR6NQwb84brdTsxRlyqJRunJcdTQ7YWTgk9F7eBgH-F8IsrQxww4xXzqgRMY5FY00FKw6XK5LEmWpaPcGlEVJ3LvilKEORSpTw&state=c75528d8dae0d2200e065b5e0e92e778 - headers: - Access-Control-Allow-Credentials: - - 'true' - Access-Control-Allow-Methods: - - GET, POST - Access-Control-Allow-Origin: - - 'null' - Access-Control-Expose-Headers: - - 'true' - Cache-Control: - - no-store - Connection: - - keep-alive - Content-Type: - - text/html; charset=utf-8 - Date: - - Fri, 06 Feb 2026 01:14:13 GMT - Expires: - - Fri, 01 Jan 1990 00:00:00 GMT - Location: - - https://harmony.uat.earthdata.nasa.gov/oauth2/redirect?code=g72rnR6NQwb84brdTsxRlyqJRunJcdTQ7YWTgk9F7eBgH-F8IsrQxww4xXzqgRMY5FY00FKw6XK5LEmWpaPcGlEVJ3LvilKEORSpTw&state=c75528d8dae0d2200e065b5e0e92e778 - Pragma: - - no-cache - Referrer-Policy: - - strict-origin-when-cross-origin - Server: - - nginx/1.22.1 - Strict-Transport-Security: - - max-age=31536000 - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Permitted-Cross-Domain-Policies: - - none - X-XSS-Protection: - - '0' - status: - code: 302 - message: Found -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-requests/2.32.4 - method: GET - uri: https://harmony.uat.earthdata.nasa.gov/oauth2/redirect?code=g72rnR6NQwb84brdTsxRlyqJRunJcdTQ7YWTgk9F7eBgH-F8IsrQxww4xXzqgRMY5FY00FKw6XK5LEmWpaPcGlEVJ3LvilKEORSpTw&state=c75528d8dae0d2200e065b5e0e92e778 - response: - body: - string: Temporary Redirect. Redirecting to https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=https - headers: - Connection: - - keep-alive - Content-Length: - - '130' - Content-Type: - - text/plain; charset=utf-8 - Date: - - Fri, 06 Feb 2026 01:14:14 GMT - Location: - - https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=https - Server: - - CloudFront - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - Vary: - - Accept - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Powered-By: - - Express - X-XSS-Protection: - - 1; mode=block - status: - code: 307 - message: Temporary Redirect -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-requests/2.32.4 - method: GET - uri: https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=https - response: - body: - string: '{"username":"jryan95","status":"successful","message":"The job has - completed successfully","progress":100,"createdAt":"2026-01-20T20:45:22.704Z","updatedAt":"2026-01-20T20:45:49.079Z","dataExpiration":"2026-02-19T20:45:22.704Z","links":[{"href":"https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=https&page=1&limit=2000","title":"The - current page","type":"application/json","rel":"self"}],"labels":[],"request":"https://harmony.uat.earthdata.nasa.gov/C1274178386-LARC_CLOUD/ogc-api-coverages/1.0.0/collections/parameter_vars/coverage/rangeset?forceAsync=true&granuleId=G1275705373-LARC_CLOUD&format=image%2Fpng&variable=product%2Fcolumn_amount_o3","numInputGranules":1,"jobID":"60c6de41-a51a-4283-aa7c-2d530ebab8d9"}' - headers: - Connection: - - keep-alive - Content-Length: - - '756' - Content-Type: - - application/json; charset=utf-8 - Date: - - Fri, 06 Feb 2026 01:14:14 GMT - ETag: - - W/"2f4-xZQWHcIBs+pnSKv9lxiHcN0TJxc" - Server: - - CloudFront - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Powered-By: - - Express - X-XSS-Protection: - - 1; mode=block - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-requests/2.32.4 - method: GET - uri: https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=https - response: - body: - string: '{"username":"jryan95","status":"successful","message":"The job has - completed successfully","progress":100,"createdAt":"2026-01-20T20:45:22.704Z","updatedAt":"2026-01-20T20:45:49.079Z","dataExpiration":"2026-02-19T20:45:22.704Z","links":[{"href":"https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=https&page=1&limit=2000","title":"The - current page","type":"application/json","rel":"self"}],"labels":[],"request":"https://harmony.uat.earthdata.nasa.gov/C1274178386-LARC_CLOUD/ogc-api-coverages/1.0.0/collections/parameter_vars/coverage/rangeset?forceAsync=true&granuleId=G1275705373-LARC_CLOUD&format=image%2Fpng&variable=product%2Fcolumn_amount_o3","numInputGranules":1,"jobID":"60c6de41-a51a-4283-aa7c-2d530ebab8d9"}' - headers: - Connection: - - keep-alive - Content-Length: - - '756' - Content-Type: - - application/json; charset=utf-8 - Date: - - Fri, 06 Feb 2026 01:14:14 GMT - ETag: - - W/"2f4-xZQWHcIBs+pnSKv9lxiHcN0TJxc" - Server: - - CloudFront - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Powered-By: - - Express - X-XSS-Protection: - - 1; mode=block - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-requests/2.32.4 - method: GET - uri: https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=s3 - response: - body: - string: '{"username":"jryan95","status":"successful","message":"The job has - completed successfully","progress":100,"createdAt":"2026-01-20T20:45:22.704Z","updatedAt":"2026-01-20T20:45:49.079Z","dataExpiration":"2026-02-19T20:45:22.704Z","links":[{"href":"https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=s3&page=1&limit=2000","title":"The - current page","type":"application/json","rel":"self"}],"labels":[],"request":"https://harmony.uat.earthdata.nasa.gov/C1274178386-LARC_CLOUD/ogc-api-coverages/1.0.0/collections/parameter_vars/coverage/rangeset?forceAsync=true&granuleId=G1275705373-LARC_CLOUD&format=image%2Fpng&variable=product%2Fcolumn_amount_o3","numInputGranules":1,"jobID":"60c6de41-a51a-4283-aa7c-2d530ebab8d9"}' - headers: - Connection: - - keep-alive - Content-Length: - - '753' - Content-Type: - - application/json; charset=utf-8 - Date: - - Fri, 06 Feb 2026 01:14:14 GMT - ETag: - - W/"2f1-b0Cm0wV82BHxA0lzpZmKudRDANo" - Server: - - CloudFront - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Powered-By: - - Express - X-XSS-Protection: - - 1; mode=block - status: - code: 200 - message: OK -version: 1 diff --git a/tests/test_handle_big_result.py b/tests/test_handle_big_result.py index efdb97e..f0fb34b 100644 --- a/tests/test_handle_big_result.py +++ b/tests/test_handle_big_result.py @@ -79,7 +79,9 @@ def test_process_harmony_results_no_data(mock_boto): mock_boto.return_value = mock_lambda mock_lambda.invoke.return_value = { - "Payload": MagicMock(read=lambda: b'{"token": "fake-token"}') + "Payload": MagicMock( + read=lambda: b'{"status": "SUCCESS", "results": []}' + ) } bignbit.utils.ED_USER = 'test' From c732c7252eae53920bdcaf33da0ae7178de9d2e2 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 10:27:30 -0400 Subject: [PATCH 11/33] changing boto mocking for tests --- tests/test_get_harmony_job_status.py | 12 +++++++++++- tests/test_handle_big_result.py | 2 +- tests/test_utils.py | 21 ++++++--------------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index 5265c40..0011048 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -1,6 +1,7 @@ """Unit tests for get_harmony_job_status module""" import pytest from moto import mock_s3 +from unittest.mock import patch, MagicMock import bignbit.utils from bignbit.get_harmony_job_status import check_harmony_job, HarmonyJobNoDataError @@ -8,8 +9,17 @@ @pytest.mark.vcr @mock_s3 -def test_process_results_no_data(): +def test_process_results_no_data(mock_boto): """Test that HarmonyJobNoDataError is raised when Harmony returns no data""" + mock_lambda = MagicMock() + mock_boto.return_value = mock_lambda + + mock_lambda.invoke.return_value = { + "Payload": MagicMock( + read=lambda: b'{"status": "SUCCESS", "results": []}' + ) + } + bignbit.utils.ED_USER = 'test' bignbit.utils.ED_PASS = 'test' diff --git a/tests/test_handle_big_result.py b/tests/test_handle_big_result.py index f0fb34b..c76df86 100644 --- a/tests/test_handle_big_result.py +++ b/tests/test_handle_big_result.py @@ -20,7 +20,7 @@ @pytest.mark.vcr @mock_s3 -@patch("boto3.client") +@patch('bignbit.utils.boto3.client') def test_process_harmony_results(mock_boto): """Test pulling results of a harmony job from s3.""" mock_lambda = MagicMock() diff --git a/tests/test_utils.py b/tests/test_utils.py index 956f8ca..74cf5cc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -468,7 +468,7 @@ def test_parse_doy_leap_day(): # Tests for get_harmony_client() # --------------------------------------------------------------------------- -@patch('boto3.client') +@patch('bignbit.utils.boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): @@ -485,10 +485,7 @@ def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_bo mock_ssm.get_parameter.return_value = { "Parameter": { - "Value": json.dumps({ - "username": "test_user", - "password": "test_pass" - }) + "Value": '{"username": "test_user", "password": "test_pass"}' } } @@ -501,7 +498,7 @@ def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_bo mock_harmony_client.assert_called_once() -@patch('boto3.client') +@patch('bignbit.utils.boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_boto): @@ -518,10 +515,7 @@ def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_b mock_ssm.get_parameter.return_value = { "Parameter": { - "Value": json.dumps({ - "username": "test_user", - "password": "test_pass" - }) + "Value": '{"username": "test_user", "password": "test_pass"}' } } @@ -534,7 +528,7 @@ def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_b mock_harmony_client.assert_called_once() -@patch('boto3.client') +@patch('bignbit.utils.boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): @@ -551,10 +545,7 @@ def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_harmony mock_ssm.get_parameter.return_value = { "Parameter": { - "Value": json.dumps({ - "username": "test_user", - "password": "test_pass" - }) + "Value": '{"username": "test_user", "password": "test_pass"}' } } From 796de8b15ea1305632860e1890b93fd2c3b9dd11 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 10:41:08 -0400 Subject: [PATCH 12/33] working on UAT mock --- tests/test_handle_big_result.py | 2 +- tests/test_utils.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_handle_big_result.py b/tests/test_handle_big_result.py index c76df86..681e67b 100644 --- a/tests/test_handle_big_result.py +++ b/tests/test_handle_big_result.py @@ -72,7 +72,7 @@ def test_process_harmony_results(mock_boto): assert file['variable'] == 'flx' @pytest.mark.vcr -@patch("boto3.client") +@patch('bignbit.utils.boto3.client') def test_process_harmony_results_no_data(mock_boto): """Test case where a harmony job returned no data and was passed empty.""" mock_lambda = MagicMock() diff --git a/tests/test_utils.py b/tests/test_utils.py index 74cf5cc..45d5f98 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -471,15 +471,17 @@ def test_parse_doy_leap_day(): @patch('bignbit.utils.boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): - """Test getting Harmony client for UAT environment.""" +def test_get_harmony_client_uat( + mock_get_edl_creds, + mock_harmony_client, + mock_boto +): mock_get_edl_creds.return_value = ('test_user', 'test_pass') - # --- Harmony client mock --- mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance - # --- boto3 SSM mock --- + # boto3 mock mock_ssm = MagicMock() mock_boto.return_value = mock_ssm @@ -495,7 +497,6 @@ def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_bo result = get_harmony_client('UAT') assert result == mock_client_instance - mock_harmony_client.assert_called_once() @patch('bignbit.utils.boto3.client') From 0822c64668cf038809edaea03403f1868009fee0 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 10:52:22 -0400 Subject: [PATCH 13/33] working on UAT remove boto mock --- tests/test_utils.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 45d5f98..a348532 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -468,29 +468,15 @@ def test_parse_doy_leap_day(): # Tests for get_harmony_client() # --------------------------------------------------------------------------- -@patch('bignbit.utils.boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_uat( - mock_get_edl_creds, - mock_harmony_client, - mock_boto -): +def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client): + """Test getting Harmony client for UAT environment.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance - # boto3 mock - mock_ssm = MagicMock() - mock_boto.return_value = mock_ssm - - mock_ssm.get_parameter.return_value = { - "Parameter": { - "Value": '{"username": "test_user", "password": "test_pass"}' - } - } - import bignbit.utils bignbit.utils.HARMONY_CLIENT = None From c2d2f807f07f736e5702ea94820cdd0a61e873f9 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 11:05:03 -0400 Subject: [PATCH 14/33] working on UAT remove boto mock --- tests/test_get_harmony_job_status.py | 1 + tests/test_utils.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index 0011048..f498f28 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -9,6 +9,7 @@ @pytest.mark.vcr @mock_s3 +@patch('bignbit.utils.boto3.client') def test_process_results_no_data(mock_boto): """Test that HarmonyJobNoDataError is raised when Harmony returns no data""" mock_lambda = MagicMock() diff --git a/tests/test_utils.py b/tests/test_utils.py index a348532..d15aa7c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -468,21 +468,34 @@ def test_parse_doy_leap_day(): # Tests for get_harmony_client() # --------------------------------------------------------------------------- +@patch('bignbit.utils.boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client): - """Test getting Harmony client for UAT environment.""" +def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_boto): + """Test getting Harmony client for PROD environment.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') + # --- Harmony client mock --- mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance + # --- boto3 SSM mock --- + mock_ssm = MagicMock() + mock_boto.return_value = mock_ssm + + mock_ssm.get_parameter.return_value = { + "Parameter": { + "Value": '{"username": "test_user", "password": "test_pass"}' + } + } + import bignbit.utils bignbit.utils.HARMONY_CLIENT = None result = get_harmony_client('UAT') assert result == mock_client_instance + mock_harmony_client.assert_called_once() @patch('bignbit.utils.boto3.client') From 442d7529910676b85c91ccf7c722df54d781342b Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 11:27:08 -0400 Subject: [PATCH 15/33] changing lambda return to json --- tests/test_get_harmony_job_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index f498f28..2d7ab16 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -17,7 +17,7 @@ def test_process_results_no_data(mock_boto): mock_lambda.invoke.return_value = { "Payload": MagicMock( - read=lambda: b'{"status": "SUCCESS", "results": []}' + read=lambda: b'"{\\"status\\": \\"SUCCESS\\", \\"results\\": []}"' ) } From 67084c0f00d1e1e75ef15b1a9f3f815699bd4265 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 11:31:58 -0400 Subject: [PATCH 16/33] adding access-token as lambda return --- tests/test_get_harmony_job_status.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index 2d7ab16..b4ccde3 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -9,8 +9,9 @@ @pytest.mark.vcr @mock_s3 +@patch('bignbit.utils.get_edl_creds') @patch('bignbit.utils.boto3.client') -def test_process_results_no_data(mock_boto): +def test_process_results_no_data(mock_boto, mock_get_creds): """Test that HarmonyJobNoDataError is raised when Harmony returns no data""" mock_lambda = MagicMock() mock_boto.return_value = mock_lambda @@ -21,6 +22,10 @@ def test_process_results_no_data(mock_boto): ) } + mock_get_creds.return_value = { + "access-token": "test-token" + } + bignbit.utils.ED_USER = 'test' bignbit.utils.ED_PASS = 'test' From 79d1f946a3898be272723b53ca0a2db2384a8876 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 11:48:06 -0400 Subject: [PATCH 17/33] change mock for no_data --- tests/test_get_harmony_job_status.py | 54 ++++++++++++++++++---------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index b4ccde3..fac0307 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -8,33 +8,49 @@ @pytest.mark.vcr -@mock_s3 @patch('bignbit.utils.get_edl_creds') @patch('bignbit.utils.boto3.client') -def test_process_results_no_data(mock_boto, mock_get_creds): - """Test that HarmonyJobNoDataError is raised when Harmony returns no data""" +def test_process_results_no_data(mock_boto, mock_get_edl_creds): + """Test HarmonyJobNoDataError is raised when no results are returned""" + + import bignbit.utils + from bignbit.utils import check_harmony_job, HarmonyJobNoDataError + + # ---- creds MUST be tuple (fixes unpacking error) ---- + mock_get_edl_creds.return_value = ('test_user', 'test_pass') + + # ---- lambda client mock ---- mock_lambda = MagicMock() mock_boto.return_value = mock_lambda - mock_lambda.invoke.return_value = { - "Payload": MagicMock( - read=lambda: b'"{\\"status\\": \\"SUCCESS\\", \\"results\\": []}"' - ) + # ---- payload must behave like AWS (realistic read()) ---- + payload_data = { + "status": "SUCCESS", + "results": [] } - - mock_get_creds.return_value = { - "access-token": "test-token" + + mock_payload = MagicMock() + mock_payload.read.return_value = json.dumps(payload_data).encode("utf-8") + + mock_lambda.invoke.return_value = { + "Payload": mock_payload } - bignbit.utils.ED_USER = 'test' - bignbit.utils.ED_PASS = 'test' + # optional globals if your function depends on them + bignbit.utils.ED_USER = "test" + bignbit.utils.ED_PASS = "test" - # Note: This test uses VCR to record the Harmony API response - # The cassette should show a successful job with no result URLs - # Using UAT environment since the test job ID exists in UAT + # ---- assertion ---- with pytest.raises(HarmonyJobNoDataError) as exc_info: - check_harmony_job('60c6de41-a51a-4283-aa7c-2d530ebab8d9', 'uat', 'test_variable', 'EPSG:4326') + check_harmony_job( + '60c6de41-a51a-4283-aa7c-2d530ebab8d9', + 'uat', + 'test_variable', + 'EPSG:4326' + ) + + msg = str(exc_info.value).lower() - assert 'no data' in str(exc_info.value).lower() - assert 'test_variable' in str(exc_info.value) - assert 'EPSG:4326' in str(exc_info.value) \ No newline at end of file + assert "no data" in msg + assert "test_variable" in msg + assert "epsg:4326" in msg \ No newline at end of file From bae613379e562c4932e3b56afa9403b9ef41a37a Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 11:51:12 -0400 Subject: [PATCH 18/33] move imports --- tests/test_get_harmony_job_status.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index fac0307..83bf9c1 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -13,9 +13,6 @@ def test_process_results_no_data(mock_boto, mock_get_edl_creds): """Test HarmonyJobNoDataError is raised when no results are returned""" - import bignbit.utils - from bignbit.utils import check_harmony_job, HarmonyJobNoDataError - # ---- creds MUST be tuple (fixes unpacking error) ---- mock_get_edl_creds.return_value = ('test_user', 'test_pass') From 5aee785b170529650afa8c36dd2103ca4f1597ef Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 11:55:21 -0400 Subject: [PATCH 19/33] add missing import --- tests/test_get_harmony_job_status.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index 83bf9c1..28364cf 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -1,6 +1,7 @@ """Unit tests for get_harmony_job_status module""" import pytest from moto import mock_s3 +import json from unittest.mock import patch, MagicMock import bignbit.utils From 0c5312587781b5ad79fa64d73a8a1b68212041d0 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 13:52:58 -0400 Subject: [PATCH 20/33] copied uat 2 prod,sit tests --- tests/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index d15aa7c..b9f2fab 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -555,3 +555,4 @@ def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_harmony result = get_harmony_client('SIT') assert result == mock_client_instance + mock_harmony_client.assert_called_once() From 31f8512bfb2fbbe62b239206636dfd42cbe7cd3b Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 14:08:28 -0400 Subject: [PATCH 21/33] working with diff mock usage --- tests/test_utils.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index b9f2fab..39fcc3b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -471,24 +471,30 @@ def test_parse_doy_leap_day(): @patch('bignbit.utils.boto3.client') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_boto): +def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): """Test getting Harmony client for PROD environment.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') - # --- Harmony client mock --- + # Harmony client mock mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance - # --- boto3 SSM mock --- + # boto3 SSM mock mock_ssm = MagicMock() mock_boto.return_value = mock_ssm - mock_ssm.get_parameter.return_value = { + # real dict (important) + response = { "Parameter": { "Value": '{"username": "test_user", "password": "test_pass"}' } } + # force `.get()` to behave like dict + response.get = lambda k: response[k] + + mock_ssm.get_parameter.return_value = response + import bignbit.utils bignbit.utils.HARMONY_CLIENT = None @@ -505,20 +511,26 @@ def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_b """Test getting Harmony client for PROD environment.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') - # --- Harmony client mock --- + # Harmony client mock mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance - # --- boto3 SSM mock --- + # boto3 SSM mock mock_ssm = MagicMock() mock_boto.return_value = mock_ssm - mock_ssm.get_parameter.return_value = { + # real dict (important) + response = { "Parameter": { "Value": '{"username": "test_user", "password": "test_pass"}' } } + # force `.get()` to behave like dict + response.get = lambda k: response[k] + + mock_ssm.get_parameter.return_value = response + import bignbit.utils bignbit.utils.HARMONY_CLIENT = None @@ -535,20 +547,26 @@ def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_harmony """Test that SIT environment defaults to UAT.""" mock_get_edl_creds.return_value = ('test_user', 'test_pass') - # --- Harmony client mock --- + # Harmony client mock mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance - # --- boto3 SSM mock --- + # boto3 SSM mock mock_ssm = MagicMock() mock_boto.return_value = mock_ssm - mock_ssm.get_parameter.return_value = { + # real dict (important) + response = { "Parameter": { "Value": '{"username": "test_user", "password": "test_pass"}' } } + # force `.get()` to behave like dict + response.get = lambda k: response[k] + + mock_ssm.get_parameter.return_value = response + import bignbit.utils bignbit.utils.HARMONY_CLIENT = None From 453b6c2c5a8bbda81192ff6dd997336bf709ff96 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 14:26:50 -0400 Subject: [PATCH 22/33] working with diff mock usage-2 --- tests/test_utils.py | 84 ++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 39fcc3b..e13d34b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -473,33 +473,37 @@ def test_parse_doy_leap_day(): @patch('bignbit.utils.get_edl_creds') def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): """Test getting Harmony client for PROD environment.""" + # --- creds --- mock_get_edl_creds.return_value = ('test_user', 'test_pass') - # Harmony client mock + # --- harmony client --- mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance - # boto3 SSM mock + # --- boto3 SSM mock chain --- mock_ssm = MagicMock() mock_boto.return_value = mock_ssm - # real dict (important) - response = { - "Parameter": { - "Value": '{"username": "test_user", "password": "test_pass"}' - } - } + mock_value = '{"username": "test_user", "password": "test_pass"}' + + # This represents param.get("Parameter") + mock_parameter = MagicMock() + mock_parameter.get.return_value = mock_value # .get("Value") - # force `.get()` to behave like dict - response.get = lambda k: response[k] + # This represents response.get("Parameter") + mock_response = MagicMock() + mock_response.get.return_value = mock_parameter - mock_ssm.get_parameter.return_value = response + mock_ssm.get_parameter.return_value = mock_response + # --- reset singleton --- import bignbit.utils bignbit.utils.HARMONY_CLIENT = None - result = get_harmony_client('UAT') + # --- execute --- + result = bignbit.utils.get_harmony_client('UAT') + # --- assert --- assert result == mock_client_instance mock_harmony_client.assert_called_once() @@ -509,33 +513,37 @@ def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_bo @patch('bignbit.utils.get_edl_creds') def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_boto): """Test getting Harmony client for PROD environment.""" + # --- creds --- mock_get_edl_creds.return_value = ('test_user', 'test_pass') - # Harmony client mock + # --- harmony client --- mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance - # boto3 SSM mock + # --- boto3 SSM mock chain --- mock_ssm = MagicMock() mock_boto.return_value = mock_ssm - # real dict (important) - response = { - "Parameter": { - "Value": '{"username": "test_user", "password": "test_pass"}' - } - } + mock_value = '{"username": "test_user", "password": "test_pass"}' - # force `.get()` to behave like dict - response.get = lambda k: response[k] + # This represents param.get("Parameter") + mock_parameter = MagicMock() + mock_parameter.get.return_value = mock_value # .get("Value") - mock_ssm.get_parameter.return_value = response + # This represents response.get("Parameter") + mock_response = MagicMock() + mock_response.get.return_value = mock_parameter + mock_ssm.get_parameter.return_value = mock_response + + # --- reset singleton --- import bignbit.utils bignbit.utils.HARMONY_CLIENT = None - result = get_harmony_client('PROD') + # --- execute --- + result = bignbit.utils.get_harmony_client('PROD') + # --- assert --- assert result == mock_client_instance mock_harmony_client.assert_called_once() @@ -545,32 +553,36 @@ def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_b @patch('bignbit.utils.get_edl_creds') def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): """Test that SIT environment defaults to UAT.""" + # --- creds --- mock_get_edl_creds.return_value = ('test_user', 'test_pass') - # Harmony client mock + # --- harmony client --- mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance - # boto3 SSM mock + # --- boto3 SSM mock chain --- mock_ssm = MagicMock() mock_boto.return_value = mock_ssm - # real dict (important) - response = { - "Parameter": { - "Value": '{"username": "test_user", "password": "test_pass"}' - } - } + mock_value = '{"username": "test_user", "password": "test_pass"}' + + # This represents param.get("Parameter") + mock_parameter = MagicMock() + mock_parameter.get.return_value = mock_value # .get("Value") - # force `.get()` to behave like dict - response.get = lambda k: response[k] + # This represents response.get("Parameter") + mock_response = MagicMock() + mock_response.get.return_value = mock_parameter - mock_ssm.get_parameter.return_value = response + mock_ssm.get_parameter.return_value = mock_response + # --- reset singleton --- import bignbit.utils bignbit.utils.HARMONY_CLIENT = None - result = get_harmony_client('SIT') + # --- execute --- + result = bignbit.utils.get_harmony_client('SIT') + # --- assert --- assert result == mock_client_instance mock_harmony_client.assert_called_once() From 218e97f1a6f58657cf4233726994169453bb9194 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 16 Apr 2026 14:54:27 -0400 Subject: [PATCH 23/33] working with diff mock usage-3 --- tests/test_utils.py | 139 +++++++++++++------------------------------- 1 file changed, 42 insertions(+), 97 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index e13d34b..05819bc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -16,7 +16,8 @@ extract_granule_dates, parse_datetime, parse_doy, - get_harmony_client + get_harmony_client, + Environment ) @@ -468,121 +469,65 @@ def test_parse_doy_leap_day(): # Tests for get_harmony_client() # --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "input_env, expected_env", + [ + ("UAT", Environment.UAT), + ("PROD", Environment.PROD), + ("SIT", Environment.UAT), # defaults to UAT + ] +) @patch('bignbit.utils.boto3.client') +@patch('bignbit.utils.boto3.session.Session') @patch('bignbit.utils.Client') @patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): - """Test getting Harmony client for PROD environment.""" - # --- creds --- - mock_get_edl_creds.return_value = ('test_user', 'test_pass') - - # --- harmony client --- - mock_client_instance = MagicMock() - mock_harmony_client.return_value = mock_client_instance - - # --- boto3 SSM mock chain --- - mock_ssm = MagicMock() - mock_boto.return_value = mock_ssm - - mock_value = '{"username": "test_user", "password": "test_pass"}' - - # This represents param.get("Parameter") - mock_parameter = MagicMock() - mock_parameter.get.return_value = mock_value # .get("Value") - - # This represents response.get("Parameter") - mock_response = MagicMock() - mock_response.get.return_value = mock_parameter - - mock_ssm.get_parameter.return_value = mock_response - - # --- reset singleton --- +def test_get_harmony_client_environments( + mock_get_edl_creds, + mock_harmony_client, + mock_session, + mock_boto, + input_env, + expected_env +): import bignbit.utils - bignbit.utils.HARMONY_CLIENT = None - - # --- execute --- - result = bignbit.utils.get_harmony_client('UAT') - - # --- assert --- - assert result == mock_client_instance - mock_harmony_client.assert_called_once() - -@patch('bignbit.utils.boto3.client') -@patch('bignbit.utils.Client') -@patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_prod(mock_get_edl_creds, mock_harmony_client, mock_boto): - """Test getting Harmony client for PROD environment.""" # --- creds --- mock_get_edl_creds.return_value = ('test_user', 'test_pass') - # --- harmony client --- - mock_client_instance = MagicMock() - mock_harmony_client.return_value = mock_client_instance - - # --- boto3 SSM mock chain --- - mock_ssm = MagicMock() - mock_boto.return_value = mock_ssm - - mock_value = '{"username": "test_user", "password": "test_pass"}' - - # This represents param.get("Parameter") - mock_parameter = MagicMock() - mock_parameter.get.return_value = mock_value # .get("Value") - - # This represents response.get("Parameter") - mock_response = MagicMock() - mock_response.get.return_value = mock_parameter - - mock_ssm.get_parameter.return_value = mock_response - - # --- reset singleton --- - import bignbit.utils - bignbit.utils.HARMONY_CLIENT = None - - # --- execute --- - result = bignbit.utils.get_harmony_client('PROD') - - # --- assert --- - assert result == mock_client_instance - mock_harmony_client.assert_called_once() - - -@patch('bignbit.utils.boto3.client') -@patch('bignbit.utils.Client') -@patch('bignbit.utils.get_edl_creds') -def test_get_harmony_client_sit_defaults_to_uat(mock_get_edl_creds, mock_harmony_client, mock_boto): - """Test that SIT environment defaults to UAT.""" - # --- creds --- - mock_get_edl_creds.return_value = ('test_user', 'test_pass') + # --- session / region --- + mock_session.return_value.region_name = "us-west-2" # --- harmony client --- mock_client_instance = MagicMock() mock_harmony_client.return_value = mock_client_instance - # --- boto3 SSM mock chain --- - mock_ssm = MagicMock() - mock_boto.return_value = mock_ssm - - mock_value = '{"username": "test_user", "password": "test_pass"}' + # --- lambda client --- + mock_lambda = MagicMock() + mock_boto.return_value = mock_lambda - # This represents param.get("Parameter") - mock_parameter = MagicMock() - mock_parameter.get.return_value = mock_value # .get("Value") + # 👇 double-encoded JSON (required by function) + inner = json.dumps({"access-token": "test-token"}) + outer = json.dumps(inner).encode("utf-8") - # This represents response.get("Parameter") - mock_response = MagicMock() - mock_response.get.return_value = mock_parameter + mock_payload = MagicMock() + mock_payload.read.return_value = outer - mock_ssm.get_parameter.return_value = mock_response + mock_lambda.invoke.return_value = { + "Payload": mock_payload + } - # --- reset singleton --- - import bignbit.utils + # reset singleton bignbit.utils.HARMONY_CLIENT = None # --- execute --- - result = bignbit.utils.get_harmony_client('SIT') + result = bignbit.utils.get_harmony_client(input_env) - # --- assert --- + # --- assertions --- assert result == mock_client_instance - mock_harmony_client.assert_called_once() + + # verify correct environment passed to Client + mock_harmony_client.assert_called_with( + env=expected_env, + token="test-token", + should_validate_auth=bignbit.utils.HARMONY_SHOULD_VALIDATE_AUTH + ) \ No newline at end of file From b1f6d843cc9424cd89ab5a91a39f510fad1d34a0 Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 27 Apr 2026 10:06:37 -0400 Subject: [PATCH 24/33] pushing region to os --- tests/test_utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 05819bc..e2a1aa2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,4 @@ +import os import json import pathlib import tempfile @@ -464,6 +465,9 @@ def test_parse_doy_leap_day(): result = parse_doy(2024, 60) assert result == "2024-02-29T00:00:00.000000Z" +@pytest.fixture(autouse=True) +def aws_region(): + os.environ["AWS_DEFAULT_REGION"] = "us-west-2" # --------------------------------------------------------------------------- # Tests for get_harmony_client() From da1c8ff4500d0e939523bf0c31bc45ea602142e2 Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 27 Apr 2026 10:17:35 -0400 Subject: [PATCH 25/33] fixing payload for lambda response --- tests/test_get_harmony_job_status.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index 28364cf..471b0fa 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -23,8 +23,7 @@ def test_process_results_no_data(mock_boto, mock_get_edl_creds): # ---- payload must behave like AWS (realistic read()) ---- payload_data = { - "status": "SUCCESS", - "results": [] + "access-token": "fake-token" } mock_payload = MagicMock() From 6b3512d2e4a099db9c5591adddefa95dd34644fa Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 27 Apr 2026 10:25:27 -0400 Subject: [PATCH 26/33] proper lambda response mocked --- tests/test_handle_big_result.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_handle_big_result.py b/tests/test_handle_big_result.py index 681e67b..3609835 100644 --- a/tests/test_handle_big_result.py +++ b/tests/test_handle_big_result.py @@ -79,9 +79,7 @@ def test_process_harmony_results_no_data(mock_boto): mock_boto.return_value = mock_lambda mock_lambda.invoke.return_value = { - "Payload": MagicMock( - read=lambda: b'{"status": "SUCCESS", "results": []}' - ) + "Payload": MagicMock(read=lambda: b'{"access-token": "fake-token"}') } bignbit.utils.ED_USER = 'test' From 402bad5bd279c88ff8c5d62e43810fe0ab5a9955 Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 27 Apr 2026 10:30:32 -0400 Subject: [PATCH 27/33] changing all lambda mock response to the same values --- tests/test_get_harmony_job_status.py | 2 +- tests/test_handle_big_result.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index 471b0fa..8ecd6fb 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -23,7 +23,7 @@ def test_process_results_no_data(mock_boto, mock_get_edl_creds): # ---- payload must behave like AWS (realistic read()) ---- payload_data = { - "access-token": "fake-token" + "access-token": "test-token" } mock_payload = MagicMock() diff --git a/tests/test_handle_big_result.py b/tests/test_handle_big_result.py index 3609835..3f433cb 100644 --- a/tests/test_handle_big_result.py +++ b/tests/test_handle_big_result.py @@ -27,7 +27,7 @@ def test_process_harmony_results(mock_boto): mock_boto.return_value = mock_lambda mock_lambda.invoke.return_value = { - "Payload": MagicMock(read=lambda: b'{"token": "fake-token"}') + "Payload": MagicMock(read=lambda: b'{"access-token": "test-token"}') } bignbit.utils.ED_USER = 'test' @@ -79,7 +79,7 @@ def test_process_harmony_results_no_data(mock_boto): mock_boto.return_value = mock_lambda mock_lambda.invoke.return_value = { - "Payload": MagicMock(read=lambda: b'{"access-token": "fake-token"}') + "Payload": MagicMock(read=lambda: b'{"access-token": "test-token"}') } bignbit.utils.ED_USER = 'test' From dd6a6aa2282456082a566f0aa72400df5994aaf7 Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 27 Apr 2026 10:36:55 -0400 Subject: [PATCH 28/33] removing doule json.loads --- bignbit/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bignbit/utils.py b/bignbit/utils.py index ba15af2..1a92032 100644 --- a/bignbit/utils.py +++ b/bignbit/utils.py @@ -360,8 +360,8 @@ def get_harmony_client(environment_str: str) -> Client: ) # Read the payload from Lambda response and parse JSON - response_payload = json.loads(response["Payload"].read()) - token = json.loads(response_payload)['access-token'] + # response_payload = json.loads(response["Payload"].read()) + token = json.loads(response["Payload"].read())['access-token'] # Instantiate Harmony client with retrieved token HARMONY_CLIENT = Client( From d36268edb893862bf46bb89cb9f3d8e727e82de6 Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 27 Apr 2026 10:40:33 -0400 Subject: [PATCH 29/33] working out getting access-token --- bignbit/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bignbit/utils.py b/bignbit/utils.py index 1a92032..67fb412 100644 --- a/bignbit/utils.py +++ b/bignbit/utils.py @@ -360,8 +360,8 @@ def get_harmony_client(environment_str: str) -> Client: ) # Read the payload from Lambda response and parse JSON - # response_payload = json.loads(response["Payload"].read()) - token = json.loads(response["Payload"].read())['access-token'] + response_payload = json.loads(response["Payload"].read()) + token = response_payload['access-token'] # Instantiate Harmony client with retrieved token HARMONY_CLIENT = Client( From 728068df7b09e52702c90fd55df0e38fcc8ff2b1 Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 27 Apr 2026 10:46:38 -0400 Subject: [PATCH 30/33] working out getting access-token --- bignbit/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bignbit/utils.py b/bignbit/utils.py index 67fb412..985371e 100644 --- a/bignbit/utils.py +++ b/bignbit/utils.py @@ -361,7 +361,7 @@ def get_harmony_client(environment_str: str) -> Client: # Read the payload from Lambda response and parse JSON response_payload = json.loads(response["Payload"].read()) - token = response_payload['access-token'] + token = response_payload["access-token"] # Instantiate Harmony client with retrieved token HARMONY_CLIENT = Client( From 956d55a0d8858d554294a9a41971a20c261b0890 Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 27 Apr 2026 10:51:10 -0400 Subject: [PATCH 31/33] json debugging --- bignbit/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bignbit/utils.py b/bignbit/utils.py index 985371e..1fb7163 100644 --- a/bignbit/utils.py +++ b/bignbit/utils.py @@ -361,6 +361,8 @@ def get_harmony_client(environment_str: str) -> Client: # Read the payload from Lambda response and parse JSON response_payload = json.loads(response["Payload"].read()) + print(type(response_payload)) + print(response_payload) token = response_payload["access-token"] # Instantiate Harmony client with retrieved token From 3c99c1ef062f389d73a913a10a288568575768be Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 27 Apr 2026 10:57:57 -0400 Subject: [PATCH 32/33] sync up fake token generation --- tests/test_utils.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index e2a1aa2..5ea92a7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -510,15 +510,27 @@ def test_get_harmony_client_environments( mock_boto.return_value = mock_lambda # 👇 double-encoded JSON (required by function) - inner = json.dumps({"access-token": "test-token"}) - outer = json.dumps(inner).encode("utf-8") + # inner = json.dumps({"access-token": "test-token"}) + # outer = json.dumps(inner).encode("utf-8") + + # mock_payload = MagicMock() + # mock_payload.read.return_value = outer + + # mock_lambda.invoke.return_value = { + # "Payload": mock_payload + # } + + # ---- payload must behave like AWS (realistic read()) ---- + payload_data = { + "access-token": "test-token" + } mock_payload = MagicMock() - mock_payload.read.return_value = outer + mock_payload.read.return_value = json.dumps(payload_data).encode("utf-8") mock_lambda.invoke.return_value = { "Payload": mock_payload - } + } # reset singleton bignbit.utils.HARMONY_CLIENT = None From 1e1b267c5f274946b953efe34a42e9c44c673d31 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 7 May 2026 08:30:29 -0400 Subject: [PATCH 33/33] fixed harmony client tests --- .../test_process_harmony_results.yaml | 57 ++++ cassettes/test_process_harmony_results.yaml | 267 ++++++++++++++++++ .../test_process_results_no_data.yaml | 57 ++++ tests/test_get_harmony_job_status.py | 49 +++- 4 files changed, 420 insertions(+), 10 deletions(-) create mode 100644 cassettes/test_handle_big_result/test_process_harmony_results.yaml create mode 100644 cassettes/test_process_harmony_results.yaml create mode 100644 tests/cassettes/test_get_harmony_job_status/test_process_results_no_data.yaml diff --git a/cassettes/test_handle_big_result/test_process_harmony_results.yaml b/cassettes/test_handle_big_result/test_process_harmony_results.yaml new file mode 100644 index 0000000..91ec09c --- /dev/null +++ b/cassettes/test_handle_big_result/test_process_harmony_results.yaml @@ -0,0 +1,57 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.5 + method: GET + uri: https://harmony.uat.earthdata.nasa.gov/jobs/3d276f84-56e2-4f0a-acb2-35b9fcaaa317?linktype=https + response: + body: + string: '{"code":"harmony.UnauthorizedError","description":"Error: You are not + authorized to access the requested resource"}' + headers: + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '115' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 29 Apr 2026 15:50:03 GMT + ETag: + - W/"73-nItWkl68x6IlPn6tx7bg/IcouGY" + Server: + - CloudFront + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Via: + - 1.1 22922394e3534f85930c1603d51f2156.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - 8h3Stmve4aRgESrEJpwVOaf5co7Gx6FbA5eRgrpChi2FoepoNyHhPg== + X-Amz-Cf-Pop: + - SFO53-P10 + X-Cache: + - Error from cloudfront + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Request-Id: + - 8h3Stmve4aRgESrEJpwVOaf5co7Gx6FbA5eRgrpChi2FoepoNyHhPg== + X-XSS-Protection: + - 1; mode=block + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/cassettes/test_process_harmony_results.yaml b/cassettes/test_process_harmony_results.yaml new file mode 100644 index 0000000..6b176a6 --- /dev/null +++ b/cassettes/test_process_harmony_results.yaml @@ -0,0 +1,267 @@ +interactions: +- request: + body: us-west-2 + headers: + Content-Length: + - '153' + User-Agent: + - !!binary | + Qm90bzMvMS4yNi4xNjUgUHl0aG9uLzMuMTAuMTMgRGFyd2luLzI0LjYuMCBCb3RvY29yZS8xLjI5 + LjE2NQ== + X-Amz-Content-SHA256: + - !!binary | + YTRhM2QyNjMxNzliZmNiYWY4ODc1NWIxNGVhOWRiMzA0OGU0MTc5YzAzYmJjOTRjY2YwZjEzYzhj + MmQzZGY2YQ== + X-Amz-Date: + - !!binary | + MjAyNjA0MjhUMTIxNzI0Wg== + amz-sdk-invocation-id: + - !!binary | + MjlhZTA0ODctMGE3NC00YzViLWJmNDctNzc1ZDQ4NTA2NDVj + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + method: PUT + uri: https://harmony-uat-staging.s3.us-west-2.amazonaws.com/ + response: + body: + string: ' + + BucketAlreadyExistsThe requested bucket name + is not available. The bucket namespace is shared by all users of the system. + Please select a different name and try again.harmony-uat-staging3KDANE7WF976V34NSjY5MpO95JO7GJNLWoMrvbLjgpvS0/fKYzAYt2d4IlZlHDxSzPVp+Pg2yHdinX9fMG/fxeBRhs8=' + headers: + Content-Type: + - application/xml + Date: + - Tue, 28 Apr 2026 12:17:24 GMT + Server: + - AmazonS3 + Transfer-Encoding: + - chunked + x-amz-bucket-region: + - us-west-2 + x-amz-id-2: + - SjY5MpO95JO7GJNLWoMrvbLjgpvS0/fKYzAYt2d4IlZlHDxSzPVp+Pg2yHdinX9fMG/fxeBRhs8= + x-amz-request-id: + - 3KDANE7WF976V34N + status: + code: 409 + message: Conflict +- request: + body: us-west-2 + headers: + Content-Length: + - '153' + User-Agent: + - !!binary | + Qm90bzMvMS4yNi4xNjUgUHl0aG9uLzMuMTAuMTMgRGFyd2luLzI0LjYuMCBCb3RvY29yZS8xLjI5 + LjE2NQ== + X-Amz-Content-SHA256: + - !!binary | + YTRhM2QyNjMxNzliZmNiYWY4ODc1NWIxNGVhOWRiMzA0OGU0MTc5YzAzYmJjOTRjY2YwZjEzYzhj + MmQzZGY2YQ== + X-Amz-Date: + - !!binary | + MjAyNjA0MjhUMTM1NDA5Wg== + amz-sdk-invocation-id: + - !!binary | + NGM2YTI3OGUtYWI2ZS00ZDY2LWE2MWMtOGQ0ODY5ZDUyODkx + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + method: PUT + uri: https://harmony-uat-staging.s3.us-west-2.amazonaws.com/ + response: + body: + string: ' + + BucketAlreadyExistsThe requested bucket name + is not available. The bucket namespace is shared by all users of the system. + Please select a different name and try again.harmony-uat-stagingXZ4WJ0RP7BAVS2S2R/NYuETRTfQoJN9u01s056AkIFtbPMBuwRZ6B001HZxo3Cy2n+h2KrN21yxcQvTKjLPfkg3jg3qbz/JsH4v3GA==' + headers: + Content-Type: + - application/xml + Date: + - Tue, 28 Apr 2026 13:54:09 GMT + Server: + - AmazonS3 + Transfer-Encoding: + - chunked + x-amz-bucket-region: + - us-west-2 + x-amz-id-2: + - R/NYuETRTfQoJN9u01s056AkIFtbPMBuwRZ6B001HZxo3Cy2n+h2KrN21yxcQvTKjLPfkg3jg3qbz/JsH4v3GA== + x-amz-request-id: + - XZ4WJ0RP7BAVS2S2 + status: + code: 409 + message: Conflict +- request: + body: !!python/object/new:_io.BytesIO + state: !!python/tuple + - !!binary | + ZmFrZSBpbWFnZSBkYXRhIGZvciB0ZXN0aW5n + - 0 + - null + headers: + Content-Length: + - '27' + Content-MD5: + - !!binary | + dTlXQ0gvREpaZ1RqQnVYbXBVOUE0UT09 + Expect: + - !!binary | + MTAwLWNvbnRpbnVl + User-Agent: + - !!binary | + Qm90bzMvMS4yNi4xNjUgUHl0aG9uLzMuMTAuMTMgRGFyd2luLzI0LjYuMCBCb3RvY29yZS8xLjI5 + LjE2NQ== + X-Amz-Content-SHA256: + - !!binary | + VU5TSUdORUQtUEFZTE9BRA== + X-Amz-Date: + - !!binary | + MjAyNjA0MjhUMTM1NDEwWg== + amz-sdk-invocation-id: + - !!binary | + ZDM3MzZjMGItNmMyZi00YjM2LWIyNGEtMWMzNGZmZjA4MTc0 + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + method: PUT + uri: https://harmony-uat-staging.s3.us-west-2.amazonaws.com/public/3d276f84-56e2-4f0a-acb2-35b9fcaaa317/9202859/PREFIRE_SAT2_2B-FLX_S07_R00_20210721013413_03040.nc.G00.png + response: + body: + string: ' + + AccessDeniedUser: arn:aws:iam::536711851782:user/NGAPShApplicationDeveloper-bcwong1-580 + is not authorized to perform: s3:PutObject on resource: "arn:aws:s3:::harmony-uat-staging/public/3d276f84-56e2-4f0a-acb2-35b9fcaaa317/9202859/PREFIRE_SAT2_2B-FLX_S07_R00_20210721013413_03040.nc.G00.png" + because no resource-based policy allows the s3:PutObject actionEQNCR3A24D1J8J7BfLXDRiPEiJDpPvoYwRr2ODSjX6WKk8+3FR0PmA0fyr053y4YnthlfNV7DZSxjiwU8J4RPm1yssvW8bdfn4NTXw==' + headers: + Connection: + - close + Content-Type: + - application/xml + Date: + - Tue, 28 Apr 2026 13:54:09 GMT + Server: + - AmazonS3 + Transfer-Encoding: + - chunked + x-amz-id-2: + - fLXDRiPEiJDpPvoYwRr2ODSjX6WKk8+3FR0PmA0fyr053y4YnthlfNV7DZSxjiwU8J4RPm1yssvW8bdfn4NTXw== + x-amz-request-id: + - EQNCR3A24D1J8J7B + status: + code: 403 + message: Forbidden +- request: + body: us-west-2 + headers: + Content-Length: + - '153' + User-Agent: + - !!binary | + Qm90bzMvMS4yNi4xNjUgUHl0aG9uLzMuMTAuMTMgRGFyd2luLzI0LjYuMCBCb3RvY29yZS8xLjI5 + LjE2NQ== + X-Amz-Content-SHA256: + - !!binary | + YTRhM2QyNjMxNzliZmNiYWY4ODc1NWIxNGVhOWRiMzA0OGU0MTc5YzAzYmJjOTRjY2YwZjEzYzhj + MmQzZGY2YQ== + X-Amz-Date: + - !!binary | + MjAyNjA0MjhUMTM1NjQ1Wg== + amz-sdk-invocation-id: + - !!binary | + N2Q1ZWIzZjEtZWEwNi00N2U1LWEzYTItN2Q3N2JhZjM5NmNj + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + method: PUT + uri: https://harmony-uat-staging.s3.us-west-2.amazonaws.com/ + response: + body: + string: ' + + BucketAlreadyExistsThe requested bucket name + is not available. The bucket namespace is shared by all users of the system. + Please select a different name and try again.harmony-uat-stagingGX93N1KJ872KXDC7LKQ8fp13yzWAVRjOAcanxIYz73LuMyti0FSQMzgf3hguKCc/2WPppBxM4OYDWN/pLrBQehzPPSk=' + headers: + Content-Type: + - application/xml + Date: + - Tue, 28 Apr 2026 13:56:46 GMT + Server: + - AmazonS3 + Transfer-Encoding: + - chunked + x-amz-bucket-region: + - us-west-2 + x-amz-id-2: + - LKQ8fp13yzWAVRjOAcanxIYz73LuMyti0FSQMzgf3hguKCc/2WPppBxM4OYDWN/pLrBQehzPPSk= + x-amz-request-id: + - GX93N1KJ872KXDC7 + status: + code: 409 + message: Conflict +- request: + body: !!python/object/new:_io.BytesIO + state: !!python/tuple + - !!binary | + ZmFrZSBpbWFnZSBkYXRhIGZvciB0ZXN0aW5n + - 0 + - null + headers: + Content-Length: + - '27' + Content-MD5: + - !!binary | + dTlXQ0gvREpaZ1RqQnVYbXBVOUE0UT09 + Expect: + - !!binary | + MTAwLWNvbnRpbnVl + User-Agent: + - !!binary | + Qm90bzMvMS4yNi4xNjUgUHl0aG9uLzMuMTAuMTMgRGFyd2luLzI0LjYuMCBCb3RvY29yZS8xLjI5 + LjE2NQ== + X-Amz-Content-SHA256: + - !!binary | + VU5TSUdORUQtUEFZTE9BRA== + X-Amz-Date: + - !!binary | + MjAyNjA0MjhUMTM1NjQ2Wg== + amz-sdk-invocation-id: + - !!binary | + M2Y4YWQzNGQtYmQzZS00M2ViLWFiYjAtNTlhYjMxZWQxNjgx + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + method: PUT + uri: https://harmony-uat-staging.s3.us-west-2.amazonaws.com/public/3d276f84-56e2-4f0a-acb2-35b9fcaaa317/9202859/PREFIRE_SAT2_2B-FLX_S07_R00_20210721013413_03040.nc.G00.png + response: + body: + string: ' + + AccessDeniedUser: arn:aws:iam::536711851782:user/NGAPShApplicationDeveloper-bcwong1-580 + is not authorized to perform: s3:PutObject on resource: "arn:aws:s3:::harmony-uat-staging/public/3d276f84-56e2-4f0a-acb2-35b9fcaaa317/9202859/PREFIRE_SAT2_2B-FLX_S07_R00_20210721013413_03040.nc.G00.png" + because no resource-based policy allows the s3:PutObject actionGX9C5QGV8VCN50B3iq7RXLPY4W/n09vtZ7afhBtw97J5kjFmBJ97/hA2Z7K62SScb5c9vjlFAleCZkvsGoFcKUDxOGI=' + headers: + Connection: + - close + Content-Type: + - application/xml + Date: + - Tue, 28 Apr 2026 13:56:46 GMT + Server: + - AmazonS3 + Transfer-Encoding: + - chunked + x-amz-id-2: + - iq7RXLPY4W/n09vtZ7afhBtw97J5kjFmBJ97/hA2Z7K62SScb5c9vjlFAleCZkvsGoFcKUDxOGI= + x-amz-request-id: + - GX9C5QGV8VCN50B3 + status: + code: 403 + message: Forbidden +version: 1 diff --git a/tests/cassettes/test_get_harmony_job_status/test_process_results_no_data.yaml b/tests/cassettes/test_get_harmony_job_status/test_process_results_no_data.yaml new file mode 100644 index 0000000..52044a1 --- /dev/null +++ b/tests/cassettes/test_get_harmony_job_status/test_process_results_no_data.yaml @@ -0,0 +1,57 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.5 + method: GET + uri: https://harmony.uat.earthdata.nasa.gov/jobs/60c6de41-a51a-4283-aa7c-2d530ebab8d9?linktype=https + response: + body: + string: '{"code":"harmony.UnauthorizedError","description":"Error: You are not + authorized to access the requested resource"}' + headers: + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '115' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 06 May 2026 19:02:26 GMT + ETag: + - W/"73-nItWkl68x6IlPn6tx7bg/IcouGY" + Server: + - CloudFront + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Via: + - 1.1 5198710b16ec61d8b7d209042d83632c.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - 1ZjYuc9kzsBuehsFfPDOLIrmP_QkWoYyu1K7GO0WkspFgpGQKMbCBA== + X-Amz-Cf-Pop: + - SFO53-P10 + X-Cache: + - Error from cloudfront + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Request-Id: + - 1ZjYuc9kzsBuehsFfPDOLIrmP_QkWoYyu1K7GO0WkspFgpGQKMbCBA== + X-XSS-Protection: + - 1; mode=block + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/tests/test_get_harmony_job_status.py b/tests/test_get_harmony_job_status.py index 8ecd6fb..e5c2507 100644 --- a/tests/test_get_harmony_job_status.py +++ b/tests/test_get_harmony_job_status.py @@ -1,27 +1,35 @@ """Unit tests for get_harmony_job_status module""" -import pytest -from moto import mock_s3 + import json -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + +import pytest import bignbit.utils -from bignbit.get_harmony_job_status import check_harmony_job, HarmonyJobNoDataError +from bignbit.get_harmony_job_status import ( + check_harmony_job, + HarmonyJobNoDataError +) @pytest.mark.vcr +@patch('bignbit.utils.get_harmony_client') @patch('bignbit.utils.get_edl_creds') @patch('bignbit.utils.boto3.client') -def test_process_results_no_data(mock_boto, mock_get_edl_creds): +def test_process_results_no_data( + mock_boto, + mock_get_edl_creds, + mock_get_harmony_client +): """Test HarmonyJobNoDataError is raised when no results are returned""" - # ---- creds MUST be tuple (fixes unpacking error) ---- + # ---- creds ---- mock_get_edl_creds.return_value = ('test_user', 'test_pass') - # ---- lambda client mock ---- + # ---- lambda client ---- mock_lambda = MagicMock() mock_boto.return_value = mock_lambda - # ---- payload must behave like AWS (realistic read()) ---- payload_data = { "access-token": "test-token" } @@ -33,7 +41,21 @@ def test_process_results_no_data(mock_boto, mock_get_edl_creds): "Payload": mock_payload } - # optional globals if your function depends on them + # ---- harmony client ---- + mock_harmony_client = MagicMock() + + # simulate empty Harmony results + mock_harmony_client.result_urls.return_value = [] + + # serializable status response + mock_harmony_client.status.return_value = { + "status": "successful", + "message": "No data found" + } + + mock_get_harmony_client.return_value = mock_harmony_client + + # optional globals bignbit.utils.ED_USER = "test" bignbit.utils.ED_PASS = "test" @@ -50,4 +72,11 @@ def test_process_results_no_data(mock_boto, mock_get_edl_creds): assert "no data" in msg assert "test_variable" in msg - assert "epsg:4326" in msg \ No newline at end of file + assert "epsg:4326" in msg + + # ---- verify mocks ---- + mock_get_harmony_client.assert_called_once_with('uat') + + mock_harmony_client.result_urls.assert_called_once() + + mock_harmony_client.status.assert_called() \ No newline at end of file