diff --git a/bignbit/utils.py b/bignbit/utils.py
index 57b63e5..6bbc6a8 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
@@ -345,9 +346,49 @@ 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 = (
+ os.environ.get("AWS_REGION") or
+ os.environ.get("AWS_DEFAULT_REGION") or
+ session.region_name or
+ "us-west-2"
+ )
+
+ 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())
+ print(type(response_payload))
+ print(response_payload)
+ token = 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
)
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
index 67b421d..52044a1 100644
--- 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
@@ -9,270 +9,49 @@ interactions:
Connection:
- keep-alive
User-Agent:
- - python-requests/2.32.4
+ - 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: 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
+ string: '{"code":"harmony.UnauthorizedError","description":"Error: You are not
+ authorized to access the requested resource"}'
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'
+ - '115'
Content-Type:
- application/json; charset=utf-8
Date:
- - Fri, 06 Feb 2026 01:14:14 GMT
+ - Wed, 06 May 2026 19:02:26 GMT
ETag:
- - W/"2f1-b0Cm0wV82BHxA0lzpZmKudRDANo"
+ - 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: 200
- message: OK
+ 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 5265c40..e5c2507 100644
--- a/tests/test_get_harmony_job_status.py
+++ b/tests/test_get_harmony_job_status.py
@@ -1,24 +1,82 @@
"""Unit tests for get_harmony_job_status module"""
+
+import json
+from unittest.mock import MagicMock, patch
+
import pytest
-from moto import mock_s3
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
-@mock_s3
-def test_process_results_no_data():
- """Test that HarmonyJobNoDataError is raised when Harmony returns no data"""
- 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
+@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,
+ mock_get_harmony_client
+):
+ """Test HarmonyJobNoDataError is raised when no results are returned"""
+
+ # ---- creds ----
+ mock_get_edl_creds.return_value = ('test_user', 'test_pass')
+
+ # ---- lambda client ----
+ mock_lambda = MagicMock()
+ mock_boto.return_value = mock_lambda
+
+ payload_data = {
+ "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
+ }
+
+ # ---- 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"
+
+ # ---- 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 msg
+ assert "test_variable" in msg
+ assert "epsg:4326" in msg
+
+ # ---- verify mocks ----
+ mock_get_harmony_client.assert_called_once_with('uat')
+
+ mock_harmony_client.result_urls.assert_called_once()
- 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
+ mock_harmony_client.status.assert_called()
\ No newline at end of file
diff --git a/tests/test_handle_big_result.py b/tests/test_handle_big_result.py
index 0190c20..247eef8 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, mock_sts
+from unittest.mock import patch, MagicMock
import bignbit.utils
from bignbit.handle_big_result import (
@@ -20,8 +21,17 @@
@pytest.mark.vcr
@mock_sts
@mock_s3
-def test_process_harmony_results():
+@patch('bignbit.utils.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'{"access-token": "test-token"}')
+ }
+
bignbit.utils.AWS_ACCOUNT_ID = None
bignbit.utils.ED_USER = 'test'
bignbit.utils.ED_PASS = 'test'
@@ -65,8 +75,16 @@ def test_process_harmony_results():
assert file['variable'] == 'flx'
@pytest.mark.vcr
-def test_process_harmony_results_no_data():
+@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()
+ mock_boto.return_value = mock_lambda
+
+ mock_lambda.invoke.return_value = {
+ "Payload": MagicMock(read=lambda: b'{"access-token": "test-token"}')
+ }
+
bignbit.utils.ED_USER = 'test'
bignbit.utils.ED_PASS = 'test'
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 85461b0..5ea92a7 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,3 +1,4 @@
+import os
import json
import pathlib
import tempfile
@@ -16,7 +17,8 @@
extract_granule_dates,
parse_datetime,
parse_doy,
- get_harmony_client
+ get_harmony_client,
+ Environment
)
@@ -463,58 +465,85 @@ 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()
# ---------------------------------------------------------------------------
+@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_client):
- """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
-
- # Reset global client
+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
- result = get_harmony_client('UAT')
-
- assert result == mock_client_instance
- mock_client.assert_called_once()
+ # --- creds ---
+ mock_get_edl_creds.return_value = ('test_user', 'test_pass')
+ # --- session / region ---
+ mock_session.return_value.region_name = "us-west-2"
-@patch('bignbit.utils.Client')
-@patch('bignbit.utils.get_edl_creds')
-def test_get_harmony_client_prod(mock_get_edl_creds, mock_client):
- """Test getting Harmony client for PROD environment."""
- mock_get_edl_creds.return_value = ('test_user', 'test_pass')
+ # --- harmony client ---
mock_client_instance = MagicMock()
- mock_client.return_value = mock_client_instance
+ mock_harmony_client.return_value = mock_client_instance
- # Reset global client
- import bignbit.utils
- bignbit.utils.HARMONY_CLIENT = None
+ # --- lambda client ---
+ mock_lambda = MagicMock()
+ mock_boto.return_value = mock_lambda
- result = get_harmony_client('PROD')
+ # 👇 double-encoded JSON (required by function)
+ # inner = json.dumps({"access-token": "test-token"})
+ # outer = json.dumps(inner).encode("utf-8")
- assert result == mock_client_instance
+ # mock_payload = MagicMock()
+ # mock_payload.read.return_value = outer
+ # mock_lambda.invoke.return_value = {
+ # "Payload": mock_payload
+ # }
-@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):
- """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
+ # ---- payload must behave like AWS (realistic read()) ----
+ payload_data = {
+ "access-token": "test-token"
+ }
- # Reset global client
- import bignbit.utils
+ mock_payload = MagicMock()
+ 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
- result = get_harmony_client('SIT')
+ # --- execute ---
+ result = bignbit.utils.get_harmony_client(input_env)
+ # --- assertions ---
assert result == mock_client_instance
+
+ # 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