From 543ad6997ce07e79681cbcc5e3aedcaf02f83675 Mon Sep 17 00:00:00 2001 From: Alexander Yen Date: Wed, 23 Jul 2025 12:39:28 -0700 Subject: [PATCH 1/6] Added multiple changes features in forecasting --- .gitignore | 21 ++++ test/common/common_steps.py | 15 ++- test/conftest.py | 43 ++++++- .../features/limit_snapshot_caching.feature | 2 +- .../features/limits_snapshot_filters.feature | 111 ++++++++++++++++-- .../features/limits_snapshot_formats.feature | 109 ++++++++++++++++- .../features/require_authentication.feature | 2 +- test/forecasting/forecast_helpers.py | 11 +- .../step_defs/limits_snapshot_caching.py | 2 +- .../step_defs/limits_snapshot_filters.py | 73 +++++++++++- .../step_defs/limits_snapshot_formats.py | 22 +++- 11 files changed, 388 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 895cd64..bad14c9 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,24 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +.vscode/ + +# Don't want to push my conformed pynb +Forecasting_Conformance_Profile.ipynb + +.github/gpg.exe +pytest.ini + +# lep changes don't push +LEP_Changes_Needed.txt + +# Allure files +allure-results/ +assets/ + +# Init files +test/__init__.py +test/forecasting/__init__.py +test/forecasting/step_defs/__init__.py +test/common/__init__.py \ No newline at end of file diff --git a/test/common/common_steps.py b/test/common/common_steps.py index 6b1ac8a..86ee5c4 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,9 @@ 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 `{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 +34,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 +73,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): diff --git a/test/conftest.py b/test/conftest.py index 738ddcf..8d25514 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -9,9 +9,14 @@ # we need to help pytest-bdd find the step definitions. The simplest way to do this is # get them loaded via the pytest_plugins. # https://gist.github.com/peterhurford/09f7dcda0ab04b95c026c60fa49c2a68?permalink_comment_id=3453153#gistcomment-3453153 +# pytest_plugins = [ +# fixture.replace("/", ".").replace(".py", "") +# for fixture in glob("test/**/step_defs/*.py") +# if "__" not in fixture +# ] + ["test.common.common_steps"] pytest_plugins = [ - fixture.replace("/", ".").replace(".py", "") - for fixture in glob("test/**/step_defs/*.py") + fixture.replace("\\", ".").replace("/", ".").replace(".py", "") + for fixture in glob("test/**/step_defs/*.py", recursive=True) if "__" not in fixture ] + ["test.common.common_steps"] @@ -48,3 +53,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_snapshot_filters.feature b/test/forecasting/features/limits_snapshot_filters.feature index 5dc3435..a3df942 100644 --- a/test/forecasting/features/limits_snapshot_filters.feature +++ b/test/forecasting/features/limits_snapshot_filters.feature @@ -11,7 +11,9 @@ 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 @@ -19,29 +21,33 @@ Feature: Support querying subsets of the available forecasted limits 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 | + + | 15:00:00-05:00 | 17:00:00-05:00 | 17:00:00-05:00 | + # | 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 | @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 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 | + # | 06:00:00-05:00 | 09:00:00-06:00 | 10: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 | + | TO1 | - @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 +55,96 @@ 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 is today at 11am GMT, i.e., + # And the period requested is set to + When the client requests historical 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., + + Examples: + | server_time | period_requested | request_offset_time | response_first_period | + # | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + # | 06:00:00-05:00 | 2023-07-12T16:00:00-07:00 | 09:00:00-06:00 | 10:00:00-05:00 | + + + Scenario Outline: Query historical limits forecast snapshots with period-end + Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., + When the client requests historical 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 | 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 | + + 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 + Scenario Outline: Query regional limits forecast snapshots with offset-period-start + Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., + When the client requests regional 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., + + Examples: + | server_time | request_offset_time | response_first_period | + | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + # | 05:00:00-06:00 | 07:00:00-05:00 | 06:00:00-06:00 | + + Scenario Outline: Query regional limits forecast snapshots with period-end + Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., + When the client requests regional 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 | + | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + # | 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 | + + 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..f0c931b 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,8 @@ 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 | - + + # sends 200 OK , false 200 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 +132,103 @@ 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 + @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 + @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 | + # include-psr-header=false throws error + | 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 | + + @forecast_snapshot + 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 + @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 | + + + # GET Obtain Forecast Proposal Status + @forecast_proposal + 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 in 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 + @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 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_proposal.json | application/vnd.trolie.rating-forecast-proposal-status.v1+json | + # Does not work + # | application/vnd.trolie.rating-forecast-proposal-slim.v1+json; limit-type=apparent-power | data/forecast_proposal_slim.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..a6247c6 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 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}") @@ -17,6 +23,9 @@ def get_todays_iso8601_for(time_with_timezone: str) -> str: 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_caching.py b/test/forecasting/step_defs/limits_snapshot_caching.py index 2a76d93..79fc809 100644 --- a/test/forecasting/step_defs/limits_snapshot_caching.py +++ b/test/forecasting/step_defs/limits_snapshot_caching.py @@ -8,7 +8,7 @@ @given("the client has obtained the current Forecast Limits Snapshot with an ETag", target_fixture="etag") def get_etag_for_forecast_limits_snapshot(client): - client.set_server_time(base_time) + # client.set_server_time(base_time) return get_etag(get_forecast_limits_snapshot(client)) diff --git a/test/forecasting/step_defs/limits_snapshot_filters.py b/test/forecasting/step_defs/limits_snapshot_filters.py index c8a29a8..62a8d7c 100644 --- a/test/forecasting/step_defs/limits_snapshot_filters.py +++ b/test/forecasting/step_defs/limits_snapshot_filters.py @@ -1,8 +1,12 @@ +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, ) @@ -14,6 +18,19 @@ def clearinghouse_wall_clock_today_at_11amGMT(server_time, client: TrolieClient) get_todays_iso8601_for(server_time), ) +@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) + +@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): @@ -26,32 +43,71 @@ def forecast_snapshot_request_filter_last_period(request_last_period, client: Tr client.set_query_param("period-end", get_todays_iso8601_for(request_last_period)) get_forecast_limits_snapshot(client) +@when(parsers.parse("the client requests historical forecast limits with period-end {request_last_period}")) +def historical_forecast_snapshot_request_filter_last_period(request_last_period, client: TrolieClient): + client.set_query_param("period-end", get_todays_iso8601_for(request_last_period)) + get_historical_limits_forecast_snapshot(client) + +@when(parsers.parse("the client requests regional forecast limits with period-end {request_last_period}")) +def regional_forecast_snapshot_request_filter_last_period(request_last_period, client: TrolieClient): + client.set_query_param("period-end", get_todays_iso8601_for(request_last_period)) + 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) +@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 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) + # print(client.get_json()) limits = client.get_json()["limits"] targets = ((entry["resource-id"], entry["periods"][0]["period-start"]) for entry in limits) for resource_id, period_start in targets: - assert expected_start == period_start, f"Failed for resource {resource_id}" + assert parser.isoparse(expected_start) == parser.isoparse(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")) @@ -60,7 +116,7 @@ def forecast_snapshot_request_last_period_includes(response_last_period, client: limits = client.get_json()["limits"] targets = ((limits_entry["resource-id"], limits_entry["periods"][-1]["period-end"]) for limits_entry in limits) for resource_id, period_end in targets: - assert expected_end == period_end, f"Failed for resource {resource_id}" + assert parser.isoparse(expected_end) == parser.isoparse(period_end), f"Failed for resource {resource_id}" @then(parsers.parse("the response should include only static forecast limits")) @@ -90,3 +146,16 @@ 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}" + +#todo +@then(parsers.parse("see output {response_first_period}")) +def forecase_snapshot_request_past_period(response_first_period, client: TrolieClient): + expected_start = get_todays_iso8601_for(response_first_period) + print("Expected value: ", expected_start) + limits = client.get_json()["limits"] + print(limits) + targets = ((entry["resource-id"], entry["periods"][0]["period-start"]) for entry in limits) + for resource_id, period_start in targets: + assert expected_start == period_start, 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}") + From fe56c7d18061ff4f0bedd0555e7875be3931cd9b Mon Sep 17 00:00:00 2001 From: Alexander Yen Date: Thu, 24 Jul 2025 11:24:18 -0700 Subject: [PATCH 2/6] Reviewed PR comments and made appropriate changes --- .gitignore | 6 --- test/conftest.py | 6 +-- .../features/limits_proposal_formats.feature | 38 +++++++++++++++++++ .../features/limits_snapshot_filters.feature | 17 +++------ .../features/limits_snapshot_formats.feature | 37 +----------------- .../step_defs/limits_snapshot_caching.py | 2 +- .../step_defs/limits_snapshot_filters.py | 10 ----- 7 files changed, 47 insertions(+), 69 deletions(-) create mode 100644 test/forecasting/features/limits_proposal_formats.feature diff --git a/.gitignore b/.gitignore index bad14c9..625091a 100644 --- a/.gitignore +++ b/.gitignore @@ -175,15 +175,9 @@ cython_debug/ .vscode/ -# Don't want to push my conformed pynb -Forecasting_Conformance_Profile.ipynb - .github/gpg.exe pytest.ini -# lep changes don't push -LEP_Changes_Needed.txt - # Allure files allure-results/ assets/ diff --git a/test/conftest.py b/test/conftest.py index 8d25514..0e34b66 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -9,11 +9,7 @@ # we need to help pytest-bdd find the step definitions. The simplest way to do this is # get them loaded via the pytest_plugins. # https://gist.github.com/peterhurford/09f7dcda0ab04b95c026c60fa49c2a68?permalink_comment_id=3453153#gistcomment-3453153 -# pytest_plugins = [ -# fixture.replace("/", ".").replace(".py", "") -# for fixture in glob("test/**/step_defs/*.py") -# if "__" not in fixture -# ] + ["test.common.common_steps"] + pytest_plugins = [ fixture.replace("\\", ".").replace("/", ".").replace(".py", "") for fixture in glob("test/**/step_defs/*.py", recursive=True) diff --git a/test/forecasting/features/limits_proposal_formats.feature b/test/forecasting/features/limits_proposal_formats.feature new file mode 100644 index 0000000..1ba3448 --- /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 in 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 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_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 a3df942..02d72a9 100644 --- a/test/forecasting/features/limits_snapshot_filters.feature +++ b/test/forecasting/features/limits_snapshot_filters.feature @@ -23,8 +23,6 @@ Feature: Support querying subsets of the available forecasted limits | server_time | request_offset_time | response_first_period | | 15:00:00-05:00 | 17:00:00-05:00 | 17:00:00-05:00 | - # | 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 | @todo Scenario: What to do when `offset-period-start` is in the past? @@ -38,14 +36,13 @@ Feature: Support querying subsets of the available forecasted limits Examples: | server_time | request_last_period | response_last_period | | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | - # | 06:00:00-05:00 | 09:00:00-06:00 | 10: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 | - | TO1 | + | default | @requires_model Scenario Outline: Query forecast limits with resource-id filter @@ -69,8 +66,7 @@ Feature: Support querying subsets of the available forecasted limits Examples: | server_time | period_requested | request_offset_time | response_first_period | - # | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | - # | 06:00:00-05:00 | 2023-07-12T16:00:00-07:00 | 09:00:00-06:00 | 10:00:00-05:00 | + | 18:00:00-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | Scenario Outline: Query historical limits forecast snapshots with period-end @@ -114,7 +110,7 @@ Feature: Support querying subsets of the available forecasted limits Examples: | server_time | request_offset_time | response_first_period | | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | - # | 05:00:00-06:00 | 07:00:00-05:00 | 06:00:00-06:00 | + Scenario Outline: Query regional limits forecast snapshots with period-end Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., @@ -123,9 +119,8 @@ Feature: Support querying subsets of the available forecasted limits Examples: | server_time | request_last_period | response_last_period | - | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | - # | 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 | + | 18:00:00-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + Scenario Outline: Query regional limits forecast snapshots with monitoring-set When the client requests regional forecast limits with monitoring-set filter diff --git a/test/forecasting/features/limits_snapshot_formats.feature b/test/forecasting/features/limits_snapshot_formats.feature index f0c931b..7e2c471 100644 --- a/test/forecasting/features/limits_snapshot_formats.feature +++ b/test/forecasting/features/limits_snapshot_formats.feature @@ -102,7 +102,6 @@ Feature: Provide forecast limits in appropriate formats #| 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 | - # sends 200 OK , false 200 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 @@ -134,7 +133,7 @@ Feature: Provide forecast limits in appropriate formats #| application/vnd.trolie.forecast-limits-snapshot-slim.v1+json; inputs-used=true; limit-type=apparent-power | # GET Historical Limits Forecast Snapshot - @forecast_snapshot @todo + @todo Scenario Outline: Get historical limits forecast snapshot Given the Accept header is set to `` When the client requests a Historical Forecast Limits Snapshot @@ -147,7 +146,6 @@ Feature: Provide forecast limits in appropriate formats | application/vnd.trolie.forecast-limits-snapshot.v1+json | 2025-07-12T03:00:00-05:00 | # GET Regional Limits Forecast Snapshot - @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 @@ -158,7 +156,6 @@ Feature: Provide forecast limits in appropriate formats Examples: | content_type | | application/vnd.trolie.forecast-limits-snapshot.v1+json | - # include-psr-header=false throws error | 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 | @@ -166,7 +163,6 @@ Feature: Provide forecast limits in appropriate formats # | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json | # | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json; include-psr-header=false | - @forecast_snapshot 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 @@ -185,7 +181,6 @@ Feature: Provide forecast limits in appropriate formats # POST Update Regional Limits Forecast Snapshot - @forecast_snapshot Scenario Outline: Update Regional Limits Forecast Snapshot Given the Content-type header is set to `` And the body is loaded from `` @@ -202,33 +197,3 @@ Feature: Provide forecast limits in appropriate formats #| 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 | - # GET Obtain Forecast Proposal Status - @forecast_proposal - 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 in 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 - @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 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_proposal.json | application/vnd.trolie.rating-forecast-proposal-status.v1+json | - # Does not work - # | application/vnd.trolie.rating-forecast-proposal-slim.v1+json; limit-type=apparent-power | data/forecast_proposal_slim.json | - diff --git a/test/forecasting/step_defs/limits_snapshot_caching.py b/test/forecasting/step_defs/limits_snapshot_caching.py index 79fc809..2a76d93 100644 --- a/test/forecasting/step_defs/limits_snapshot_caching.py +++ b/test/forecasting/step_defs/limits_snapshot_caching.py @@ -8,7 +8,7 @@ @given("the client has obtained the current Forecast Limits Snapshot with an ETag", target_fixture="etag") def get_etag_for_forecast_limits_snapshot(client): - # client.set_server_time(base_time) + client.set_server_time(base_time) return get_etag(get_forecast_limits_snapshot(client)) diff --git a/test/forecasting/step_defs/limits_snapshot_filters.py b/test/forecasting/step_defs/limits_snapshot_filters.py index 62a8d7c..5ecd0f2 100644 --- a/test/forecasting/step_defs/limits_snapshot_filters.py +++ b/test/forecasting/step_defs/limits_snapshot_filters.py @@ -147,15 +147,5 @@ def forecast_snapshot_contains_requested_resource(resource_id, client: TrolieCli 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}" -#todo -@then(parsers.parse("see output {response_first_period}")) -def forecase_snapshot_request_past_period(response_first_period, client: TrolieClient): - expected_start = get_todays_iso8601_for(response_first_period) - print("Expected value: ", expected_start) - limits = client.get_json()["limits"] - print(limits) - targets = ((entry["resource-id"], entry["periods"][0]["period-start"]) for entry in limits) - for resource_id, period_start in targets: - assert expected_start == period_start, f"Failed for resource {resource_id}" From e09ec1406aaa7fefeb1860b480561740ec1bae26 Mon Sep 17 00:00:00 2001 From: Alexander Yen Date: Fri, 25 Jul 2025 14:16:10 -0700 Subject: [PATCH 3/6] Minor fix preventing tests from running --- test/forecasting/features/limits_snapshot_filters.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/forecasting/features/limits_snapshot_filters.feature b/test/forecasting/features/limits_snapshot_filters.feature index 02d72a9..b90ceda 100644 --- a/test/forecasting/features/limits_snapshot_filters.feature +++ b/test/forecasting/features/limits_snapshot_filters.feature @@ -65,7 +65,7 @@ Feature: Support querying subsets of the available forecasted limits Then the response should only include forecast limits starting at the `offset-period-start` in the server's time zone, i.e., Examples: - | server_time | period_requested | request_offset_time | response_first_period | + | server_time | request_offset_time | response_first_period | | 18:00:00-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | From 52b99c6dcf2656a93a1e02f08e7f52077c9a219c Mon Sep 17 00:00:00 2001 From: Alexander Yen Date: Wed, 23 Jul 2025 12:39:28 -0700 Subject: [PATCH 4/6] Added multiple changes features in forecasting Rebasing, resolved conflict in gitignore and conftest.py Rebasing, resolved conflict in gitignore and conftest. --- .gitignore | 1 - test/common/common_steps.py | 15 ++- test/conftest.py | 34 ++++++ .../features/limit_snapshot_caching.feature | 2 +- .../features/limits_snapshot_filters.feature | 111 ++++++++++++++++-- .../features/limits_snapshot_formats.feature | 109 ++++++++++++++++- .../features/require_authentication.feature | 2 +- test/forecasting/forecast_helpers.py | 11 +- .../step_defs/limits_snapshot_caching.py | 2 +- .../step_defs/limits_snapshot_filters.py | 73 +++++++++++- .../step_defs/limits_snapshot_formats.py | 22 +++- 11 files changed, 360 insertions(+), 22 deletions(-) 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..86ee5c4 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,9 @@ 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 `{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 +34,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 +73,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): diff --git a/test/conftest.py b/test/conftest.py index 4c4b630..6773b2c 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -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_snapshot_filters.feature b/test/forecasting/features/limits_snapshot_filters.feature index 5dc3435..a3df942 100644 --- a/test/forecasting/features/limits_snapshot_filters.feature +++ b/test/forecasting/features/limits_snapshot_filters.feature @@ -11,7 +11,9 @@ 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 @@ -19,29 +21,33 @@ Feature: Support querying subsets of the available forecasted limits 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 | + + | 15:00:00-05:00 | 17:00:00-05:00 | 17:00:00-05:00 | + # | 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 | @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 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 | + # | 06:00:00-05:00 | 09:00:00-06:00 | 10: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 | + | TO1 | - @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 +55,96 @@ 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 is today at 11am GMT, i.e., + # And the period requested is set to + When the client requests historical 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., + + Examples: + | server_time | period_requested | request_offset_time | response_first_period | + # | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + # | 06:00:00-05:00 | 2023-07-12T16:00:00-07:00 | 09:00:00-06:00 | 10:00:00-05:00 | + + + Scenario Outline: Query historical limits forecast snapshots with period-end + Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., + When the client requests historical 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 | 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 | + + 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 + Scenario Outline: Query regional limits forecast snapshots with offset-period-start + Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., + When the client requests regional 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., + + Examples: + | server_time | request_offset_time | response_first_period | + | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + # | 05:00:00-06:00 | 07:00:00-05:00 | 06:00:00-06:00 | + + Scenario Outline: Query regional limits forecast snapshots with period-end + Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., + When the client requests regional 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 | + | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + # | 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 | + + 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..f0c931b 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,8 @@ 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 | - + + # sends 200 OK , false 200 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 +132,103 @@ 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 + @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 + @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 | + # include-psr-header=false throws error + | 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 | + + @forecast_snapshot + 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 + @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 | + + + # GET Obtain Forecast Proposal Status + @forecast_proposal + 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 in 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 + @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 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_proposal.json | application/vnd.trolie.rating-forecast-proposal-status.v1+json | + # Does not work + # | application/vnd.trolie.rating-forecast-proposal-slim.v1+json; limit-type=apparent-power | data/forecast_proposal_slim.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..a6247c6 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 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}") @@ -17,6 +23,9 @@ def get_todays_iso8601_for(time_with_timezone: str) -> str: 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_caching.py b/test/forecasting/step_defs/limits_snapshot_caching.py index 2a76d93..79fc809 100644 --- a/test/forecasting/step_defs/limits_snapshot_caching.py +++ b/test/forecasting/step_defs/limits_snapshot_caching.py @@ -8,7 +8,7 @@ @given("the client has obtained the current Forecast Limits Snapshot with an ETag", target_fixture="etag") def get_etag_for_forecast_limits_snapshot(client): - client.set_server_time(base_time) + # client.set_server_time(base_time) return get_etag(get_forecast_limits_snapshot(client)) diff --git a/test/forecasting/step_defs/limits_snapshot_filters.py b/test/forecasting/step_defs/limits_snapshot_filters.py index c8a29a8..62a8d7c 100644 --- a/test/forecasting/step_defs/limits_snapshot_filters.py +++ b/test/forecasting/step_defs/limits_snapshot_filters.py @@ -1,8 +1,12 @@ +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, ) @@ -14,6 +18,19 @@ def clearinghouse_wall_clock_today_at_11amGMT(server_time, client: TrolieClient) get_todays_iso8601_for(server_time), ) +@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) + +@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): @@ -26,32 +43,71 @@ def forecast_snapshot_request_filter_last_period(request_last_period, client: Tr client.set_query_param("period-end", get_todays_iso8601_for(request_last_period)) get_forecast_limits_snapshot(client) +@when(parsers.parse("the client requests historical forecast limits with period-end {request_last_period}")) +def historical_forecast_snapshot_request_filter_last_period(request_last_period, client: TrolieClient): + client.set_query_param("period-end", get_todays_iso8601_for(request_last_period)) + get_historical_limits_forecast_snapshot(client) + +@when(parsers.parse("the client requests regional forecast limits with period-end {request_last_period}")) +def regional_forecast_snapshot_request_filter_last_period(request_last_period, client: TrolieClient): + client.set_query_param("period-end", get_todays_iso8601_for(request_last_period)) + 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) +@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 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) + # print(client.get_json()) limits = client.get_json()["limits"] targets = ((entry["resource-id"], entry["periods"][0]["period-start"]) for entry in limits) for resource_id, period_start in targets: - assert expected_start == period_start, f"Failed for resource {resource_id}" + assert parser.isoparse(expected_start) == parser.isoparse(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")) @@ -60,7 +116,7 @@ def forecast_snapshot_request_last_period_includes(response_last_period, client: limits = client.get_json()["limits"] targets = ((limits_entry["resource-id"], limits_entry["periods"][-1]["period-end"]) for limits_entry in limits) for resource_id, period_end in targets: - assert expected_end == period_end, f"Failed for resource {resource_id}" + assert parser.isoparse(expected_end) == parser.isoparse(period_end), f"Failed for resource {resource_id}" @then(parsers.parse("the response should include only static forecast limits")) @@ -90,3 +146,16 @@ 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}" + +#todo +@then(parsers.parse("see output {response_first_period}")) +def forecase_snapshot_request_past_period(response_first_period, client: TrolieClient): + expected_start = get_todays_iso8601_for(response_first_period) + print("Expected value: ", expected_start) + limits = client.get_json()["limits"] + print(limits) + targets = ((entry["resource-id"], entry["periods"][0]["period-start"]) for entry in limits) + for resource_id, period_start in targets: + assert expected_start == period_start, 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}") + From e4e661855849cad1f60ffe162a8142a7f2fc0176 Mon Sep 17 00:00:00 2001 From: Alexander Yen Date: Thu, 24 Jul 2025 11:24:18 -0700 Subject: [PATCH 5/6] Reviewed PR comments and made appropriate changes Resolved 2nd conflic in gitignore and conftest.py for rebase --- .../features/limits_proposal_formats.feature | 38 +++++++++++++++++++ .../features/limits_snapshot_filters.feature | 17 +++------ .../features/limits_snapshot_formats.feature | 37 +----------------- .../step_defs/limits_snapshot_caching.py | 2 +- .../step_defs/limits_snapshot_filters.py | 10 ----- 5 files changed, 46 insertions(+), 58 deletions(-) create mode 100644 test/forecasting/features/limits_proposal_formats.feature diff --git a/test/forecasting/features/limits_proposal_formats.feature b/test/forecasting/features/limits_proposal_formats.feature new file mode 100644 index 0000000..1ba3448 --- /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 in 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 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_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 a3df942..02d72a9 100644 --- a/test/forecasting/features/limits_snapshot_filters.feature +++ b/test/forecasting/features/limits_snapshot_filters.feature @@ -23,8 +23,6 @@ Feature: Support querying subsets of the available forecasted limits | server_time | request_offset_time | response_first_period | | 15:00:00-05:00 | 17:00:00-05:00 | 17:00:00-05:00 | - # | 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 | @todo Scenario: What to do when `offset-period-start` is in the past? @@ -38,14 +36,13 @@ Feature: Support querying subsets of the available forecasted limits Examples: | server_time | request_last_period | response_last_period | | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | - # | 06:00:00-05:00 | 09:00:00-06:00 | 10: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 | - | TO1 | + | default | @requires_model Scenario Outline: Query forecast limits with resource-id filter @@ -69,8 +66,7 @@ Feature: Support querying subsets of the available forecasted limits Examples: | server_time | period_requested | request_offset_time | response_first_period | - # | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | - # | 06:00:00-05:00 | 2023-07-12T16:00:00-07:00 | 09:00:00-06:00 | 10:00:00-05:00 | + | 18:00:00-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | Scenario Outline: Query historical limits forecast snapshots with period-end @@ -114,7 +110,7 @@ Feature: Support querying subsets of the available forecasted limits Examples: | server_time | request_offset_time | response_first_period | | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | - # | 05:00:00-06:00 | 07:00:00-05:00 | 06:00:00-06:00 | + Scenario Outline: Query regional limits forecast snapshots with period-end Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., @@ -123,9 +119,8 @@ Feature: Support querying subsets of the available forecasted limits Examples: | server_time | request_last_period | response_last_period | - | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | - # | 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 | + | 18:00:00-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + Scenario Outline: Query regional limits forecast snapshots with monitoring-set When the client requests regional forecast limits with monitoring-set filter diff --git a/test/forecasting/features/limits_snapshot_formats.feature b/test/forecasting/features/limits_snapshot_formats.feature index f0c931b..7e2c471 100644 --- a/test/forecasting/features/limits_snapshot_formats.feature +++ b/test/forecasting/features/limits_snapshot_formats.feature @@ -102,7 +102,6 @@ Feature: Provide forecast limits in appropriate formats #| 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 | - # sends 200 OK , false 200 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 @@ -134,7 +133,7 @@ Feature: Provide forecast limits in appropriate formats #| application/vnd.trolie.forecast-limits-snapshot-slim.v1+json; inputs-used=true; limit-type=apparent-power | # GET Historical Limits Forecast Snapshot - @forecast_snapshot @todo + @todo Scenario Outline: Get historical limits forecast snapshot Given the Accept header is set to `` When the client requests a Historical Forecast Limits Snapshot @@ -147,7 +146,6 @@ Feature: Provide forecast limits in appropriate formats | application/vnd.trolie.forecast-limits-snapshot.v1+json | 2025-07-12T03:00:00-05:00 | # GET Regional Limits Forecast Snapshot - @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 @@ -158,7 +156,6 @@ Feature: Provide forecast limits in appropriate formats Examples: | content_type | | application/vnd.trolie.forecast-limits-snapshot.v1+json | - # include-psr-header=false throws error | 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 | @@ -166,7 +163,6 @@ Feature: Provide forecast limits in appropriate formats # | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json | # | application/vnd.trolie.forecast-limits-detailed-snapshot.v1+json; include-psr-header=false | - @forecast_snapshot 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 @@ -185,7 +181,6 @@ Feature: Provide forecast limits in appropriate formats # POST Update Regional Limits Forecast Snapshot - @forecast_snapshot Scenario Outline: Update Regional Limits Forecast Snapshot Given the Content-type header is set to `` And the body is loaded from `` @@ -202,33 +197,3 @@ Feature: Provide forecast limits in appropriate formats #| 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 | - # GET Obtain Forecast Proposal Status - @forecast_proposal - 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 in 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 - @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 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_proposal.json | application/vnd.trolie.rating-forecast-proposal-status.v1+json | - # Does not work - # | application/vnd.trolie.rating-forecast-proposal-slim.v1+json; limit-type=apparent-power | data/forecast_proposal_slim.json | - diff --git a/test/forecasting/step_defs/limits_snapshot_caching.py b/test/forecasting/step_defs/limits_snapshot_caching.py index 79fc809..2a76d93 100644 --- a/test/forecasting/step_defs/limits_snapshot_caching.py +++ b/test/forecasting/step_defs/limits_snapshot_caching.py @@ -8,7 +8,7 @@ @given("the client has obtained the current Forecast Limits Snapshot with an ETag", target_fixture="etag") def get_etag_for_forecast_limits_snapshot(client): - # client.set_server_time(base_time) + client.set_server_time(base_time) return get_etag(get_forecast_limits_snapshot(client)) diff --git a/test/forecasting/step_defs/limits_snapshot_filters.py b/test/forecasting/step_defs/limits_snapshot_filters.py index 62a8d7c..5ecd0f2 100644 --- a/test/forecasting/step_defs/limits_snapshot_filters.py +++ b/test/forecasting/step_defs/limits_snapshot_filters.py @@ -147,15 +147,5 @@ def forecast_snapshot_contains_requested_resource(resource_id, client: TrolieCli 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}" -#todo -@then(parsers.parse("see output {response_first_period}")) -def forecase_snapshot_request_past_period(response_first_period, client: TrolieClient): - expected_start = get_todays_iso8601_for(response_first_period) - print("Expected value: ", expected_start) - limits = client.get_json()["limits"] - print(limits) - targets = ((entry["resource-id"], entry["periods"][0]["period-start"]) for entry in limits) - for resource_id, period_start in targets: - assert expected_start == period_start, f"Failed for resource {resource_id}" From 3e79fd1cbfa7a94cd31dbcca0c29b7e57b342d1f Mon Sep 17 00:00:00 2001 From: Alexander Yen Date: Thu, 31 Jul 2025 11:36:38 -0700 Subject: [PATCH 6/6] Changed how snapshot_filters was implemented for offset_start and period_end. --- test/common/common_steps.py | 13 ++- .../features/limits_proposal_formats.feature | 11 ++- .../features/limits_snapshot_filters.feature | 82 ++++++++------- test/forecasting/forecast_helpers.py | 6 +- .../step_defs/limits_snapshot_filters.py | 99 +++++++++++-------- test/helpers.py | 3 + 6 files changed, 129 insertions(+), 85 deletions(-) diff --git a/test/common/common_steps.py b/test/common/common_steps.py index 86ee5c4..4178a55 100644 --- a/test/common/common_steps.py +++ b/test/common/common_steps.py @@ -20,9 +20,9 @@ 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 `{content_type}`")) -def set_content_header(content_type, client): - client.set_header(Header.ContentType, content_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("the client has bad query parameters") def bad_query_parameters(client: TrolieClient): @@ -40,6 +40,11 @@ def set_body_from_file(client: TrolieClient, filename): body = json.load(f) client.set_body(body) +@given(parsers.parse("the request body is a valid {request_type}")) +def validate_request_body(client: TrolieClient, request_type): + # todo + return + @then("the response is 202 OK") def request_forecast_limits_snapshot(client: TrolieClient): assert client.get_status_code() == 202 @@ -86,6 +91,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/forecasting/features/limits_proposal_formats.feature b/test/forecasting/features/limits_proposal_formats.feature index 1ba3448..50c5694 100644 --- a/test/forecasting/features/limits_proposal_formats.feature +++ b/test/forecasting/features/limits_proposal_formats.feature @@ -15,7 +15,7 @@ Feature: Provide forecast proposal limits in appropriate formats 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 in the response is `` + And the Content-Type header of the response is `` And the response is schema-valid @@ -25,14 +25,15 @@ Feature: Provide forecast proposal limits in appropriate formats # 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 `` + Given the Content-type header is set to `` + And the body is loaded from `` + And the request body is a valid When the client submits a Forecast Proposal Then the response is 202 OK - And the Content-Type header in the response is `` + And the Content-Type header of the response is `` And the response is schema-valid Examples: - | content_type | file_name | response_type | + | 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 02d72a9..072d803 100644 --- a/test/forecasting/features/limits_snapshot_filters.feature +++ b/test/forecasting/features/limits_snapshot_filters.feature @@ -15,14 +15,17 @@ Feature: Support querying subsets of the available forecasted limits # 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 | - - | 15:00:00-05:00 | 17:00:00-05:00 | 17:00:00-05:00 | + | offset_hours | + | 1 | + | 5 | + | 7 | + @todo Scenario: What to do when `offset-period-start` is in the past? @@ -30,12 +33,15 @@ Feature: Support querying subsets of the available forecasted limits @prism_fail Scenario Outline: Query forecast limits with period-end - 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 + 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: - | server_time | request_last_period | response_last_period | - | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + | offset_hours | + | 1 | + | 5 | + | 7 | Scenario Outline: Query forecast limits with monitoring-set filter When the client requests forecast limits with monitoring-set filter @@ -59,25 +65,27 @@ Feature: Support querying subsets of the available forecasted 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 is today at 11am GMT, i.e., - # And the period requested is set to - When the client requests historical 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 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: - | server_time | period_requested | request_offset_time | response_first_period | - | 18:00:00-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + | offset_hours | + | 1 | + | 5 | + | 7 | Scenario Outline: Query historical limits forecast snapshots with period-end - Given the current wall clock time at the Clearinghouse is today at 11am GMT, i.e., - When the client requests historical forecast limits with period-end - Then the response should include forecast limits up to in the server's time zone + 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: - | server_time | request_last_period | response_last_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 | Scenario Outline: Query historical limits forecast snapshots with monitoring-set When the client requests historical forecast limits with monitoring-set filter @@ -102,24 +110,28 @@ Feature: Support querying subsets of the available forecasted limits 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 is today at 11am GMT, i.e., - When the client requests regional 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 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: - | server_time | request_offset_time | response_first_period | - | 18:35:45-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | - - + | 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 is today at 11am GMT, i.e., - When the client requests regional forecast limits with period-end - Then the response should include forecast limits up to in the server's time zone + 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: - | server_time | request_last_period | response_last_period | - | 18:00:00-05:00 | 14:00:00-05:00 | 14:00:00-05:00 | + | offset_hours | + | 1 | + | 5 | + | 7 | Scenario Outline: Query regional limits forecast snapshots with monitoring-set diff --git a/test/forecasting/forecast_helpers.py b/test/forecasting/forecast_helpers.py index a6247c6..ba80c27 100644 --- a/test/forecasting/forecast_helpers.py +++ b/test/forecasting/forecast_helpers.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from test.helpers import TrolieClient, get_period @@ -20,6 +20,10 @@ 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") diff --git a/test/forecasting/step_defs/limits_snapshot_filters.py b/test/forecasting/step_defs/limits_snapshot_filters.py index 5ecd0f2..c8af019 100644 --- a/test/forecasting/step_defs/limits_snapshot_filters.py +++ b/test/forecasting/step_defs/limits_snapshot_filters.py @@ -8,51 +8,61 @@ 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 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), - ) +@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}") - -@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) -@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)) - get_forecast_limits_snapshot(client) +@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 period-end {request_last_period}")) -def historical_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 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 period-end {request_last_period}")) -def regional_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 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): @@ -100,23 +110,32 @@ def regional_forecast_snapshot_request_filter_resource_id(resource_id, client: T client.set_query_param("resource-id", resource_id) get_regional_limits_forecast_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) - # print(client.get_json()) + + +@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 parser.isoparse(expected_start) == parser.isoparse(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 parser.isoparse(expected_end) == parser.isoparse(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")) 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