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