diff --git a/.gitignore b/.gitignore index c484d94..272203a 100644 --- a/.gitignore +++ b/.gitignore @@ -176,4 +176,3 @@ cython_debug/ # Allure files allure-results/ assets/ - diff --git a/test/common/common_steps.py b/test/common/common_steps.py index 6b1ac8a..1941f4e 100644 --- a/test/common/common_steps.py +++ b/test/common/common_steps.py @@ -1,3 +1,4 @@ +import json from pytest_bdd import given, then, parsers from test.helpers import Header, TrolieClient, Role @@ -19,6 +20,13 @@ def set_accept_header(content_type, client): def set_accept_encoding_header(compression_type, client): client.set_header(Header.Accept_Encoding, compression_type) +@given(parsers.parse("the Content-type header is set to `{request_type}`")) +def set_content_header(request_type, client): + client.set_header(Header.ContentType, request_type) + +@given(parsers.parse("the Content-type header is set to `{content_type}`")) +def set_content_header(content_type, client): + client.set_header(Header.ContentType, content_type) @given("the client has bad query parameters") def bad_query_parameters(client: TrolieClient): @@ -30,6 +38,15 @@ def non_empty_body(client: TrolieClient): client.set_body({"key": "value"}) client.set_header("Content-Type", "application/json") +@given(parsers.parse("the body is loaded from `{filename}`")) +def set_body_from_file(client: TrolieClient, filename): + with open(filename, "r") as f: + body = json.load(f) + client.set_body(body) + +@then("the response is 202 OK") +def request_forecast_limits_snapshot(client: TrolieClient): + assert client.get_status_code() == 202 @then("the response is 200 OK") def request_forecast_limits_snapshot(client: TrolieClient): @@ -60,7 +77,7 @@ def request_forecast_limits_snapshot_406(client: TrolieClient): @then("the response is schema-valid") def valid_snapshot(client: TrolieClient): - assert client.validate_response() + assert client.validate_response(), "Schema invalid" def conditional_get(client: TrolieClient): @@ -73,6 +90,6 @@ def empty_response(client: TrolieClient): assert client.response_is_empty() -@then(parsers.parse("the Content-Type header in the response is `{content_type}`")) +@then(parsers.parse("the Content-Type header of the response is `{content_type}`")) def content_type_header(content_type, client): assert content_type == client.get_response_header(Header.ContentType) diff --git a/test/conftest.py b/test/conftest.py index 4c4b630..0642e22 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,13 +5,13 @@ from glob import glob - pytest_plugins = [ "test.common.common_steps", "test.forecasting.step_defs" ] + def pytest_bdd_apply_tag(tag, function): using_mock = os.getenv("TROLIE_BASE_URL") == "http://localhost:4010" should_skip = tag == "prism_fail" and using_mock @@ -44,3 +44,37 @@ def pytest_bdd_after_scenario(request, feature, scenario): if client.get_status_code() >= 200 and client.get_status_code() < 300: assert client.get_response_header("ETag") + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + """Add a pretty summary of test results at the end of the pytest run.""" + passed = len(terminalreporter.stats.get('passed', [])) + failed = len(terminalreporter.stats.get('failed', [])) + skipped = len(terminalreporter.stats.get('skipped', [])) + deselected = len(terminalreporter.stats.get('deselected', [])) + warnings = len(terminalreporter.stats.get('warnings', [])) + + # Build the summary output as a string + summary_lines = [] + summary_lines.append('\n') + summary_lines.append('==================== ๐Ÿงช Test Results Summary ====================\n') + summary_lines.append(f' โœ… Passed: {passed}\n') + summary_lines.append(f' โŒ Failed: {failed}\n') + summary_lines.append(f' โš ๏ธ Skipped: {skipped}\n') + summary_lines.append(f' ๐Ÿšซ Deselected: {deselected}\n') + summary_lines.append(f' โš ๏ธ Warnings: {warnings}\n') + if passed: + summary_lines.append(f'\n โœ… Passed Tests:\n') + for rep in terminalreporter.stats.get('passed', []): + if hasattr(rep, 'nodeid'): + summary_lines.append(f' - {rep.nodeid}\n') + if failed: + summary_lines.append(f'\n โŒ Failed Tests:\n') + for rep in terminalreporter.stats.get('failed', []): + if hasattr(rep, 'nodeid'): + summary_lines.append(f' - {rep.nodeid}\n') + summary_lines.append('===============================================================\n') + + # Write to terminal as before + for line in summary_lines: + terminalreporter.write(line) diff --git a/test/forecasting/features/limit_snapshot_caching.feature b/test/forecasting/features/limit_snapshot_caching.feature index cf7e1fc..dcb86e7 100644 --- a/test/forecasting/features/limit_snapshot_caching.feature +++ b/test/forecasting/features/limit_snapshot_caching.feature @@ -17,7 +17,7 @@ Feature: Caching of Forecast Limits Snapshots supporting conditional GET And the client has obtained the current Forecast Limits Snapshot with an ETag When the client immediately issues a conditional GET for the same resource Then the response is 304 Not Modified - And the the response is empty + And the response is empty Examples: | accept_header | accept_encoding | diff --git a/test/forecasting/features/limits_proposal_formats.feature b/test/forecasting/features/limits_proposal_formats.feature new file mode 100644 index 0000000..d375fa3 --- /dev/null +++ b/test/forecasting/features/limits_proposal_formats.feature @@ -0,0 +1,38 @@ +@forecasting +Feature: Provide forecast proposal limits in appropriate formats + + As a Clearinghouse Operator + I want to provide forecast limits in a variety of formats + when a client sends the appropriate media type + So that clients can obtain the data in the format they need + Without defining a generalized query capability, like OData or GraphQL + + Background: Authenticated as a Ratings Provider + Given a TROLIE client that has been authenticated as a Ratings Provider + + # GET Obtain Forecast Proposal Status + Scenario Outline: Get the forecast proposal status + Given the Accept header is set to `` + When the client requests the status of a Forecast Proposal + Then the response is 200 OK + And the Content-Type header of the response is `` + And the response is schema-valid + + + Examples: + | content_type | + | application/vnd.trolie.rating-forecast-proposal-status.v1+json | + + # PATCH Submit a Forecast Proposal + Scenario Outline: Submit a forecast proposal + Given the Content-type header is set to `` + And the body is loaded from `` + When the client submits a Forecast Proposal + Then the response is 202 OK + And the Content-Type header of the response is `` + And the response is schema-valid + + Examples: + | request_type | file_name | response_type | + | application/vnd.trolie.rating-forecast-proposal.v1+json | data/forecast_proposal.json | application/vnd.trolie.rating-forecast-proposal-status.v1+json | + | application/vnd.trolie.rating-forecast-proposal-slim.v1+json; limit-type=apparent-power | data/forecast_proposal_slim.json | diff --git a/test/forecasting/features/limits_snapshot_filters.feature b/test/forecasting/features/limits_snapshot_filters.feature index 5dc3435..60b0bb1 100644 --- a/test/forecasting/features/limits_snapshot_filters.feature +++ b/test/forecasting/features/limits_snapshot_filters.feature @@ -11,37 +11,54 @@ Feature: Support querying subsets of the available forecasted limits Given a TROLIE client that has been authenticated as a Ratings Provider And the Accept header is set to `application/vnd.trolie.forecast-limits-snapshot.v1+json` - @prism_fail + + # Query parameters for : GET Limits Forecast Snapshot + @prism_fail Scenario Outline: Query forecast limits with offset-period-start - Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., - When the client requests forecast limits with `offset-period-start` for an hour from then at - Then the response should only include forecast limits starting at the `offset-period-start` in the server's time zone, i.e., + Given the current wall clock time at the Clearinghouse today is set to the user's current time + When the client requests forecast limits with `offset-period-start` set to after the current time + Then the response should include only forecast limits beginning at the current time plus , in the server's time zone + Examples: - | server_time | request_offset_time | response_first_period | - | 06:00:00-05:00 | 06:00:00-06:00 | 07:00:00-05:00 | - | 05:00:00-06:00 | 07:00:00-05:00 | 06:00:00-06:00 | + | offset_hours | + | 1 | + | 5 | + | 7 | + @todo Scenario: What to do when `offset-period-start` is in the past? - @prism_fail + + @prism_fail Scenario Outline: Query forecast limits with period-end + Given the current wall clock time at the Clearinghouse today is set to the user's current time + When the client requests forecast limits with `period-end` set to after the current time + Then the response should include forecast limits up to the current time plus , in the server's time zone + + Examples: + | offset_hours | + | 1 | + | 5 | + | 7 | + Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., When the client requests forecast limits with period-end Then the response should include forecast limits up to in the server's time zone Examples: | server_time | request_last_period | response_last_period | - | 06:00:00-05:00 | 09:00:00-06:00 | 10:00:00-05:00 | + | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + Scenario Outline: Query forecast limits with monitoring-set filter When the client requests forecast limits with monitoring-set filter Then the response should include forecast limits for the monitoring set Examples: | monitoring_set_id | - | default | + | default | - @requires_model + @requires_model Scenario Outline: Query forecast limits with resource-id filter When the client requests forecast limits with resource-id filter Then the response should include forecast limits for the resource id @@ -49,7 +66,100 @@ Feature: Support querying subsets of the available forecasted limits | resource_id | | 8badf00d | - @prism_fail + @prism_fail Scenario: Query forecast limits with static-only When the client requests forecast limits with static-only set to true Then the response should include only static forecast limits + + # Query parameters for : GET Historical Limits Forecast Snapshot + Scenario Outline: Query historical limits forecast snapshots with offset-period-start + Given the current wall clock time at the Clearinghouse today is set to the user's current time + When the client requests historical forecast limits with `offset-period-start` set to after the current time + Then the response should include only forecast limits beginning at the current time plus , in the server's time zone + + Examples: + | offset_hours | + | 1 | + | 5 | + | 7 | + + + Scenario Outline: Query historical limits forecast snapshots with period-end + Given the current wall clock time at the Clearinghouse today is set to the user's current time + When the client requests historical forecast limits with `period-end` set to after the current time + Then the response should include forecast limits up to the current time plus , in the server's time zone + + Examples: + | offset_hours | + | 1 | + | 5 | + | 7 | + + + Scenario Outline: Query historical limits forecast snapshots with monitoring-set + When the client requests historical forecast limits with monitoring-set filter + Then the response should include forecast limits for the monitoring set + + Examples: + | monitoring_set_id | + | TO1 | + + Scenario Outline: Query historical limits forcasting snapshots with resource-id + When the client requests historical forecast limits with resource-id filter + Then the response should include forecast limits for the resource id + + Examples: + | resource_id | + | DOUGLAS.T538.1 OUT | + | PARKHILL.T5.T5 | + | HEARN.34562.1 | + + Scenario Outline: Query historical limits forcasting snapshots with static-only + When the client requests historical forecast limits with static-only set to true + Then the response should include only static forecast limits + + # Query parameters for : GET Regional Limits Forecast Snapshot + @offset_regional + Scenario Outline: Query regional limits forecast snapshots with offset-period-start + Given the current wall clock time at the Clearinghouse today is set to the user's current time + When the client requests regional forecast limits with `offset-period-start` set to after the current time + Then the response should include only forecast limits beginning at the current time plus , in the server's time zone + + Examples: + | offset_hours | + | 1 | + | 5 | + | 7 | + @offset_regional + Scenario Outline: Query regional limits forecast snapshots with period-end + Given the current wall clock time at the Clearinghouse today is set to the user's current time + When the client requests regional forecast limits with `period-end` set to after the current time + Then the response should include forecast limits up to the current time plus , in the server's time zone + + Examples: + | offset_hours | + | 1 | + | 5 | + | 7 | + + Scenario Outline: Query regional limits forecast snapshots with monitoring-set + When the client requests regional forecast limits with monitoring-set filter + Then the response should include forecast limits for the monitoring set + + Examples: + | monitoring_set_id | + | TO1 | + + Scenario Outline: Query regional limits forecast snapshots with resource-id + When the client requests regional forecast limits with resource-id filter + Then the response should include forecast limits for the resource id + + Examples: + | resource_id | + | DOUGLAS.T538.1 OUT | + | PARKHILL.T5.T5 | + | HEARN.34562.1 | + + Scenario: Query regional limits forecast snapshots with static-only + When the client requests regional forecast limits with static-only set to true + Then the response should include only static forecast limits diff --git a/test/forecasting/features/limits_snapshot_formats.feature b/test/forecasting/features/limits_snapshot_formats.feature index 93faed2..7e2c471 100644 --- a/test/forecasting/features/limits_snapshot_formats.feature +++ b/test/forecasting/features/limits_snapshot_formats.feature @@ -10,6 +10,7 @@ Feature: Provide forecast limits in appropriate formats Background: Authenticated as a Ratings Provider Given a TROLIE client that has been authenticated as a Ratings Provider + # GET Limits Forecast Snapshot Scenario Outline: Obtaining the latest forecast snapshot Given the Accept header is set to `` When the client requests the current Forecast Limits Snapshot @@ -20,9 +21,10 @@ Feature: Provide forecast limits in appropriate formats | content_type | | application/vnd.trolie.forecast-limits-snapshot.v1+json | | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json | - | application/vnd.trolie.forecast-limits-snapshot.v1+json; include-psr-header=false | - | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json; include-psr-header=false | + | application/vnd.trolie.forecast-limits-snapshot.v1+json;include-psr-header=false | + | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json;include-psr-header=false | + # GET Limits Forecast Snapshot (Slim format) Scenario Outline: Obtaining the latest slim forecast snapshot Given the Accept header is set to `` When the client requests the current Forecast Limits Snapshot @@ -99,7 +101,7 @@ Feature: Provide forecast limits in appropriate formats | application/vnd.trolie.forecast-limits-snapshot-slim.v1+json; limit-type=apparent-power | #| application/vnd.trolie.forecast-limits-snapshot-slim.v1+json; limit-type=apparent-power, inputs-used=true | #| application/vnd.trolie.forecast-limits-snapshot-slim.v1+json; inputs-used=true, limit-type=apparent-power | - + Scenario Outline: Sending a body with a GET request is a bad request Given the Accept header is set to `` And the client has a non-empty body @@ -129,3 +131,69 @@ Feature: Provide forecast limits in appropriate formats #| */* | #| application/vnd.trolie.forecast-limits-snapshot-slim.v1+json, limit-type=apparent-power | #| application/vnd.trolie.forecast-limits-snapshot-slim.v1+json; inputs-used=true; limit-type=apparent-power | + + # GET Historical Limits Forecast Snapshot + @todo + Scenario Outline: Get historical limits forecast snapshot + Given the Accept header is set to `` + When the client requests a Historical Forecast Limits Snapshot + Then the response is 200 OK + And the Content-Type header in the response is `` + And the response is schema-valid + + Examples: + | content_type | time_frame | + | application/vnd.trolie.forecast-limits-snapshot.v1+json | 2025-07-12T03:00:00-05:00 | + + # GET Regional Limits Forecast Snapshot + Scenario Outline: Get regional limits forecast snapshot + Given the Accept header is set to `` + When the client requests a Regional Forecast Limits Snapshot + Then the response is 200 OK + And the Content-Type header in the response is `` + And the response is schema-valid + + Examples: + | content_type | + | application/vnd.trolie.forecast-limits-snapshot.v1+json | + | application/vnd.trolie.forecast-limits-snapshot.v1+json; include-psr-header=false | + + # | application/vnd.trolie.forecast-limits-snapshot-slim.v1+json;limit-type=apparent-power | + # | application/vnd.trolie.forecast-limits-snapshot-slim.v1+json;limit-type=apparent-power; inputs-used=true | + # | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json | + # | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json; include-psr-header=false | + + Scenario Outline: Sending a body with a GET Regional Limits Forecast Snapshot is a bad request + Given the Accept header is set to `` + And the client has a non-empty body + When the client requests a Regional Forecast Limits Snapshot + Then the response is 400 Bad Request + And the Content-Type header in the response is `application/vnd.trolie.error.v1+json` + And the response is schema-valid + + Examples: + | content_type | + | application/vnd.trolie.forecast-limits-snapshot.v1+json | + | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json | + | application/vnd.trolie.forecast-limits-snapshot.v1+json; include-psr-header=false | + | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json; include-psr-header=false | + | application/vnd.trolie.forecast-limits-snapshot-slim.v1+json; limit-type=apparent-power | + + + # POST Update Regional Limits Forecast Snapshot + Scenario Outline: Update Regional Limits Forecast Snapshot + Given the Content-type header is set to `` + And the body is loaded from `` + When the client submits a Regional Forecast Limits Snapshot + Then the response is 202 OK + And the Content-Type header in the response is `` + And the response is schema-valid + + + + Examples: + | content_type | file_name | response_type | + | application/vnd.trolie.rating-forecast-proposal.v1+json | data/forecast_snapshot.json | application/vnd.trolie.rating-forecast-proposal-status.v1+json | + #| application/vnd.trolie.rating-forecast-proposal-slim.v1+json; limit-type=apparent-power | data/forecast_proposal_slim.json | application/vnd.trolie.rating-forecast-proposal-status.v1+json | + + diff --git a/test/forecasting/features/require_authentication.feature b/test/forecasting/features/require_authentication.feature index 7ff34e9..c4e7d44 100644 --- a/test/forecasting/features/require_authentication.feature +++ b/test/forecasting/features/require_authentication.feature @@ -1,4 +1,4 @@ -@skip_rate_limiting @forecasting +@skip_rate_limiting @forecasting @auth Feature: All Forecasting requests require authentication As a Clearinghouse Operator I want to ensure that requests that cannot be authenticated receive 401 Unauthorized diff --git a/test/forecasting/forecast_helpers.py b/test/forecasting/forecast_helpers.py index c22c23e..ba80c27 100644 --- a/test/forecasting/forecast_helpers.py +++ b/test/forecasting/forecast_helpers.py @@ -1,10 +1,16 @@ -from datetime import datetime -from test.helpers import TrolieClient +from datetime import datetime, timedelta +from test.helpers import TrolieClient, get_period def get_forecast_limits_snapshot(client: TrolieClient): return client.request("/limits/forecast-snapshot") +def get_regional_limits_forecast_snapshot(client: TrolieClient): + return client.request("/limits/regional/forecast-snapshot") + +def get_historical_limits_forecast_snapshot(client: TrolieClient): + return client.request(f"/limits/forecast-snapshot/{get_period(-1)}") + def get_todays_iso8601_for(time_with_timezone: str) -> str: iso8601_offset = datetime.now().strftime(f"%Y-%m-%dT{time_with_timezone}") @@ -14,9 +20,16 @@ def get_todays_iso8601_for(time_with_timezone: str) -> str: raise ValueError(f"Invalid ISO8601 format: {iso8601_offset}") return iso8601_offset +def round_up_to_next_hour(dt: datetime) -> datetime: + if dt.minute == 0 and dt.second == 0 and dt.microsecond == 0: + return dt + return (dt + timedelta(hours=1)).replace(minute=0, second=0, microsecond=0) def get_etag(client: TrolieClient): etag = client.get_response_header("ETag") + print("ETag:", etag) + print("Status code:", client.get_status_code()) + # Verify ETag exists as it's required for the caching test assert etag is not None and client.get_status_code() == 200 # Verify ETag is not a weak ETag diff --git a/test/forecasting/step_defs/limits_snapshot_filters.py b/test/forecasting/step_defs/limits_snapshot_filters.py index c8a29a8..0235743 100644 --- a/test/forecasting/step_defs/limits_snapshot_filters.py +++ b/test/forecasting/step_defs/limits_snapshot_filters.py @@ -1,68 +1,152 @@ +import json from pytest_bdd import given, when, then, parsers +from dateutil import parser from test.helpers import TrolieClient from datetime import datetime, timedelta from test.forecasting.forecast_helpers import ( get_forecast_limits_snapshot, + get_historical_limits_forecast_snapshot, + get_regional_limits_forecast_snapshot, get_todays_iso8601_for, + round_up_to_next_hour, ) +@given(parsers.parse("the current wall clock time at the Clearinghouse today is set to the user's current time")) +def set_clearinghouse_time_to_user_time(client: TrolieClient): + user_time = datetime.now().astimezone() + client.set_server_time(user_time.isoformat()) + +@given(parsers.parse("the period requested is set to {period_requested}")) +def set_historical_forecast_period(period_requested, client: TrolieClient): + return client.request(f"limits/forecast-snapshot/{period_requested}") + +@given(parsers.parse("the period requested is set to {period_requested}")) +def set_historical_forecast_period(period_requested, client: TrolieClient): + return client.request(f"limits/forecast-snapshot/{period_requested}") + +@when(parsers.parse("the client requests historical forecast limits with `offset-period-start` for an hour from then at {request_offset_time}")) +def historical_forecast_snapshot_request_filter_offset_period_start(request_offset_time, client: TrolieClient): + client.set_query_param("offset-period-start", get_todays_iso8601_for(request_offset_time)) + get_historical_limits_forecast_snapshot(client) -@given(parsers.parse("the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., {server_time}")) -def clearinghouse_wall_clock_today_at_11amGMT(server_time, client: TrolieClient): - client.set_header( - "X-TROLIE-Testing-Current-DateTime", - get_todays_iso8601_for(server_time), - ) +@when(parsers.parse("the client requests regional forecast limits with `offset-period-start` for an hour from then at {request_offset_time}")) +def regional_forcast_snapshot_request_filter_offset_period_start(request_offset_time, client: TrolieClient): + client.set_query_param("offset-period-start", get_todays_iso8601_for(request_offset_time)) + get_regional_limits_forecast_snapshot(client) -@when(parsers.parse("the client requests forecast limits with `offset-period-start` for an hour from then at {request_offset_time}")) -def forecast_snapshot_request_filter_offset_period_start(request_offset_time, client: TrolieClient): - client.set_query_param("offset-period-start", get_todays_iso8601_for(request_offset_time)) +@when(parsers.parse("the client requests forecast limits with `offset-period-start` set to {offset_hours} after the current time")) +def forecast_snapshot_request_filter_offset_period_start(offset_hours, client: TrolieClient): + offset = timedelta(hours=int(offset_hours)) + request_time = parser.isoparse(client.get_server_time()) + offset + client.set_query_param("offset-period-start", request_time.isoformat()) get_forecast_limits_snapshot(client) - -@when(parsers.parse("the client requests forecast limits with period-end {request_last_period}")) -def forecast_snapshot_request_filter_last_period(request_last_period, client: TrolieClient): - client.set_query_param("period-end", get_todays_iso8601_for(request_last_period)) +@when(parsers.parse("the client requests forecast limits with `period-end` set to {offset_hours} after the current time")) +def forecast_snapshot_request_filter_period_end(offset_hours, client: TrolieClient): + offset = timedelta(hours=int(offset_hours)) + request_time = parser.isoparse(client.get_server_time()) + offset + client.set_query_param("period-end", request_time.isoformat()) get_forecast_limits_snapshot(client) +@when(parsers.parse("the client requests historical forecast limits with `offset-period-start` set to {offset_hours} after the current time")) +def historical_forecast_snapshot_request_filter_offset_period_start(offset_hours, client: TrolieClient): + offset = timedelta(hours=int(offset_hours)) + request_time = parser.isoparse(client.get_server_time()) + offset + client.set_query_param("offset-period-start", request_time.isoformat()) + get_historical_limits_forecast_snapshot(client) + +@when(parsers.parse("the client requests historical forecast limits with `period-end` set to {offset_hours} after the current time")) +def historical_forecast_snapshot_request_filter_period_end(offset_hours, client: TrolieClient): + offset = timedelta(hours=int(offset_hours)) + request_time = parser.isoparse(client.get_server_time()) + offset + client.set_query_param("period-end", request_time.isoformat()) + get_historical_limits_forecast_snapshot(client) + +@when(parsers.parse("the client requests regional forecast limits with `offset-period-start` set to {offset_hours} after the current time")) +def regional_forecast_snapshot_request_filter_offset_period_start(offset_hours, client: TrolieClient): + offset = timedelta(hours=int(offset_hours)) + request_time = parser.isoparse(client.get_server_time()) + offset + client.set_query_param("offset-period-start", request_time.isoformat()) + get_regional_limits_forecast_snapshot(client) + +@when(parsers.parse("the client requests regional forecast limits with `period-end` set to {offset_hours} after the current time")) +def regional_forecast_snapshot_request_filter_period_end(offset_hours, client: TrolieClient): + offset = timedelta(hours=int(offset_hours)) + request_time = parser.isoparse(client.get_server_time()) + offset + client.set_query_param("period-end", request_time.isoformat()) + get_regional_limits_forecast_snapshot(client) @when(parsers.parse("the client requests forecast limits with static-only set to true")) def forecast_snapshot_request_filter_static_only(client: TrolieClient): client.set_query_param("static-only", "true") get_forecast_limits_snapshot(client) +@when(parsers.parse("the client requests historical forecast limits with static-only set to true")) +def historical_snapshot_request_filter_static_only(client: TrolieClient): + client.set_query_param("static-only", "true") + get_historical_limits_forecast_snapshot(client) + +@when(parsers.parse("the client requests regional forecast limits with static-only set to true")) +def regional_forecast_snapshot_request_filter_static_only(client: TrolieClient): + client.set_query_param("static-only", "true") + get_regional_limits_forecast_snapshot(client) @when(parsers.parse("the client requests forecast limits with monitoring-set filter {monitoring_set_id}")) def forecast_snapshot_request_filter_monitoring_set(monitoring_set_id, client: TrolieClient): client.set_query_param("monitoring-set", monitoring_set_id) get_forecast_limits_snapshot(client) +@when(parsers.parse("the client requests historical forecast limits with monitoring-set filter {monitoring_set_id}")) +def historical_forecast_snapshot_request_filter_monitoring_set(monitoring_set_id, client: TrolieClient): + client.set_query_param("monitoring-set", monitoring_set_id) + get_historical_limits_forecast_snapshot(client) + +@when(parsers.parse("the client requests regional forecast limits with monitoring-set filter {monitoring_set_id}")) +def regional_forecast_snapshot_request_filter_monitoring_set(monitoring_set_id, client: TrolieClient): + client.set_query_param("monitoring-set", monitoring_set_id) + get_regional_limits_forecast_snapshot(client) + @when(parsers.parse("the client requests forecast limits with resource-id filter {resource_id}")) def forecast_snapshot_request_filter_resource_id(resource_id, client: TrolieClient): client.set_query_param("resource-id-filter", resource_id) get_forecast_limits_snapshot(client) - -@then(parsers.parse("the response should only include forecast limits starting at the `offset-period-start` in the server's time zone, i.e., {response_first_period}")) -def forecast_snapshot_request_first_period_starts_on(response_first_period, client: TrolieClient): - expected_start = get_todays_iso8601_for(response_first_period) +@when(parsers.parse("the client requests historical forecast limits with resource-id filter {resource_id}")) +def historical_forecast_snapshot_request_filter_resource_id(resource_id, client : TrolieClient): + client.set_query_param("resource-id", resource_id) + get_historical_limits_forecast_snapshot(client) + +@when(parsers.parse("the client requests regional forecast limits with resource-id filter {resource_id}")) +def regional_forecast_snapshot_request_filter_resource_id(resource_id, client: TrolieClient): + client.set_query_param("resource-id", resource_id) + get_regional_limits_forecast_snapshot(client) + +@then(parsers.parse("the response should include only forecast limits beginning at the current time plus {offset_hours}, in the server's time zone")) +def forecast_snapshot_request_first_period_starts_on(offset_hours, client: TrolieClient): + offset = timedelta(hours=int(offset_hours)) + exact_expected_start = (parser.isoparse(client.get_server_time()) + offset).isoformat() + rounded_expected_start = round_up_to_next_hour(parser.isoparse(exact_expected_start)).isoformat() limits = client.get_json()["limits"] targets = ((entry["resource-id"], entry["periods"][0]["period-start"]) for entry in limits) + print("Expected start time: ", exact_expected_start) for resource_id, period_start in targets: - assert expected_start == period_start, f"Failed for resource {resource_id}" - - -@then(parsers.parse("the response should include forecast limits up to {response_last_period} in the server's time zone")) -def forecast_snapshot_request_last_period_includes(response_last_period, client: TrolieClient): - expected_end = get_todays_iso8601_for(response_last_period) + print("Period start: ", period_start) + assert parser.isoparse(rounded_expected_start) == parser.isoparse(period_start), f"Failed for resource {resource_id}" + +@then(parsers.parse("the response should include forecast limits up to the current time plus {offset_hours}, in the server's time zone")) +def forecast_snapshot_request_last_period_includes(offset_hours, client: TrolieClient): + offset = timedelta(hours=int(offset_hours)) + exact_expected_end = (parser.isoparse(client.get_server_time()) + offset).isoformat() + rounded_expected_end = round_up_to_next_hour(parser.isoparse(exact_expected_end)).isoformat() limits = client.get_json()["limits"] targets = ((limits_entry["resource-id"], limits_entry["periods"][-1]["period-end"]) for limits_entry in limits) + print("Expected end time: ", exact_expected_end) for resource_id, period_end in targets: - assert expected_end == period_end, f"Failed for resource {resource_id}" - - + print("Period end: ", period_end) + assert parser.isoparse(rounded_expected_end) == parser.isoparse(period_end), f"Failed for resource {resource_id}" + @then(parsers.parse("the response should include only static forecast limits")) def forecast_snapshot_request_only_static(client: TrolieClient): limits = client.get_json()["limits"] @@ -90,3 +174,6 @@ def forecast_snapshot_request_monitoring_set_includes(monitoring_set_id, client: def forecast_snapshot_contains_requested_resource(resource_id, client: TrolieClient): resources = client.get_json()["snapshot-header"]["power-system-resources"] assert resource_id in [resource["resource-id"] for resource in resources], f"Failed for resource {resource_id}" + + + diff --git a/test/forecasting/step_defs/limits_snapshot_formats.py b/test/forecasting/step_defs/limits_snapshot_formats.py index 76708c4..57c5b24 100644 --- a/test/forecasting/step_defs/limits_snapshot_formats.py +++ b/test/forecasting/step_defs/limits_snapshot_formats.py @@ -1,8 +1,24 @@ -from pytest_bdd import given, when -from test.helpers import TrolieClient - +import json +from pytest_bdd import given, when, then, parsers +from test.helpers import TrolieClient, Header +from test.forecasting.forecast_helpers import ( + get_forecast_limits_snapshot, + get_todays_iso8601_for, +) @when("the client requests the current Forecast Limits Snapshot") @given("the client requests the current Forecast Limits Snapshot") def request_forecast_limits_snapshot(client: TrolieClient): return client.request("/limits/forecast-snapshot") + +def print_forecast_snapshot(client: TrolieClient): + print("REQUEST BODY SENT: ", json.dumps(client._TrolieClient__body, indent=2)) + print("RESPONSE CONTENT-TYPE:", client._TrolieClient__response.headers.get("Content-type")) + print("RESPONSE STATUS:", client.get_status_code()) + print("REQUEST BODY RECIEVED: ", json.dumps(client.get_json(), indent=2)) + print("\n") + +@when("the client requests a Historical Forecast Limits Snapshot at time frame {time_frame}") +def request_historical_forecast_limits_snapshot_at_period(client: TrolieClient, time_frame): + return client.request(f"/limits/forecast-snapshot/{time_frame}") + diff --git a/test/helpers.py b/test/helpers.py index 6e2b3b1..5297202 100644 --- a/test/helpers.py +++ b/test/helpers.py @@ -111,6 +111,9 @@ def get_status_code(self) -> int: def response_is_empty(self) -> bool: return len(self.__response.content) == 0 + def get_server_time(self): + return self.__headers["X-TROLIE-Testing-Current-DateTime"] + @dataclass class ResponseInfo: verb: str