From 5a4d0d39142592ed0bfe4ead7150e0a75678ef1e Mon Sep 17 00:00:00 2001 From: ZB-io <58132646+chaitra1403@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:31:25 +0000 Subject: [PATCH] Add API Tests (Pytest Framework, Claude AI) generated by RoostGPT Using AI Model claude-opus-4-5-20251101 --- Roost-README.md | 25 + requirements-roost.txt | 62 + tests/WEATHER_API/api.json | 1747 ++++++++++++++++++ tests/WEATHER_API/astronomy_json.json | 72 + tests/WEATHER_API/config.yml | 19 + tests/WEATHER_API/conftest.py | 162 ++ tests/WEATHER_API/current_json.json | 71 + tests/WEATHER_API/forecast_json.json | 101 + tests/WEATHER_API/future_json.json | 74 + tests/WEATHER_API/history_json.json | 87 + tests/WEATHER_API/ip_json.json | 52 + tests/WEATHER_API/marine_json.json | 98 + tests/WEATHER_API/search_json.json | 52 + tests/WEATHER_API/test_astronomy_json_get.py | 417 +++++ tests/WEATHER_API/test_current_json_get.py | 510 +++++ tests/WEATHER_API/test_forecast_json_get.py | 458 +++++ tests/WEATHER_API/test_future_json_get.py | 642 +++++++ tests/WEATHER_API/test_history_json_get.py | 742 ++++++++ tests/WEATHER_API/test_ip_json_get.py | 333 ++++ tests/WEATHER_API/test_marine_json_get.py | 487 +++++ tests/WEATHER_API/test_search_json_get.py | 387 ++++ tests/WEATHER_API/test_timezone_json_get.py | 410 ++++ tests/WEATHER_API/timezone_json.json | 52 + tests/WEATHER_API/validator.py | 202 ++ 24 files changed, 7262 insertions(+) create mode 100644 Roost-README.md create mode 100644 requirements-roost.txt create mode 100644 tests/WEATHER_API/api.json create mode 100644 tests/WEATHER_API/astronomy_json.json create mode 100644 tests/WEATHER_API/config.yml create mode 100644 tests/WEATHER_API/conftest.py create mode 100644 tests/WEATHER_API/current_json.json create mode 100644 tests/WEATHER_API/forecast_json.json create mode 100644 tests/WEATHER_API/future_json.json create mode 100644 tests/WEATHER_API/history_json.json create mode 100644 tests/WEATHER_API/ip_json.json create mode 100644 tests/WEATHER_API/marine_json.json create mode 100644 tests/WEATHER_API/search_json.json create mode 100644 tests/WEATHER_API/test_astronomy_json_get.py create mode 100644 tests/WEATHER_API/test_current_json_get.py create mode 100644 tests/WEATHER_API/test_forecast_json_get.py create mode 100644 tests/WEATHER_API/test_future_json_get.py create mode 100644 tests/WEATHER_API/test_history_json_get.py create mode 100644 tests/WEATHER_API/test_ip_json_get.py create mode 100644 tests/WEATHER_API/test_marine_json_get.py create mode 100644 tests/WEATHER_API/test_search_json_get.py create mode 100644 tests/WEATHER_API/test_timezone_json_get.py create mode 100644 tests/WEATHER_API/timezone_json.json create mode 100644 tests/WEATHER_API/validator.py diff --git a/Roost-README.md b/Roost-README.md new file mode 100644 index 000000000..dd5bcc8d8 --- /dev/null +++ b/Roost-README.md @@ -0,0 +1,25 @@ + +# RoostGPT generated pytest code for API Testing + +RoostGPT generats code in `tests` folder within given project path. +Dependency file i.e. `requirements-roost.txt` is also created in the given project path + +Below are the sample steps to run the generated tests. Sample commands contains use of package manager i.e. `uv`. Alternatively python and pip can be used directly. +1. ( Optional ) Create virtual Env . +2. Install dependencies +``` +uv venv // Create virtual Env +uv pip install -r requirements-roost.txt // Install all dependencies + +``` + +Test configurations and test_data is loaded from config.yml. e.g. API HOST, auth, common path parameters of endpoint. +Either set defalt value in this config.yml file OR use ENV. e.g. export API_HOST="https://example.com/api/v2" + +Once configuration values are set, use below commands to run the tests. +``` +// Run generated tests +uv run pytest -m smoke // Run only smoke tests +uv run pytest -s tests/generated-test.py // Run specific test file +``` + \ No newline at end of file diff --git a/requirements-roost.txt b/requirements-roost.txt new file mode 100644 index 000000000..de7bd70f6 --- /dev/null +++ b/requirements-roost.txt @@ -0,0 +1,62 @@ + +agentocr +beautifulsoup4 +boto3 +botocore +chromedriver_binary +click +django_recaptcha +dlib +dnspython +emoji +exifread +ffmpeg +ffpyplayer +Flask +flask_sqlalchemy +geopy +googletrans +gtts +img2pdf +jsonschema +keras +lxml +matplotlib +nltk +numpy +pandas +pil +Pillow +psutil +pyautogui +pycryptodome +pycryptodomex +PyDictionary +pygame +pymysql +pynotifier +PyPDF2 +pytest +python-telegram-bot +pyttsx3 +pywhatkit +PyYAML +qrcode +referencing +Requests +rich +scikit_learn +selenium +sumeval +sumy +tensorflow +textblob +tqdm +tweepy +urllib3 +webdriver_manager +wechaty +wechaty_puppet +wikipedia +wordcloud +xmltodict \ No newline at end of file diff --git a/tests/WEATHER_API/api.json b/tests/WEATHER_API/api.json new file mode 100644 index 000000000..450621564 --- /dev/null +++ b/tests/WEATHER_API/api.json @@ -0,0 +1,1747 @@ +{ + "swagger": "2.0", + "info": { + "title": "Weather API", + "version": "1.0.2", + "description": "# Introduction\nWeatherAPI.com provides access to weather and geo data via a JSON/XML restful API. It allows developers to create desktop, web and mobile applications using this data very easy. We provide following data through our API: \n- Real-time weather\n- 14 day weather forecast\n- Historical Weather\n- Marine Weather and Tide Data\n- Future Weather (Upto 365 days ahead)\n- Daily and hourly intervals\n- 15 min interval (Enterprise only)\n- Astronomy\n- Time zone\n- Location data\n- Sports\n- Search or Autocomplete API\n- Weather Alerts\n- Air Quality Data\n- Bulk Request\n\n# Getting Started \n\nYou need to [signup](https://www.weatherapi.com/signup.aspx) and then you can find your API key under [your account](https://www.weatherapi.com/login.aspx), and start using API right away!\n\nTry our weather API by using interactive [API Explorer](https://www.weatherapi.com/api-explorer.aspx).\n\nWe also have SDK for popular framework/languages available on [Github](https://github.com/weatherapicom/) for quick integrations.\n\nIf you find any features missing or have any suggestions, please [contact us](https://www.weatherapi.com/contact.aspx). \n\n# Authentication \n\nAPI access to the data is protected by an API key. If at anytime, you find the API key has become vulnerable, please regenerate the key using Regenerate button next to the API key. \n\nAuthentication to the WeatherAPI.com API is provided by passing your API key as request parameter through an API . \n\n## key parameter \nkey=YOUR API KEY \n" + }, + "host": "api.weatherapi.com", + "basePath": "/v1", + "schemes": [ + "https" + ], + "produces": [ + "application/json", + "application/xml" + ], + "responses": { + "400": { + "description": "Error code 1003: Parameter 'q' not provided.
Error code 1005: API request url is invalid.
Error code 1006: No location found matching parameter 'q'
Error code 9000: Json body passed in bulk request is invalid. Please make sure it is valid json with utf-8 encoding.
Error code 9001: Json body contains too many locations for bulk request. Please keep it below 50 in a single request.
Error code 9999: Internal application error.", + "schema": { + "$ref": "#/definitions/error400" + } + }, + "401": { + "description": "Error code 1002: API key not provided.
Error code 2006: API key provided is invalid", + "schema": { + "$ref": "#/definitions/error401" + } + }, + "403": { + "description": "Error code 2007: API key has exceeded calls per month quota.
Error code 2008: API key has been disabled.
Error code 2009: API key does not have access to the resource. Please check pricing page for what is allowed in your API subscription plan.", + "schema": { + "$ref": "#/definitions/error403" + } + } + }, + "x-explorer-enabled": true, + "tags": [ + { + "name": "APIs", + "description": "APIs" + } + ], + "paths": { + "/current.json": { + "get": { + "tags": [ + "APIs" + ], + "summary": "Realtime API", + "description": "Current weather or realtime weather API method allows a user to get up to date current weather information in json and xml. The data is returned as a Current Object.

Current object contains current or realtime weather information for a given city.", + "operationId": "realtime-weather", + "parameters": [ + { + "in": "query", + "name": "q", + "type": "string", + "required": true, + "description": "Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to learn more." + }, + { + "in": "query", + "name": "lang", + "type": "string", + "required": false, + "description": "Returns 'condition:text' field in API in the desired language.
Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to check 'lang-code'." + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "properties": { + "location": { + "$ref": "#/definitions/location" + }, + "current": { + "$ref": "#/definitions/current" + } + } + } + }, + "400": { + "$ref": "#/responses/400" + }, + "401": { + "$ref": "#/responses/401" + }, + "403": { + "$ref": "#/responses/403" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/forecast.json": { + "get": { + "tags": [ + "APIs" + ], + "summary": "Forecast API", + "description": "Forecast weather API method returns, depending upon your price plan level, upto next 14 day weather forecast and weather alert as json or xml. The data is returned as a Forecast Object.

Forecast object contains astronomy data, day weather forecast and hourly interval weather information for a given city.", + "operationId": "forecast-weather", + "parameters": [ + { + "in": "query", + "name": "q", + "type": "string", + "required": true, + "description": "Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to learn more." + }, + { + "in": "query", + "name": "days", + "type": "integer", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 + ], + "required": true, + "description": "Number of days of weather forecast. Value ranges from 1 to 14" + }, + { + "in": "query", + "name": "dt", + "type": "string", + "format": "date", + "required": false, + "description": "Date should be between today and next 14 day in yyyy-MM-dd format. e.g. '2015-01-01'" + }, + { + "in": "query", + "name": "unixdt", + "type": "integer", + "required": false, + "description": "Please either pass 'dt' or 'unixdt' and not both in same request. unixdt should be between today and next 14 day in Unix format. e.g. 1490227200" + }, + { + "in": "query", + "name": "hour", + "type": "integer", + "required": false, + "description": "Must be in 24 hour. For example 5 pm should be hour=17, 6 am as hour=6" + }, + { + "in": "query", + "name": "lang", + "type": "string", + "required": false, + "description": "Returns 'condition:text' field in API in the desired language.
Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to check 'lang-code'." + }, + { + "in": "query", + "name": "alerts", + "type": "string", + "required": false, + "description": "Enable/Disable alerts in forecast API output. Example, alerts=yes or alerts=no." + }, + { + "in": "query", + "name": "aqi", + "type": "string", + "required": false, + "description": "Enable/Disable Air Quality data in forecast API output. Example, aqi=yes or aqi=no." + }, + { + "in": "query", + "name": "tp", + "type": "integer", + "required": false, + "description": "Get 15 min interval or 24 hour average data for Forecast and History API. Available for Enterprise clients only. E.g:- tp=15" + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "properties": { + "location": { + "$ref": "#/definitions/location" + }, + "current": { + "$ref": "#/definitions/current" + }, + "forecast": { + "$ref": "#/definitions/forecast" + }, + "alerts": { + "$ref": "#/definitions/alerts" + } + } + } + }, + "400": { + "$ref": "#/responses/400" + }, + "401": { + "$ref": "#/responses/401" + }, + "403": { + "$ref": "#/responses/403" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/future.json": { + "get": { + "tags": [ + "APIs" + ], + "summary": "Future API", + "description": "Future weather API method returns weather in a 3 hourly interval in future for a date between 14 days and 365 days from today in the future.", + "operationId": "future-weather", + "parameters": [ + { + "in": "query", + "name": "q", + "type": "string", + "required": true, + "description": "Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to learn more." + }, + { + "in": "query", + "name": "dt", + "type": "string", + "format": "date", + "required": false, + "description": "Date should be between 14 days and 300 days from today in the future in yyyy-MM-dd format (i.e. dt=2023-01-01)" + }, + { + "in": "query", + "name": "lang", + "type": "string", + "required": false, + "description": "Returns 'condition:text' field in API in the desired language.
Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to check 'lang-code'." + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "properties": { + "location": { + "$ref": "#/definitions/location" + }, + "forecast": { + "$ref": "#/definitions/forecast" + } + } + } + }, + "400": { + "$ref": "#/responses/400" + }, + "401": { + "$ref": "#/responses/401" + }, + "403": { + "$ref": "#/responses/403" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/history.json": { + "get": { + "tags": [ + "APIs" + ], + "summary": "History API", + "description": "History weather API method returns historical weather for a date on or after 1st Jan, 2010 as json. The data is returned as a Forecast Object.", + "operationId": "history-weather", + "parameters": [ + { + "in": "query", + "name": "q", + "type": "string", + "required": true, + "description": "Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to learn more." + }, + { + "in": "query", + "name": "dt", + "type": "string", + "format": "date", + "required": true, + "description": "Date on or after 1st Jan, 2015 in yyyy-MM-dd format" + }, + { + "in": "query", + "name": "unixdt", + "type": "integer", + "required": false, + "description": "Please either pass 'dt' or 'unixdt' and not both in same request.
unixdt should be on or after 1st Jan, 2015 in Unix format" + }, + { + "in": "query", + "name": "end_dt", + "type": "string", + "format": "date", + "required": false, + "description": "Date on or after 1st Jan, 2015 in yyyy-MM-dd format
'end_dt' should be greater than 'dt' parameter and difference should not be more than 30 days between the two dates." + }, + { + "in": "query", + "name": "unixend_dt", + "type": "integer", + "required": false, + "description": "Date on or after 1st Jan, 2015 in Unix Timestamp format
unixend_dt has same restriction as 'end_dt' parameter. Please either pass 'end_dt' or 'unixend_dt' and not both in same request. e.g. unixend_dt=1490227200" + }, + { + "in": "query", + "name": "hour", + "type": "integer", + "required": false, + "description": "Must be in 24 hour. For example 5 pm should be hour=17, 6 am as hour=6" + }, + { + "in": "query", + "name": "lang", + "type": "string", + "required": false, + "description": "Returns 'condition:text' field in API in the desired language.
Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to check 'lang-code'." + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "properties": { + "location": { + "$ref": "#/definitions/location" + }, + "forecast": { + "$ref": "#/definitions/forecast" + } + } + } + }, + "400": { + "$ref": "#/responses/400" + }, + "401": { + "$ref": "#/responses/401" + }, + "403": { + "$ref": "#/responses/403" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/marine.json": { + "get": { + "tags": [ + "APIs" + ], + "summary": "Marine Weather API", + "description": "Marine weather API method returns upto next 7 day (depending upon your price plan level) marine and sailing weather forecast and tide data (depending upon your price plan level) as json or xml. The data is returned as a Marine Object.

Marine object, depending upon your price plan level, contains astronomy data, day weather forecast and hourly interval weather information and tide data for a given sea/ocean point.", + "operationId": "marine-weather", + "parameters": [ + { + "in": "query", + "name": "q", + "type": "string", + "required": true, + "description": "Pass Latitude/Longitude (decimal degree) which is on a sea/ocean. Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to learn more." + }, + { + "in": "query", + "name": "days", + "type": "integer", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "required": true, + "description": "Number of days of weather forecast. Value ranges from 1 to 7" + }, + { + "in": "query", + "name": "dt", + "type": "string", + "format": "date", + "required": false, + "description": "Date should be between today and next 7 day in yyyy-MM-dd format. e.g. '2023-05-20'" + }, + { + "in": "query", + "name": "unixdt", + "type": "integer", + "required": false, + "description": "Please either pass 'dt' or 'unixdt' and not both in same request. unixdt should be between today and next 7 day in Unix format. e.g. 1490227200" + }, + { + "in": "query", + "name": "hour", + "type": "integer", + "required": false, + "description": "Must be in 24 hour. For example 5 pm should be hour=17, 6 am as hour=6" + }, + { + "in": "query", + "name": "lang", + "type": "string", + "required": false, + "description": "Returns 'condition:text' field in API in the desired language.
Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to check 'lang-code'." + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "properties": { + "location": { + "$ref": "#/definitions/location" + }, + "forecast": { + "$ref": "#/definitions/marine" + } + } + } + }, + "400": { + "$ref": "#/responses/400" + }, + "401": { + "$ref": "#/responses/401" + }, + "403": { + "$ref": "#/responses/403" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/search.json": { + "get": { + "tags": [ + "APIs" + ], + "summary": "Search/Autocomplete API", + "description": "WeatherAPI.com Search or Autocomplete API returns matching cities and towns as an array of Location object.", + "operationId": "search-autocomplete-weather", + "parameters": [ + { + "in": "query", + "name": "q", + "type": "string", + "required": true, + "description": "Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to learn more." + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "$ref": "#/definitions/ArrayOfSearch" + } + }, + "400": { + "$ref": "#/responses/400" + }, + "401": { + "$ref": "#/responses/401" + }, + "403": { + "$ref": "#/responses/403" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/ip.json": { + "get": { + "tags": [ + "APIs" + ], + "summary": "IP Lookup API", + "description": "IP Lookup API method allows a user to get up to date information for an IP address.", + "operationId": "ip-lookup", + "parameters": [ + { + "in": "query", + "name": "q", + "type": "string", + "required": true, + "description": "Pass IP address." + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "$ref": "#/definitions/ip" + } + }, + "400": { + "$ref": "#/responses/400" + }, + "401": { + "$ref": "#/responses/401" + }, + "403": { + "$ref": "#/responses/403" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/timezone.json": { + "get": { + "tags": [ + "APIs" + ], + "summary": "Time Zone API", + "description": "Return Location Object", + "operationId": "time-zone", + "parameters": [ + { + "in": "query", + "name": "q", + "type": "string", + "required": true, + "description": "Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to learn more." + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "$ref": "#/definitions/location" + } + }, + "400": { + "$ref": "#/responses/400" + }, + "401": { + "$ref": "#/responses/401" + }, + "403": { + "$ref": "#/responses/403" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/astronomy.json": { + "get": { + "tags": [ + "APIs" + ], + "summary": "Astronomy API", + "description": "Return Location and Astronomy Object", + "operationId": "astronomy", + "parameters": [ + { + "in": "query", + "name": "q", + "type": "string", + "required": true, + "description": "Pass US Zipcode, UK Postcode, Canada Postalcode, IP address, Latitude/Longitude (decimal degree) or city name. Visit [request parameter section](https://www.weatherapi.com/docs/#intro-request) to learn more." + }, + { + "in": "query", + "name": "dt", + "type": "string", + "format": "date", + "required": true, + "description": "Date on or after 1st Jan, 2015 in yyyy-MM-dd format" + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "properties": { + "location": { + "$ref": "#/definitions/location" + }, + "astronomy": { + "$ref": "#/definitions/astronomy" + } + } + } + }, + "400": { + "$ref": "#/responses/400" + }, + "401": { + "$ref": "#/responses/401" + }, + "403": { + "$ref": "#/responses/403" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "query", + "name": "key" + } + }, + "definitions": { + "search": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "example": 2796590 + }, + "name": { + "type": "string", + "example": "Holborn" + }, + "region": { + "type": "string", + "example": "Camden Greater London" + }, + "country": { + "type": "string", + "example": "United Kingdom" + }, + "lat": { + "type": "number", + "example": 51.52 + }, + "lon": { + "type": "number", + "example": -0.12 + }, + "url": { + "type": "string", + "example": "holborn-camden-greater-london-united-kingdom" + } + } + }, + "ArrayOfSearch": { + "type": "array", + "items": { + "$ref": "#/definitions/search" + } + }, + "location": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "New York" + }, + "region": { + "type": "string", + "example": "New York" + }, + "country": { + "type": "string", + "example": "United States of America" + }, + "lat": { + "type": "number", + "example": 40.71 + }, + "lon": { + "type": "number", + "example": -74.01 + }, + "tz_id": { + "type": "string", + "example": "America/New_York" + }, + "localtime_epoch": { + "type": "integer", + "format": "int32", + "example": 1658522976 + }, + "localtime": { + "type": "string", + "example": "2022-07-22 16:49" + } + } + }, + "current": { + "type": "object", + "properties": { + "last_updated_epoch": { + "type": "integer", + "format": "int32", + "example": 1658522700 + }, + "last_updated": { + "type": "string", + "example": "2022-07-22 16:45" + }, + "temp_c": { + "type": "number", + "example": 34.4 + }, + "temp_f": { + "type": "number", + "example": 93.9 + }, + "is_day": { + "type": "integer", + "format": "int32", + "example": 1 + }, + "condition": { + "type": "object", + "properties": { + "text": { + "type": "string", + "example": "Partly cloudy" + }, + "icon": { + "type": "string", + "example": "//cdn.weatherapi.com/weather/64x64/day/116.png" + }, + "code": { + "type": "integer", + "format": "int32", + "example": 1003 + } + } + }, + "wind_mph": { + "type": "number", + "example": 16.1 + }, + "wind_kph": { + "type": "number", + "example": 25.9 + }, + "wind_degree": { + "type": "number", + "example": 180 + }, + "wind_dir": { + "type": "string", + "example": "S" + }, + "pressure_mb": { + "type": "number", + "example": 1011 + }, + "pressure_in": { + "type": "number", + "example": 29.85 + }, + "precip_mm": { + "type": "number", + "example": 0 + }, + "precip_in": { + "type": "number", + "example": 0 + }, + "humidity": { + "type": "number", + "example": 31 + }, + "cloud": { + "type": "number", + "example": 75 + }, + "feelslike_c": { + "type": "number", + "example": 37 + }, + "feelslike_f": { + "type": "number", + "example": 98.6 + }, + "vis_km": { + "type": "number", + "example": 16 + }, + "vis_miles": { + "type": "number", + "example": 9 + }, + "uv": { + "type": "integer", + "format": "int32", + "example": 8 + }, + "gust_mph": { + "type": "number", + "example": 11.6 + }, + "gust_kph": { + "type": "number", + "example": 18.7 + }, + "air_quality": { + "type": "object", + "properties": { + "co": { + "type": "number", + "example": 293.70001220703125 + }, + "no2": { + "type": "number", + "example": 18.5 + }, + "o3": { + "type": "number", + "example": 234.60000610351562 + }, + "so2": { + "type": "number", + "example": 12 + }, + "pm2_5": { + "type": "number", + "example": 13.600000381469727 + }, + "pm10": { + "type": "number", + "example": 15 + }, + "us-epa-index": { + "type": "integer", + "format": "int32", + "example": 1 + }, + "gb-defra-index": { + "type": "integer", + "format": "int32", + "example": 2 + } + } + } + } + }, + "forecast": { + "type": "object", + "properties": { + "forecastday": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date", + "example": "2022-07-22T00:00:00.000Z" + }, + "date_epoch": { + "type": "integer", + "format": "int32", + "example": 1658448000 + }, + "day": { + "type": "object", + "properties": { + "maxtemp_c": { + "type": "number", + "example": 35.9 + }, + "maxtemp_f": { + "type": "number", + "example": 96.6 + }, + "mintemp_c": { + "type": "number", + "example": 26.3 + }, + "mintemp_f": { + "type": "number", + "example": 79.3 + }, + "avgtemp_c": { + "type": "number", + "example": 30.7 + }, + "avgtemp_f": { + "type": "number", + "example": 87.3 + }, + "maxwind_mph": { + "type": "number", + "example": 12.8 + }, + "maxwind_kph": { + "type": "number", + "example": 20.5 + }, + "totalprecip_mm": { + "type": "number", + "example": 0 + }, + "totalprecip_in": { + "type": "number", + "example": 0 + }, + "avgvis_km": { + "type": "number", + "example": 10 + }, + "avgvis_miles": { + "type": "number", + "example": 6 + }, + "avghumidity": { + "type": "number", + "example": 53 + }, + "daily_will_it_rain": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "daily_chance_of_rain": { + "type": "number", + "example": 0 + }, + "daily_will_it_snow": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "daily_chance_of_snow": { + "type": "number", + "example": 0 + }, + "condition": { + "type": "object", + "properties": { + "text": { + "type": "string", + "example": "Sunny" + }, + "icon": { + "type": "string", + "example": "//cdn.weatherapi.com/weather/64x64/day/113.png" + }, + "code": { + "type": "integer", + "format": "int32", + "example": 1000 + } + } + }, + "uv": { + "type": "integer", + "format": "int32", + "example": 8 + } + } + }, + "astro": { + "type": "object", + "properties": { + "sunrise": { + "type": "string", + "example": "05:44 AM" + }, + "sunset": { + "type": "string", + "example": "08:20 PM" + }, + "moonrise": { + "type": "string", + "example": "12:58 AM" + }, + "moonset": { + "type": "string", + "example": "03:35 PM" + }, + "moon_phase": { + "type": "string", + "example": "Last Quarter" + }, + "moon_illumination": { + "type": "string", + "example": 36 + } + } + }, + "hour": { + "type": "array", + "items": { + "type": "object", + "properties": { + "time_epoch": { + "type": "integer", + "format": "int32", + "example": 1658462400 + }, + "time": { + "type": "string", + "example": "2022-07-22 00:00" + }, + "temp_c": { + "type": "number", + "example": 28.7 + }, + "temp_f": { + "type": "number", + "example": 83.7 + }, + "is_day": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "condition": { + "type": "object", + "properties": { + "text": { + "type": "string", + "example": "Clear" + }, + "icon": { + "type": "string", + "example": "//cdn.weatherapi.com/weather/64x64/night/113.png" + }, + "code": { + "type": "integer", + "format": "int32", + "example": 1000 + } + } + }, + "wind_mph": { + "type": "number", + "example": 9.4 + }, + "wind_kph": { + "type": "number", + "example": 15.1 + }, + "wind_degree": { + "type": "number", + "example": 265 + }, + "wind_dir": { + "type": "string", + "example": "W" + }, + "pressure_mb": { + "type": "number", + "example": 1007 + }, + "pressure_in": { + "type": "number", + "example": 29.73 + }, + "precip_mm": { + "type": "number", + "example": 0 + }, + "precip_in": { + "type": "number", + "example": 0 + }, + "humidity": { + "type": "number", + "example": 58 + }, + "cloud": { + "type": "number", + "example": 19 + }, + "feelslike_c": { + "type": "number", + "example": 30.5 + }, + "feelslike_f": { + "type": "number", + "example": 86.9 + }, + "windchill_c": { + "type": "number", + "example": 28.7 + }, + "windchill_f": { + "type": "number", + "example": 83.7 + }, + "heatindex_c": { + "type": "number", + "example": 30.5 + }, + "heatindex_f": { + "type": "number", + "example": 86.9 + }, + "dewpoint_c": { + "type": "number", + "example": 19.6 + }, + "dewpoint_f": { + "type": "number", + "example": 67.3 + }, + "will_it_rain": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "chance_of_rain": { + "type": "number", + "example": 0 + }, + "will_it_snow": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "chance_of_snow": { + "type": "number", + "example": 0 + }, + "vis_km": { + "type": "number", + "example": 10 + }, + "vis_miles": { + "type": "number", + "example": 6 + }, + "gust_mph": { + "type": "number", + "example": 15 + }, + "gust_kph": { + "type": "number", + "example": 24.1 + }, + "uv": { + "type": "integer", + "format": "int32", + "example": 1 + } + } + } + } + } + } + } + } + }, + "marine": { + "type": "object", + "properties": { + "forecastday": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date", + "example": "2022-07-22T00:00:00.000Z" + }, + "date_epoch": { + "type": "integer", + "format": "int32", + "example": 1658448000 + }, + "day": { + "type": "object", + "properties": { + "maxtemp_c": { + "type": "number", + "example": 35.9 + }, + "maxtemp_f": { + "type": "number", + "example": 96.6 + }, + "mintemp_c": { + "type": "number", + "example": 26.3 + }, + "mintemp_f": { + "type": "number", + "example": 79.3 + }, + "avgtemp_c": { + "type": "number", + "example": 30.7 + }, + "avgtemp_f": { + "type": "number", + "example": 87.3 + }, + "maxwind_mph": { + "type": "number", + "example": 12.8 + }, + "maxwind_kph": { + "type": "number", + "example": 20.5 + }, + "totalprecip_mm": { + "type": "number", + "example": 0 + }, + "totalprecip_in": { + "type": "number", + "example": 0 + }, + "avgvis_km": { + "type": "number", + "example": 10 + }, + "avgvis_miles": { + "type": "number", + "example": 6 + }, + "avghumidity": { + "type": "number", + "example": 53 + }, + "daily_will_it_rain": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "daily_chance_of_rain": { + "type": "number", + "example": 0 + }, + "daily_will_it_snow": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "daily_chance_of_snow": { + "type": "number", + "example": 0 + }, + "condition": { + "type": "object", + "properties": { + "text": { + "type": "string", + "example": "Sunny" + }, + "icon": { + "type": "string", + "example": "//cdn.weatherapi.com/weather/64x64/day/113.png" + }, + "code": { + "type": "integer", + "format": "int32", + "example": 1000 + } + } + }, + "uv": { + "type": "integer", + "format": "int32", + "example": 8 + } + } + }, + "astro": { + "type": "object", + "properties": { + "sunrise": { + "type": "string", + "example": "05:44 AM" + }, + "sunset": { + "type": "string", + "example": "08:20 PM" + }, + "moonrise": { + "type": "string", + "example": "12:58 AM" + }, + "moonset": { + "type": "string", + "example": "03:35 PM" + }, + "moon_phase": { + "type": "string", + "example": "Last Quarter" + }, + "moon_illumination": { + "type": "string", + "example": 36 + } + } + }, + "hour": { + "type": "array", + "items": { + "type": "object", + "properties": { + "time_epoch": { + "type": "integer", + "format": "int32", + "example": 1658462400 + }, + "time": { + "type": "string", + "example": "2022-07-22 00:00" + }, + "temp_c": { + "type": "number", + "example": 28.7 + }, + "temp_f": { + "type": "number", + "example": 83.7 + }, + "is_day": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "condition": { + "type": "object", + "properties": { + "text": { + "type": "string", + "example": "Clear" + }, + "icon": { + "type": "string", + "example": "//cdn.weatherapi.com/weather/64x64/night/113.png" + }, + "code": { + "type": "integer", + "format": "int32", + "example": 1000 + } + } + }, + "wind_mph": { + "type": "number", + "example": 9.4 + }, + "wind_kph": { + "type": "number", + "example": 15.1 + }, + "wind_degree": { + "type": "number", + "example": 265 + }, + "wind_dir": { + "type": "string", + "example": "W" + }, + "pressure_mb": { + "type": "number", + "example": 1007 + }, + "pressure_in": { + "type": "number", + "example": 29.73 + }, + "precip_mm": { + "type": "number", + "example": 0 + }, + "precip_in": { + "type": "number", + "example": 0 + }, + "humidity": { + "type": "number", + "example": 58 + }, + "cloud": { + "type": "number", + "example": 19 + }, + "feelslike_c": { + "type": "number", + "example": 30.5 + }, + "feelslike_f": { + "type": "number", + "example": 86.9 + }, + "windchill_c": { + "type": "number", + "example": 28.7 + }, + "windchill_f": { + "type": "number", + "example": 83.7 + }, + "heatindex_c": { + "type": "number", + "example": 30.5 + }, + "heatindex_f": { + "type": "number", + "example": 86.9 + }, + "dewpoint_c": { + "type": "number", + "example": 19.6 + }, + "dewpoint_f": { + "type": "number", + "example": 67.3 + }, + "will_it_rain": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "chance_of_rain": { + "type": "number", + "example": 0 + }, + "will_it_snow": { + "type": "integer", + "format": "int32", + "example": 0 + }, + "chance_of_snow": { + "type": "number", + "example": 0 + }, + "vis_km": { + "type": "number", + "example": 10 + }, + "vis_miles": { + "type": "number", + "example": 6 + }, + "gust_mph": { + "type": "number", + "example": 15 + }, + "gust_kph": { + "type": "number", + "example": 24.1 + }, + "sig_ht_mt": { + "type": "number", + "example": 24.1 + }, + "swell_ht_mt": { + "type": "number", + "example": 24.1 + }, + "swell_ht_ft": { + "type": "number", + "example": 24.1 + }, + "swell_dir": { + "type": "number", + "example": 24.1 + }, + "swell_dir_16_point": { + "type": "number", + "example": 24.1 + }, + "swell_period_secs": { + "type": "number", + "example": 24.1 + }, + "uv": { + "type": "integer", + "format": "int32", + "example": 1 + } + } + } + } + } + } + } + } + }, + "alerts": { + "type": "object", + "properties": { + "alert": { + "type": "array", + "items": { + "type": "object", + "properties": { + "headline": { + "type": "string", + "example": "NWS New York City - Upton (Long Island and New York City)" + }, + "msgtype": { + "type": "string", + "example": null + }, + "severity": { + "type": "string", + "example": null + }, + "urgency": { + "type": "string", + "example": null + }, + "areas": { + "type": "string", + "example": null + }, + "category": { + "type": "string", + "example": "Extreme temperature value" + }, + "certainty": { + "type": "string", + "example": null + }, + "event": { + "type": "string", + "example": "Heat Advisory" + }, + "note": { + "type": "string", + "example": null + }, + "effective": { + "type": "string", + "format": "date-time", + "example": "2022-07-21T19:38:00.000Z" + }, + "expires": { + "type": "string", + "format": "date-time", + "example": "2022-07-25T00:00:00.000Z" + }, + "desc": { + "type": "string", + "example": "...HEAT ADVISORY REMAINS IN EFFECT UNTIL 8 PM EDT SUNDAY... * WHAT...Heat index values up to 105. * WHERE...Eastern Passaic Hudson Western Bergen Western Essex Eastern Bergen and Eastern Essex Counties. * WHEN...Until 8 PM EDT Sunday. * IMPACTS...High temperatures and high humidity may cause heat illnesses to occur." + }, + "instruction": { + "type": "string", + "example": "" + } + } + } + } + } + }, + "ip": { + "type": "object", + "properties": { + "ip": { + "type": "string", + "example": "185.249.71.82" + }, + "type": { + "type": "string", + "example": "ipv4" + }, + "continent_code": { + "type": "string", + "example": "EU" + }, + "continent_name": { + "type": "string", + "example": "Europe" + }, + "country_code": { + "type": "string", + "example": "GB" + }, + "country_name": { + "type": "string", + "example": "United Kingdom" + }, + "is_eu": { + "type": "string", + "example": false + }, + "geoname_id": { + "type": "integer", + "format": "int32", + "example": 2643743 + }, + "city": { + "type": "string", + "example": "London" + }, + "region": { + "type": "string", + "example": null + }, + "lat": { + "type": "number", + "example": 51.5264 + }, + "lon": { + "type": "number", + "example": -0.0841 + }, + "tz_id": { + "type": "string", + "example": "Europe/London" + }, + "localtime_epoch": { + "type": "integer", + "format": "int32", + "example": 1658520645 + }, + "localtime": { + "type": "string", + "example": "2022-07-22 21:10" + } + } + }, + "astronomy": { + "type": "object", + "properties": { + "astro": { + "type": "object", + "properties": { + "sunrise": { + "type": "string", + "example": "05:10 AM" + }, + "sunset": { + "type": "string", + "example": "09:03 PM" + }, + "moonrise": { + "type": "string", + "example": "12:29 AM" + }, + "moonset": { + "type": "string", + "example": "04:01 PM" + }, + "moon_phase": { + "type": "string", + "example": "Third Quarter" + }, + "moon_illumination": { + "type": "string", + "example": 42 + } + } + } + } + }, + "error400": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32", + "example": 1003 + }, + "message": { + "type": "string", + "example": "Parameter 'q' not provided." + } + } + }, + "error401": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32", + "example": 1002 + }, + "message": { + "type": "string", + "example": "API key not provided" + } + } + }, + "error403": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32", + "example": 2007 + }, + "message": { + "type": "string", + "example": "API key has exceeded calls per month quota." + } + } + } + } +} \ No newline at end of file diff --git a/tests/WEATHER_API/astronomy_json.json b/tests/WEATHER_API/astronomy_json.json new file mode 100644 index 000000000..33039f2c7 --- /dev/null +++ b/tests/WEATHER_API/astronomy_json.json @@ -0,0 +1,72 @@ +[ + { + "q": "London", + "dt": "2024-01-15", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "New York", + "dt": "2024-02-20", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Paris", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "", + "dt": "2024-03-10", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "dt": "2024-04-05", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "InvalidLocationXYZ123456", + "dt": "2024-05-15", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Tokyo", + "dt": "invalid-date-format", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Berlin", + "dt": "2024-06-25", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Sydney", + "dt": "2024-07-30", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Mumbai", + "dt": "2024-08-12", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": "Dubai", + "dt": "2024-09-18", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": " ", + "dt": "2024-10-22", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + } +] \ No newline at end of file diff --git a/tests/WEATHER_API/config.yml b/tests/WEATHER_API/config.yml new file mode 100644 index 000000000..13cf964d0 --- /dev/null +++ b/tests/WEATHER_API/config.yml @@ -0,0 +1,19 @@ + +# This config.yml contains user provided data for api testing. Allows to define values here or use ENV to load values. e.g. ENV[API_HOST] = "https://exampl2.com" +# api: +# host: "${API_HOST:-https://example.com/api/v2}" # includes base path +# auth: +# api_key: "${API_KEY:-}" +# api_key_header: "${KEYNAME:-DefaultValue}" # openapi.spec.security.KEY_NAME +# basic_auth: "${username:-}:${password:-}" +# test_data: +# id: "${TEST_ID:-282739-1238371-219393-2833}" # Any test data key value pair e.g. GET /api/v1/cart/:id +# context-id: "${TEST_context-id:-}" # GET /api/v1/{context-id}/summary + + + +api: + host: "${API_HOST:-https://api.weatherapi.com}" +auth: + ApiKeyAuth: "${key:-}" +test_data: {} diff --git a/tests/WEATHER_API/conftest.py b/tests/WEATHER_API/conftest.py new file mode 100644 index 000000000..b0e1b0dba --- /dev/null +++ b/tests/WEATHER_API/conftest.py @@ -0,0 +1,162 @@ +import os +import re +import pytest +import requests +import yaml +from pathlib import Path +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + + +def _expand_env_in_string(value): + """Expand environment variables in string with ${VAR:-default} syntax.""" + if not isinstance(value, str): + return value + + pattern = r'\$\{([^}:]+)(?::-([^}]*))?\}' + + def replace_env(match): + env_var = match.group(1) + default = match.group(2) if match.group(2) is not None else '' + return os.environ.get(env_var, default) + + return re.sub(pattern, replace_env, value) + + +def _expand_env_in_dict(data): + """Recursively expand environment variables in dictionary.""" + if isinstance(data, dict): + return {key: _expand_env_in_dict(value) for key, value in data.items()} + elif isinstance(data, list): + return [_expand_env_in_dict(item) for item in data] + elif isinstance(data, str): + return _expand_env_in_string(data) + return data + + +class APIClient: + """Simple API client for making HTTP requests.""" + + def __init__(self, host, auth=None, timeout=30): + self.host = host.strip() if host else '' + self.auth = auth or {} + self.timeout = timeout + self.session = requests.Session() + + # Configure retry strategy + retry_strategy = Retry( + total=3, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE", "PATCH"] + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + def _build_url(self, endpoint): + """Build full URL from host and endpoint.""" + host = self.host.rstrip('/') + endpoint = endpoint.lstrip('/') if endpoint else '' + return f"{host}/{endpoint}" + + def _prepare_headers(self, headers=None): + """Prepare headers with authentication.""" + prepared_headers = headers.copy() if headers else {} + return prepared_headers + + def _prepare_params(self, params=None): + """Prepare query parameters with authentication if ApiKeyAuth is set.""" + prepared_params = params.copy() if params else {} + if self.auth.get('ApiKeyAuth'): + prepared_params['key'] = self.auth['ApiKeyAuth'] + return prepared_params + + def make_request(self, endpoint, params=None, headers=None, method='GET', json=None, data=None): + """Make an HTTP request to the API.""" + url = self._build_url(endpoint) + prepared_headers = self._prepare_headers(headers) + prepared_params = self._prepare_params(params) + + response = self.session.request( + method=method.upper(), + url=url, + params=prepared_params, + headers=prepared_headers, + json=json, + data=data, + timeout=self.timeout + ) + return response + + def get(self, endpoint, headers=None, params=None): + """Make a GET request.""" + return self.make_request(endpoint, params=params, headers=headers, method='GET') + + def post(self, endpoint, headers=None, params=None, json=None, data=None): + """Make a POST request.""" + return self.make_request(endpoint, params=params, headers=headers, method='POST', json=json, data=data) + + def put(self, endpoint, headers=None, params=None, json=None, data=None): + """Make a PUT request.""" + return self.make_request(endpoint, params=params, headers=headers, method='PUT', json=json, data=data) + + def delete(self, endpoint, headers=None, params=None): + """Make a DELETE request.""" + return self.make_request(endpoint, params=params, headers=headers, method='DELETE') + + def patch(self, endpoint, headers=None, params=None, json=None, data=None): + """Make a PATCH request.""" + return self.make_request(endpoint, params=params, headers=headers, method='PATCH', json=json, data=data) + + +def pytest_configure(config): + """Register custom markers.""" + config.addinivalue_line( + "markers", "smoke: For all success scenarios" + ) + + +@pytest.fixture(scope="session") +def config(): + """Load configuration from config.yml file with environment variable expansion.""" + config_path = os.path.join(os.path.dirname(__file__), 'config.yml') + + try: + with open(config_path, 'r') as f: + raw_config = yaml.safe_load(f) + except FileNotFoundError: + raise FileNotFoundError(f"Configuration file not found at: {config_path}") + except yaml.YAMLError as e: + raise ValueError(f"Error parsing config.yml: {e}") + + if raw_config is None: + raw_config = {} + + expanded_config = _expand_env_in_dict(raw_config) + return expanded_config + + +@pytest.fixture(scope="session") +def api_host(config): + """Get API host from configuration.""" + host = config.get('api', {}).get('host', '') + return host.strip() if host else '' + + +@pytest.fixture(scope="session") +def auth(config): + """Get authentication configuration.""" + return config.get('auth', {}) + + +@pytest.fixture(scope="session") +def api_client(api_host, auth): + """Create an API client instance.""" + return APIClient(host=api_host, auth=auth) + + +@pytest.fixture(scope="session") +def config_test_data(config): + """Load test_data from config fixture.""" + return config.get('test_data', {}) diff --git a/tests/WEATHER_API/current_json.json b/tests/WEATHER_API/current_json.json new file mode 100644 index 000000000..43255405d --- /dev/null +++ b/tests/WEATHER_API/current_json.json @@ -0,0 +1,71 @@ +[ + { + "q": "London", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "New York", + "lang": "en", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Paris", + "lang": "fr", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "48.8566,2.3522", + "lang": "de", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "lang": "en", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "", + "lang": "en", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "xyznonexistentlocation12345", + "lang": "en", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "!!!@@@###", + "lang": "en", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Tokyo", + "lang": "ja", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Berlin", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Sydney", + "lang": "en", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": "Mumbai", + "lang": "hi", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + } +] \ No newline at end of file diff --git a/tests/WEATHER_API/forecast_json.json b/tests/WEATHER_API/forecast_json.json new file mode 100644 index 000000000..c4e70a4f9 --- /dev/null +++ b/tests/WEATHER_API/forecast_json.json @@ -0,0 +1,101 @@ +[ + { + "q": "London", + "days": "3", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "New York", + "days": "7", + "dt": "2024-01-15", + "lang": "en", + "alerts": "yes", + "aqi": "yes", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Paris", + "days": "5", + "hour": "12", + "lang": "fr", + "alerts": "no", + "aqi": "no", + "tp": "15", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Tokyo", + "unixdt": "1705315200", + "lang": "ja", + "aqi": "yes", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "days": "5", + "dt": "2024-01-20", + "lang": "en", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "", + "days": "3", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "xyznonexistentlocation12345", + "days": "3", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Berlin", + "days": "invalid_number", + "dt": "not-a-date", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Sydney", + "days": "3", + "alerts": "invalid_value", + "aqi": "maybe", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Madrid", + "days": "5", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Rome", + "days": "7", + "lang": "it", + "aqi": "yes", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Dubai", + "days": "10", + "alerts": "yes", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": "Singapore", + "days": "14", + "tp": "15", + "aqi": "yes", + "alerts": "yes", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + } +] \ No newline at end of file diff --git a/tests/WEATHER_API/future_json.json b/tests/WEATHER_API/future_json.json new file mode 100644 index 000000000..9573cf399 --- /dev/null +++ b/tests/WEATHER_API/future_json.json @@ -0,0 +1,74 @@ +[ + { + "q": "London", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "New York", + "dt": "2024-01-15", + "lang": "en", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Paris", + "dt": "2024-02-20", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Tokyo", + "lang": "ja", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "dt": "2024-03-10", + "lang": "es", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "", + "dt": "2024-04-05", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "xyznonexistentlocation12345", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Berlin", + "dt": "invalid-date-format", + "lang": "de", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Sydney", + "lang": "en", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Mumbai", + "dt": "2024-05-12", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Dubai", + "dt": "2024-06-18", + "lang": "ar", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": "Singapore", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + } +] \ No newline at end of file diff --git a/tests/WEATHER_API/history_json.json b/tests/WEATHER_API/history_json.json new file mode 100644 index 000000000..279aff917 --- /dev/null +++ b/tests/WEATHER_API/history_json.json @@ -0,0 +1,87 @@ +[ + { + "q": "London", + "dt": "2024-01-15", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "New York", + "dt": "2024-02-20", + "unixdt": "1708387200", + "end_dt": "2024-02-25", + "unixend_dt": "1708819200", + "hour": "12", + "lang": "en", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Paris", + "dt": "2024-03-10", + "hour": "8", + "lang": "fr", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "dt": "2024-01-15", + "hour": "10", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "", + "dt": "2024-01-15", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "InvalidLocationXYZ123456", + "dt": "2024-01-15", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Tokyo", + "dt": "invalid-date-format", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Berlin", + "dt": "2024-05-01", + "hour": "abc", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Sydney", + "dt": "2024-06-15", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Mumbai", + "dt": "2024-07-20", + "lang": "hi", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Dubai", + "dt": "2024-08-10", + "end_dt": "2024-08-15", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": "Singapore", + "dt": "2024-09-01", + "unixdt": "1725148800", + "hour": "14", + "lang": "zh", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + } +] \ No newline at end of file diff --git a/tests/WEATHER_API/ip_json.json b/tests/WEATHER_API/ip_json.json new file mode 100644 index 000000000..c09f2c32b --- /dev/null +++ b/tests/WEATHER_API/ip_json.json @@ -0,0 +1,52 @@ +[ + { + "q": "London", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "New York", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "48.8566,2.3522", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Tokyo, Japan", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "xyznonexistentlocation12345", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "!!!@@@###", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Sydney", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Paris", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": "Berlin", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + } +] \ No newline at end of file diff --git a/tests/WEATHER_API/marine_json.json b/tests/WEATHER_API/marine_json.json new file mode 100644 index 000000000..163d4b33a --- /dev/null +++ b/tests/WEATHER_API/marine_json.json @@ -0,0 +1,98 @@ +[ + { + "q": "London", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "New York", + "days": "3", + "dt": "2024-01-15", + "hour": "12", + "lang": "en", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Paris", + "days": "7", + "unixdt": "1705315200", + "lang": "fr", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Tokyo", + "days": "5", + "dt": "2024-02-20", + "hour": "18", + "lang": "ja", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "days": "3", + "dt": "2024-01-15", + "hour": "10", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "", + "days": "5", + "lang": "en", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "xyznonexistentlocation12345", + "days": "3", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Berlin", + "days": "abc", + "dt": "invalid-date", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Sydney", + "days": "3", + "hour": "99", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Madrid", + "days": "5", + "lang": "en", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Rome", + "dt": "2024-03-10", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Dubai", + "days": "7", + "hour": "6", + "lang": "ar", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": "Singapore", + "days": "14", + "dt": "2024-04-01", + "unixdt": "1711929600", + "hour": "15", + "lang": "zh", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + } +] \ No newline at end of file diff --git a/tests/WEATHER_API/search_json.json b/tests/WEATHER_API/search_json.json new file mode 100644 index 000000000..c09f2c32b --- /dev/null +++ b/tests/WEATHER_API/search_json.json @@ -0,0 +1,52 @@ +[ + { + "q": "London", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "New York", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "48.8566,2.3522", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Tokyo, Japan", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "xyznonexistentlocation12345", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "!!!@@@###", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Sydney", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Paris", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": "Berlin", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + } +] \ No newline at end of file diff --git a/tests/WEATHER_API/test_astronomy_json_get.py b/tests/WEATHER_API/test_astronomy_json_get.py new file mode 100644 index 000000000..95d90f7de --- /dev/null +++ b/tests/WEATHER_API/test_astronomy_json_get.py @@ -0,0 +1,417 @@ +# ********RoostGPT******** + +# Test generated by RoostGPT for test API-test-file using AI Type Claude AI and AI Model claude-opus-4-5-20251101 +# +# Test file generated for /astronomy.json_get for http method type GET +# RoostTestHash=656c29a7ca +# +# + +# ********RoostGPT******** +""" +Astronomy API Test Suite + +This module contains comprehensive pytest tests for the Weather API Astronomy endpoint. +Tests cover success scenarios, error handling, and schema validation. + +Setup: + 1. Install dependencies: pip install pytest requests pyyaml jsonschema + 2. Set environment variables: + - API_HOST: API base URL (default: https://api.weatherapi.com) + - key: Valid API key for authentication + 3. Ensure config.yml, api.json, and astronomy_json.json are in the test directory + 4. Run tests: pytest test_astronomy.py -v + 5. Run smoke tests only: pytest test_astronomy.py -v -m smoke +""" + +import json +import os +import pytest +from pathlib import Path +from validator import SwaggerSchemaValidator + + +# Constants +ENDPOINT = "/v1/astronomy.json" +API_SPEC_PATH = os.path.join(os.path.dirname(__file__), "api.json") +TEST_DATA_FILE = os.path.join(os.path.dirname(__file__), "astronomy_json.json") + + +@pytest.fixture(scope="module") +def schema_validator(): + """Initialize the SwaggerSchemaValidator with the API spec.""" + return SwaggerSchemaValidator(API_SPEC_PATH) + + +@pytest.fixture(scope="module") +def endpoint_test_data(): + """Load test data from astronomy_json.json file.""" + with open(TEST_DATA_FILE, "r") as f: + return json.load(f) + + +@pytest.fixture(scope="function") +def test_params(config_test_data, endpoint_test_data): + """ + Merge config_test_data with endpoint_test_data. + Endpoint test data overrides default config test data. + """ + merged_data = [] + for test_case in endpoint_test_data: + merged_case = {**config_test_data, **test_case} + merged_data.append(merged_case) + return merged_data + + +class TestAstronomyAPISuccess: + """Test class for successful Astronomy API responses.""" + + @pytest.mark.smoke + def test_astronomy_success_scenarios( + self, api_client, endpoint_test_data, schema_validator + ): + """ + Test successful astronomy API calls with valid parameters. + + This test iterates through all test cases in the JSON file + and validates successful responses against the API schema. + """ + for test_case in endpoint_test_data: + if test_case.get("statusCode") != 200: + continue + + params = { + "q": test_case["q"], + "dt": test_case["dt"] + } + + response = api_client.get(ENDPOINT, params=params) + + # Assert status code + assert response.status_code == test_case["statusCode"], ( + f"Scenario: {test_case.get('scenario', 'Unknown')} - " + f"Expected status {test_case['statusCode']}, got {response.status_code}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed for scenario '{test_case.get('scenario')}': " + f"{validation_result.get('message', 'Unknown error')}" + ) + + # Validate response structure + response_json = response.json() + assert "location" in response_json, "Response missing 'location' field" + assert "astronomy" in response_json, "Response missing 'astronomy' field" + + # Validate location object + location = response_json["location"] + assert "name" in location, "Location missing 'name' field" + assert "lat" in location, "Location missing 'lat' field" + assert "lon" in location, "Location missing 'lon' field" + + # Validate astronomy object + astronomy = response_json["astronomy"] + assert "astro" in astronomy, "Astronomy missing 'astro' field" + astro = astronomy["astro"] + assert "sunrise" in astro, "Astro missing 'sunrise' field" + assert "sunset" in astro, "Astro missing 'sunset' field" + + +class TestAstronomyAPIBadRequest: + """Test class for 400 Bad Request scenarios.""" + + def test_missing_required_param_q(self, api_client, endpoint_test_data, schema_validator): + """ + Test API response when required parameter 'q' is missing. + + Expected: 400 Bad Request with error code 1003 + """ + # Get a valid dt from test data + valid_dt = endpoint_test_data[0]["dt"] if endpoint_test_data else "2024-01-15" + + params = { + "dt": valid_dt + # 'q' is intentionally missing + } + + response = api_client.get(ENDPOINT, params=params) + + # API should return 400 for missing required parameter + assert response.status_code == 400, ( + f"Expected 400 for missing 'q' parameter, got {response.status_code}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + # Validate error response structure + response_json = response.json() + assert "error" in response_json or "code" in response_json or "message" in response_json, ( + "Error response should contain error information" + ) + + def test_missing_required_param_dt(self, api_client, endpoint_test_data, schema_validator): + """ + Test API response when required parameter 'dt' is missing. + + Expected: 400 Bad Request + """ + # Get a valid q from test data + valid_q = endpoint_test_data[0]["q"] if endpoint_test_data else "London" + + params = { + "q": valid_q + # 'dt' is intentionally missing + } + + response = api_client.get(ENDPOINT, params=params) + + # API should return 400 for missing required parameter + assert response.status_code == 400, ( + f"Expected 400 for missing 'dt' parameter, got {response.status_code}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_invalid_location_query(self, api_client, endpoint_test_data, schema_validator): + """ + Test API response with invalid location query. + + Expected: 400 Bad Request with error code 1006 (No location found) + """ + valid_dt = endpoint_test_data[0]["dt"] if endpoint_test_data else "2024-01-15" + + params = { + "q": "InvalidLocationXYZ12345NonExistent", + "dt": valid_dt + } + + response = api_client.get(ENDPOINT, params=params) + + # API should return 400 for invalid location + assert response.status_code == 400, ( + f"Expected 400 for invalid location, got {response.status_code}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_invalid_date_format(self, api_client, endpoint_test_data, schema_validator): + """ + Test API response with invalid date format. + + Expected: 400 Bad Request + """ + valid_q = endpoint_test_data[0]["q"] if endpoint_test_data else "London" + + params = { + "q": valid_q, + "dt": "invalid-date-format" + } + + response = api_client.get(ENDPOINT, params=params) + + # API should return 400 for invalid date format + assert response.status_code == 400, ( + f"Expected 400 for invalid date format, got {response.status_code}" + ) + + def test_empty_query_parameter(self, api_client, endpoint_test_data, schema_validator): + """ + Test API response with empty 'q' parameter. + + Expected: 400 Bad Request + """ + valid_dt = endpoint_test_data[0]["dt"] if endpoint_test_data else "2024-01-15" + + params = { + "q": "", + "dt": valid_dt + } + + response = api_client.get(ENDPOINT, params=params) + + # API should return 400 for empty required parameter + assert response.status_code == 400, ( + f"Expected 400 for empty 'q' parameter, got {response.status_code}" + ) + + +class TestAstronomyAPIEdgeCases: + """Test class for edge cases and boundary conditions.""" + + def test_date_boundary_earliest_valid(self, api_client, endpoint_test_data, schema_validator): + """ + Test API with earliest valid date (1st Jan 2015). + + Per API spec, dates must be on or after 1st Jan, 2015. + """ + valid_q = endpoint_test_data[0]["q"] if endpoint_test_data else "London" + + params = { + "q": valid_q, + "dt": "2015-01-01" # Earliest valid date per spec + } + + response = api_client.get(ENDPOINT, params=params) + + # Should succeed with valid earliest date + assert response.status_code == 200, ( + f"Expected 200 for earliest valid date, got {response.status_code}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_date_before_minimum(self, api_client, endpoint_test_data, schema_validator): + """ + Test API with date before minimum allowed (before 1st Jan 2015). + + Expected: 400 Bad Request + """ + valid_q = endpoint_test_data[0]["q"] if endpoint_test_data else "London" + + params = { + "q": valid_q, + "dt": "2014-12-31" # Before earliest valid date + } + + response = api_client.get(ENDPOINT, params=params) + + # API may return 400 for date before minimum + # Note: Actual behavior depends on API implementation + assert response.status_code in [200, 400], ( + f"Expected 200 or 400 for date before minimum, got {response.status_code}" + ) + + def test_special_characters_in_query(self, api_client, endpoint_test_data, schema_validator): + """ + Test API with special characters in location query. + + Tests URL encoding and special character handling. + """ + valid_dt = endpoint_test_data[0]["dt"] if endpoint_test_data else "2024-01-15" + + params = { + "q": "São Paulo", # Special character + "dt": valid_dt + } + + response = api_client.get(ENDPOINT, params=params) + + # Should handle special characters gracefully + assert response.status_code in [200, 400], ( + f"Expected 200 or 400 for special characters, got {response.status_code}" + ) + + def test_coordinates_as_query(self, api_client, endpoint_test_data, schema_validator): + """ + Test API with latitude/longitude coordinates as query. + + Per API spec, coordinates in decimal degree format are valid. + """ + valid_dt = endpoint_test_data[0]["dt"] if endpoint_test_data else "2024-01-15" + + params = { + "q": "40.71,-74.01", # New York coordinates + "dt": valid_dt + } + + response = api_client.get(ENDPOINT, params=params) + + # Should succeed with valid coordinates + assert response.status_code == 200, ( + f"Expected 200 for coordinate query, got {response.status_code}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_ip_address_as_query(self, api_client, endpoint_test_data, schema_validator): + """ + Test API with IP address as query. + + Per API spec, IP addresses are valid query parameters. + """ + valid_dt = endpoint_test_data[0]["dt"] if endpoint_test_data else "2024-01-15" + + params = { + "q": "8.8.8.8", # Google DNS IP + "dt": valid_dt + } + + response = api_client.get(ENDPOINT, params=params) + + # Should succeed with valid IP address + assert response.status_code == 200, ( + f"Expected 200 for IP address query, got {response.status_code}" + ) + + +class TestAstronomyAPITableDriven: + """Table-driven tests using data from JSON file.""" + + def test_all_scenarios_from_json( + self, api_client, endpoint_test_data, schema_validator + ): + """ + Execute all test scenarios defined in astronomy_json.json. + + This test iterates through each test case in the JSON file + and validates the response against expected status code and schema. + """ + for test_case in endpoint_test_data: + scenario = test_case.get("scenario", "Unknown scenario") + expected_status = test_case.get("statusCode") + + params = { + "q": test_case["q"], + "dt": test_case["dt"] + } + + response = api_client.get(ENDPOINT, params=params) + + # Assert expected status code + assert response.status_code == expected_status, ( + f"Scenario: {scenario} - " + f"Expected status {expected_status}, got {response.status_code}" + ) + + # Validate response schema based on status code + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", str(expected_status), response + ) + assert validation_result["valid"], ( + f"Scenario: {scenario} - Schema validation failed: " + f"{validation_result.get('message', 'Unknown error')}" + ) diff --git a/tests/WEATHER_API/test_current_json_get.py b/tests/WEATHER_API/test_current_json_get.py new file mode 100644 index 000000000..fd89090a5 --- /dev/null +++ b/tests/WEATHER_API/test_current_json_get.py @@ -0,0 +1,510 @@ +# ********RoostGPT******** + +# Test generated by RoostGPT for test API-test-file using AI Type Claude AI and AI Model claude-opus-4-5-20251101 +# +# Test file generated for /current.json_get for http method type GET +# RoostTestHash=4bfd44e81d +# +# + +# ********RoostGPT******** +""" +Pytest test suite for Weather API - Realtime Weather Endpoint + +This module tests the /v1/current.json endpoint which provides current weather data. + +Endpoint: /v1/current.json +Method: GET +Security: ApiKeyAuth (query parameter 'key') + +Response Status Codes: +- 200: OK - Successful weather data retrieval +- 400: Bad Request - Invalid parameters (missing q, invalid URL, no location found) +- 401: Unauthorized - API key not provided or invalid +- 403: Forbidden - API key exceeded quota, disabled, or lacks access + +To run tests: + pytest test_current_weather.py -v + pytest test_current_weather.py -v -m smoke # Run only smoke tests +""" + +import json +import os +import pytest +from pathlib import Path + +# Import validator +from validator import SwaggerSchemaValidator + + +# Constants +ENDPOINT = "/v1/current.json" +API_SPEC_PATH = "api.json" +TEST_DATA_FILE = "current_json.json" + + +@pytest.fixture(scope="module") +def validator(): + """Initialize the SwaggerSchemaValidator with the API spec.""" + spec_path = Path(__file__).parent / API_SPEC_PATH + return SwaggerSchemaValidator(str(spec_path)) + + +@pytest.fixture(scope="module") +def endpoint_test_data(): + """Load test data from current_json.json file.""" + test_data_path = Path(__file__).parent / TEST_DATA_FILE + with open(test_data_path, 'r') as f: + return json.load(f) + + +class TestRealtimeWeatherSuccess: + """Test class for successful weather API responses.""" + + @pytest.mark.smoke + def test_get_current_weather_with_required_params( + self, api_client, config_test_data, validator + ): + """ + Test successful weather retrieval with only required parameter 'q'. + + This is a smoke test that validates the happy path scenario + using only required parameters. + """ + # Load test data - use config_test_data if available, otherwise use default + query_param = config_test_data.get('q', 'London') + + params = {"q": query_param} + + response = api_client.get(ENDPOINT, params=params) + + # Assert status code + assert response.status_code == 200, ( + f"Expected 200, got {response.status_code}. Response: {response.text}" + ) + + # Validate response schema + validation_result = validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + # Validate response structure + response_json = response.json() + assert "location" in response_json, "Response missing 'location' object" + assert "current" in response_json, "Response missing 'current' object" + + # Validate location object has required fields + location = response_json["location"] + assert "name" in location, "Location missing 'name' field" + assert "country" in location, "Location missing 'country' field" + assert "lat" in location, "Location missing 'lat' field" + assert "lon" in location, "Location missing 'lon' field" + + # Validate current object has required fields + current = response_json["current"] + assert "temp_c" in current, "Current missing 'temp_c' field" + assert "temp_f" in current, "Current missing 'temp_f' field" + assert "condition" in current, "Current missing 'condition' field" + + @pytest.mark.smoke + def test_get_current_weather_table_driven( + self, api_client, endpoint_test_data, validator + ): + """ + Table-driven test for successful weather retrieval scenarios. + + Iterates through all test cases defined in current_json.json + and validates each scenario. + """ + for test_case in endpoint_test_data: + scenario = test_case.get("scenario", "Unknown scenario") + expected_status = test_case.get("statusCode", 200) + + # Build params from test case - only include defined params + params = {} + if "q" in test_case: + params["q"] = test_case["q"] + if "lang" in test_case: + params["lang"] = test_case["lang"] + + response = api_client.get(ENDPOINT, params=params) + + # Assert status code matches expected + assert response.status_code == expected_status, ( + f"Scenario: {scenario} - Expected {expected_status}, " + f"got {response.status_code}. Response: {response.text}" + ) + + # Validate response schema for successful responses + if expected_status == 200: + validation_result = validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Scenario: {scenario} - Schema validation failed: " + f"{validation_result.get('message')}" + ) + + response_json = response.json() + assert "location" in response_json, ( + f"Scenario: {scenario} - Response missing 'location'" + ) + assert "current" in response_json, ( + f"Scenario: {scenario} - Response missing 'current'" + ) + + +class TestRealtimeWeatherBadRequest: + """Test class for 400 Bad Request scenarios.""" + + def test_missing_required_param_q(self, api_client, validator): + """ + Test that missing required parameter 'q' returns 400. + + Error code 1003: Parameter 'q' not provided. + """ + # Make request without 'q' parameter + response = api_client.get(ENDPOINT, params={}) + + # API returns 400 for missing required parameter + assert response.status_code == 400, ( + f"Expected 400 for missing 'q' param, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + # Validate error response structure + response_json = response.json() + assert "error" in response_json or "code" in response_json, ( + "Error response should contain error details" + ) + + def test_invalid_location_param_q(self, api_client, validator): + """ + Test that invalid location returns 400. + + Error code 1006: No location found matching parameter 'q'. + """ + params = {"q": "invalidlocationxyz123456789"} + + response = api_client.get(ENDPOINT, params=params) + + # API returns 400 for invalid location + assert response.status_code == 400, ( + f"Expected 400 for invalid location, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + def test_empty_q_parameter(self, api_client, validator): + """ + Test that empty 'q' parameter returns 400. + """ + params = {"q": ""} + + response = api_client.get(ENDPOINT, params=params) + + # Empty q should be treated as missing/invalid + assert response.status_code == 400, ( + f"Expected 400 for empty 'q' param, got {response.status_code}. " + f"Response: {response.text}" + ) + + @pytest.mark.parametrize("invalid_q", [ + " ", # Whitespace only + "!@#$%^&*()", # Special characters only + ]) + def test_invalid_q_parameter_formats(self, api_client, invalid_q): + """ + Test various invalid formats for 'q' parameter. + """ + params = {"q": invalid_q} + + response = api_client.get(ENDPOINT, params=params) + + # Invalid q formats should return 400 + assert response.status_code == 400, ( + f"Expected 400 for invalid 'q'='{invalid_q}', " + f"got {response.status_code}. Response: {response.text}" + ) + + +class TestRealtimeWeatherUnauthorized: + """Test class for 401 Unauthorized scenarios.""" + + def test_missing_api_key(self, api_host): + """ + Test that missing API key returns 401. + + Error code 1002: API key not provided. + """ + import requests + + # Make request without API key + url = f"{api_host.rstrip('/')}/{ENDPOINT.lstrip('/')}" + params = {"q": "London"} + + response = requests.get(url, params=params, timeout=30) + + # API returns 401 for missing API key + assert response.status_code == 401, ( + f"Expected 401 for missing API key, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_invalid_api_key(self, api_host): + """ + Test that invalid API key returns 401. + + Error code 2006: API key provided is invalid. + """ + import requests + + url = f"{api_host.rstrip('/')}/{ENDPOINT.lstrip('/')}" + params = { + "q": "London", + "key": "invalid_api_key_12345" + } + + response = requests.get(url, params=params, timeout=30) + + # API returns 401 for invalid API key + assert response.status_code == 401, ( + f"Expected 401 for invalid API key, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_empty_api_key(self, api_host): + """ + Test that empty API key returns 401. + """ + import requests + + url = f"{api_host.rstrip('/')}/{ENDPOINT.lstrip('/')}" + params = { + "q": "London", + "key": "" + } + + response = requests.get(url, params=params, timeout=30) + + # Empty API key should be treated as missing/invalid + assert response.status_code == 401, ( + f"Expected 401 for empty API key, got {response.status_code}. " + f"Response: {response.text}" + ) + + +class TestRealtimeWeatherOptionalParams: + """Test class for optional parameter scenarios.""" + + @pytest.mark.smoke + def test_with_lang_parameter(self, api_client, config_test_data, validator): + """ + Test weather retrieval with optional 'lang' parameter. + """ + query_param = config_test_data.get('q', 'London') + + params = { + "q": query_param, + "lang": "en" + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200, got {response.status_code}. Response: {response.text}" + ) + + # Validate response schema + validation_result = validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + response_json = response.json() + assert "current" in response_json + assert "condition" in response_json["current"] + assert "text" in response_json["current"]["condition"] + + @pytest.mark.parametrize("lang_code", ["es", "fr", "de", "zh"]) + def test_various_language_codes(self, api_client, config_test_data, lang_code): + """ + Test weather retrieval with various language codes. + """ + query_param = config_test_data.get('q', 'London') + + params = { + "q": query_param, + "lang": lang_code + } + + response = api_client.get(ENDPOINT, params=params) + + # Should return 200 for valid language codes + assert response.status_code == 200, ( + f"Expected 200 for lang={lang_code}, got {response.status_code}. " + f"Response: {response.text}" + ) + + +class TestRealtimeWeatherQueryFormats: + """Test class for various query parameter formats.""" + + @pytest.mark.smoke + def test_query_by_city_name(self, api_client, validator): + """Test weather retrieval by city name.""" + params = {"q": "Paris"} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200, got {response.status_code}. Response: {response.text}" + ) + + validation_result = validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"] + + def test_query_by_coordinates(self, api_client, validator): + """Test weather retrieval by latitude/longitude coordinates.""" + # New York coordinates + params = {"q": "40.71,-74.01"} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200, got {response.status_code}. Response: {response.text}" + ) + + validation_result = validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"] + + def test_query_by_us_zipcode(self, api_client, validator): + """Test weather retrieval by US zipcode.""" + params = {"q": "10001"} # New York zipcode + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200, got {response.status_code}. Response: {response.text}" + ) + + validation_result = validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"] + + def test_query_by_ip_address(self, api_client, validator): + """Test weather retrieval by IP address.""" + params = {"q": "auto:ip"} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200, got {response.status_code}. Response: {response.text}" + ) + + validation_result = validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"] + + +class TestRealtimeWeatherResponseValidation: + """Test class for detailed response validation.""" + + def test_location_object_fields(self, api_client, config_test_data, validator): + """Validate all fields in the location object.""" + query_param = config_test_data.get('q', 'London') + + params = {"q": query_param} + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200 + + response_json = response.json() + location = response_json.get("location", {}) + + # Validate location field types + assert isinstance(location.get("name"), str) + assert isinstance(location.get("region"), str) + assert isinstance(location.get("country"), str) + assert isinstance(location.get("lat"), (int, float)) + assert isinstance(location.get("lon"), (int, float)) + assert isinstance(location.get("tz_id"), str) + assert isinstance(location.get("localtime_epoch"), int) + assert isinstance(location.get("localtime"), str) + + def test_current_object_fields(self, api_client, config_test_data, validator): + """Validate all fields in the current weather object.""" + query_param = config_test_data.get('q', 'London') + + params = {"q": query_param} + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200 + + response_json = response.json() + current = response_json.get("current", {}) + + # Validate current field types + assert isinstance(current.get("temp_c"), (int, float)) + assert isinstance(current.get("temp_f"), (int, float)) + assert isinstance(current.get("is_day"), int) + assert isinstance(current.get("wind_mph"), (int, float)) + assert isinstance(current.get("wind_kph"), (int, float)) + assert isinstance(current.get("humidity"), (int, float)) + + # Validate condition object + condition = current.get("condition", {}) + assert isinstance(condition.get("text"), str) + assert isinstance(condition.get("icon"), str) + assert isinstance(condition.get("code"), int) + + def test_temperature_values_reasonable(self, api_client, config_test_data): + """Validate temperature values are within reasonable bounds.""" + query_param = config_test_data.get('q', 'London') + + params = {"q": query_param} + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200 + + response_json = response.json() + current = response_json.get("current", {}) + + temp_c = current.get("temp_c") + temp_f = current.get("temp_f") + + # Temperature should be within Earth's recorded range + assert -90 <= temp_c <= 60, f"Temperature {temp_c}°C out of reasonable range" + assert -130 <= temp_f <= 140, f"Temperature {temp_f}°F out of reasonable range" + + # Verify Celsius to Fahrenheit conversion is approximately correct + expected_f = (temp_c * 9/5) + 32 + assert abs(temp_f - expected_f) < 1, ( + f"Temperature conversion mismatch: {temp_c}°C should be ~{expected_f}°F, got {temp_f}°F" + ) diff --git a/tests/WEATHER_API/test_forecast_json_get.py b/tests/WEATHER_API/test_forecast_json_get.py new file mode 100644 index 000000000..942216312 --- /dev/null +++ b/tests/WEATHER_API/test_forecast_json_get.py @@ -0,0 +1,458 @@ +# ********RoostGPT******** + +# Test generated by RoostGPT for test API-test-file using AI Type Claude AI and AI Model claude-opus-4-5-20251101 +# +# Test file generated for /forecast.json_get for http method type GET +# RoostTestHash=462421f697 +# +# + +# ********RoostGPT******** +""" +Pytest test suite for Weather API Forecast endpoint. + +This module tests the /v1/forecast.json endpoint with comprehensive coverage +including happy path scenarios, edge cases, and error handling. + +Setup: + 1. Install dependencies: pip install pytest requests pyyaml jsonschema + 2. Set environment variables: + - API_HOST: API base URL (default: https://api.weatherapi.com) + - key: Valid API key for authentication + 3. Run tests: pytest test_forecast.py -v + 4. Run smoke tests only: pytest test_forecast.py -v -m smoke +""" + +import json +import os +import pytest +from pathlib import Path + +# Import validator from validator.py +from validator import SwaggerSchemaValidator + + +# Initialize schema validator with API spec +SPEC_PATH = os.path.join(os.path.dirname(__file__), 'api.json') +schema_validator = SwaggerSchemaValidator(SPEC_PATH) + +# Endpoint configuration +ENDPOINT = "/v1/forecast.json" +HTTP_METHOD = "GET" + + +def load_test_data_from_json(): + """Load test data from forecast_json.json file.""" + json_path = Path(__file__).parent / "forecast_json.json" + with open(json_path, 'r') as f: + return json.load(f) + + +@pytest.fixture(scope="module") +def endpoint_test_data(): + """Fixture to load endpoint-specific test data from JSON file.""" + return load_test_data_from_json() + + +@pytest.fixture(scope="function") +def build_request_params(config_test_data): + """Factory fixture to build request parameters from test data.""" + def _build_params(test_case): + # Start with config test data as base + params = {} + + # Override with test case specific data + # Required parameters + if "q" in test_case: + params["q"] = test_case["q"] + elif "q" in config_test_data: + params["q"] = config_test_data["q"] + + if "days" in test_case: + params["days"] = test_case["days"] + elif "days" in config_test_data: + params["days"] = config_test_data["days"] + + # Optional parameters + optional_params = ["dt", "unixdt", "hour", "lang", "alerts", "aqi", "tp"] + for param in optional_params: + if param in test_case: + params[param] = test_case[param] + elif param in config_test_data: + params[param] = config_test_data[param] + + return params + + return _build_params + + +class TestForecastAPISuccess: + """Test class for successful forecast API responses.""" + + @pytest.mark.smoke + def test_forecast_with_required_params_only(self, api_client, endpoint_test_data, build_request_params): + """ + Test forecast API with only required parameters (q and days). + This is the happy path scenario using only required fields. + """ + # Find a test case with status code 200 + success_cases = [tc for tc in endpoint_test_data if tc.get("statusCode") == 200] + assert success_cases, "No success test cases found in test data" + + # Use first success case but only required params + test_case = success_cases[0] + params = { + "q": test_case.get("q"), + "days": test_case.get("days") + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, HTTP_METHOD, "200", response + ) + assert validation_result["valid"], f"Schema validation failed: {validation_result.get('message')}" + + # Validate response structure + response_json = response.json() + assert "location" in response_json, "Response missing 'location' field" + assert "current" in response_json, "Response missing 'current' field" + assert "forecast" in response_json, "Response missing 'forecast' field" + + @pytest.mark.smoke + def test_forecast_table_driven_success_scenarios(self, api_client, endpoint_test_data, build_request_params): + """ + Table-driven test iterating through all success scenarios from JSON file. + """ + success_cases = [tc for tc in endpoint_test_data if tc.get("statusCode") == 200] + + for test_case in success_cases: + params = build_request_params(test_case) + + response = api_client.get(ENDPOINT, params=params) + + expected_status = test_case.get("statusCode") + assert response.status_code == expected_status, ( + f"Scenario: {test_case.get('scenario')} - " + f"Expected {expected_status}, got {response.status_code}: {response.text}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, HTTP_METHOD, str(expected_status), response + ) + assert validation_result["valid"], ( + f"Scenario: {test_case.get('scenario')} - " + f"Schema validation failed: {validation_result.get('message')}" + ) + + def test_forecast_with_all_optional_params(self, api_client, endpoint_test_data, build_request_params): + """ + Test forecast API with all optional parameters included. + Uses test case that has optional parameters defined. + """ + # Find test case with optional params + cases_with_optional = [ + tc for tc in endpoint_test_data + if tc.get("statusCode") == 200 and any( + key in tc for key in ["dt", "lang", "alerts", "aqi"] + ) + ] + + if not cases_with_optional: + pytest.skip("No test cases with optional parameters found") + + test_case = cases_with_optional[0] + params = build_request_params(test_case) + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, HTTP_METHOD, "200", response + ) + assert validation_result["valid"], f"Schema validation failed: {validation_result.get('message')}" + + response_json = response.json() + + # If aqi=yes was passed, verify air_quality is present + if params.get("aqi") == "yes": + current = response_json.get("current", {}) + assert "air_quality" in current, "Expected air_quality data when aqi=yes" + + # If alerts=yes was passed, verify alerts structure exists + if params.get("alerts") == "yes": + assert "alerts" in response_json, "Expected alerts data when alerts=yes" + + +class TestForecastAPIValidation: + """Test class for request validation and error scenarios.""" + + def test_missing_required_param_q(self, api_client, endpoint_test_data): + """ + Test that missing 'q' parameter returns 400 error. + Error code 1003: Parameter 'q' not provided. + """ + # Get days from test data + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + + params = {"days": success_case.get("days", "3") if success_case else "3"} + + response = api_client.get(ENDPOINT, params=params) + + # Missing required param should return 400 + assert response.status_code == 400, f"Expected 400, got {response.status_code}: {response.text}" + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, HTTP_METHOD, "400", response + ) + assert validation_result["valid"], f"Schema validation failed: {validation_result.get('message')}" + + response_json = response.json() + error = response_json.get("error", response_json) + assert error.get("code") == 1003, f"Expected error code 1003, got {error.get('code')}" + + def test_missing_required_param_days(self, api_client, endpoint_test_data): + """ + Test that missing 'days' parameter returns appropriate error. + """ + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + + params = {"q": success_case.get("q", "London") if success_case else "London"} + + response = api_client.get(ENDPOINT, params=params) + + # Missing days should return 400 + assert response.status_code == 400, f"Expected 400, got {response.status_code}: {response.text}" + + def test_invalid_location_param_q(self, api_client, endpoint_test_data): + """ + Test that invalid location returns 400 error. + Error code 1006: No location found matching parameter 'q'. + """ + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + + params = { + "q": "InvalidLocationXYZ123456789", + "days": success_case.get("days", "3") if success_case else "3" + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 400, f"Expected 400, got {response.status_code}: {response.text}" + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, HTTP_METHOD, "400", response + ) + assert validation_result["valid"], f"Schema validation failed: {validation_result.get('message')}" + + response_json = response.json() + error = response_json.get("error", response_json) + assert error.get("code") == 1006, f"Expected error code 1006, got {error.get('code')}" + + +class TestForecastAPIBoundaryConditions: + """Test class for boundary conditions and edge cases.""" + + @pytest.mark.parametrize("days_value", [1, 14]) + def test_days_boundary_values(self, api_client, endpoint_test_data, days_value): + """ + Test boundary values for 'days' parameter (1 and 14). + Valid range is 1-14 according to API spec. + """ + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + + params = { + "q": success_case.get("q", "London") if success_case else "London", + "days": days_value + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, f"Expected 200 for days={days_value}, got {response.status_code}" + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, HTTP_METHOD, "200", response + ) + assert validation_result["valid"], f"Schema validation failed: {validation_result.get('message')}" + + # Verify forecast days count + response_json = response.json() + forecast_days = response_json.get("forecast", {}).get("forecastday", []) + assert len(forecast_days) <= days_value, f"Expected at most {days_value} forecast days" + + @pytest.mark.parametrize("invalid_days", [0, 15, -1, "invalid"]) + def test_days_invalid_values(self, api_client, endpoint_test_data, invalid_days): + """ + Test invalid values for 'days' parameter outside valid range. + """ + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + + params = { + "q": success_case.get("q", "London") if success_case else "London", + "days": invalid_days + } + + response = api_client.get(ENDPOINT, params=params) + + # Invalid days should return 400 + assert response.status_code == 400, ( + f"Expected 400 for invalid days={invalid_days}, got {response.status_code}" + ) + + def test_empty_q_parameter(self, api_client, endpoint_test_data): + """ + Test empty string for 'q' parameter. + """ + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + + params = { + "q": "", + "days": success_case.get("days", "3") if success_case else "3" + } + + response = api_client.get(ENDPOINT, params=params) + + # Empty q should return 400 + assert response.status_code == 400, f"Expected 400, got {response.status_code}" + + +class TestForecastAPILocationFormats: + """Test class for different location format inputs.""" + + @pytest.mark.parametrize("location_type,location_value", [ + ("city_name", "Paris"), + ("coordinates", "48.8566,2.3522"), + ("us_zipcode", "10001"), + ]) + def test_various_location_formats(self, api_client, endpoint_test_data, location_type, location_value): + """ + Test different location format inputs for 'q' parameter. + """ + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + + params = { + "q": location_value, + "days": success_case.get("days", "3") if success_case else "3" + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200 for {location_type}={location_value}, got {response.status_code}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, HTTP_METHOD, "200", response + ) + assert validation_result["valid"], f"Schema validation failed: {validation_result.get('message')}" + + +class TestForecastAPIResponseStructure: + """Test class for validating response structure and content.""" + + def test_response_contains_location_details(self, api_client, endpoint_test_data, build_request_params): + """ + Test that response contains complete location details. + """ + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + assert success_case, "No success test case found" + + params = build_request_params(success_case) + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200 + + response_json = response.json() + location = response_json.get("location", {}) + + # Verify location fields exist + expected_location_fields = ["name", "region", "country", "lat", "lon", "tz_id", "localtime"] + for field in expected_location_fields: + assert field in location, f"Location missing field: {field}" + + def test_response_contains_current_weather(self, api_client, endpoint_test_data, build_request_params): + """ + Test that response contains current weather data. + """ + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + assert success_case, "No success test case found" + + params = build_request_params(success_case) + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200 + + response_json = response.json() + current = response_json.get("current", {}) + + # Verify current weather fields exist + expected_current_fields = ["temp_c", "temp_f", "condition", "wind_mph", "humidity"] + for field in expected_current_fields: + assert field in current, f"Current weather missing field: {field}" + + def test_response_contains_forecast_data(self, api_client, endpoint_test_data, build_request_params): + """ + Test that response contains forecast data with correct structure. + """ + success_case = next( + (tc for tc in endpoint_test_data if tc.get("statusCode") == 200), + None + ) + assert success_case, "No success test case found" + + params = build_request_params(success_case) + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200 + + response_json = response.json() + forecast = response_json.get("forecast", {}) + + assert "forecastday" in forecast, "Forecast missing 'forecastday' field" + + forecastday = forecast.get("forecastday", []) + assert len(forecastday) > 0, "Forecast should contain at least one day" + + # Verify first forecast day structure + first_day = forecastday[0] + assert "date" in first_day, "Forecast day missing 'date' field" + assert "day" in first_day, "Forecast day missing 'day' field" + assert "astro" in first_day, "Forecast day missing 'astro' field" + assert "hour" in first_day, "Forecast day missing 'hour' field" diff --git a/tests/WEATHER_API/test_future_json_get.py b/tests/WEATHER_API/test_future_json_get.py new file mode 100644 index 000000000..df11e0679 --- /dev/null +++ b/tests/WEATHER_API/test_future_json_get.py @@ -0,0 +1,642 @@ +# ********RoostGPT******** + +# Test generated by RoostGPT for test API-test-file using AI Type Claude AI and AI Model claude-opus-4-5-20251101 +# +# Test file generated for /future.json_get for http method type GET +# RoostTestHash=f812784c59 +# +# + +# ********RoostGPT******** +""" +Test Suite for Weather API - Future Weather Endpoint + +This module contains comprehensive pytest tests for the /v1/future.json endpoint. +The Future Weather API returns weather data in 3-hourly intervals for dates +between 14 and 365 days from today. + +Endpoints Tested: +- GET /v1/future.json + +Security Schema: +- ApiKeyAuth (API key passed as query parameter) + +Response Status Codes: +- 200: Successful response with weather forecast data +- 400: Bad request (missing/invalid parameters) +- 401: Unauthorized (missing/invalid API key) +- 403: Forbidden (quota exceeded, disabled key, or insufficient access) + +Setup: +1. Ensure config.yml has valid API host and API key +2. Set environment variables API_HOST and key if needed +3. Place future_json.json test data file in same directory as test file +4. Run: pytest test_future_weather.py -v + +""" + +import json +import os +import pytest +from pathlib import Path + +# Import validator from validator.py +from validator import SwaggerSchemaValidator + + +# Constants +ENDPOINT = "/v1/future.json" +API_SPEC_PATH = "api.json" + + +@pytest.fixture(scope="module") +def schema_validator(): + """Initialize the SwaggerSchemaValidator with API spec.""" + spec_path = os.path.join(os.path.dirname(__file__), API_SPEC_PATH) + return SwaggerSchemaValidator(spec_path) + + +@pytest.fixture(scope="module") +def endpoint_test_data(): + """Load test data from future_json.json file.""" + json_path = os.path.join(os.path.dirname(__file__), "future_json.json") + with open(json_path, "r") as f: + return json.load(f) + + +class TestFutureWeatherSuccess: + """Test class for successful Future Weather API responses.""" + + @pytest.mark.smoke + def test_future_weather_with_required_params_only( + self, api_client, config_test_data, schema_validator + ): + """ + Test Future Weather API with only required parameter 'q'. + + This is a happy path test using only the required 'q' parameter. + The API should return a 200 response with valid forecast data. + """ + # Use config_test_data if available, otherwise use a default location + q_value = config_test_data.get("q", "London") + + params = {"q": q_value} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + # Verify response contains expected top-level keys + response_json = response.json() + assert "location" in response_json, "Response missing 'location' field" + assert "forecast" in response_json, "Response missing 'forecast' field" + + @pytest.mark.smoke + def test_future_weather_table_driven( + self, api_client, endpoint_test_data, schema_validator, config_test_data + ): + """ + Table-driven test for Future Weather API using data from future_json.json. + + Iterates through all test scenarios defined in the JSON file and validates + each response against the expected status code and schema. + """ + for test_case in endpoint_test_data: + scenario = test_case.get("scenario", "Unknown scenario") + expected_status = test_case.get("statusCode", 200) + + # Build params from test case, using config_test_data as fallback for required fields + params = {} + + # 'q' is required - get from test case or config_test_data + if "q" in test_case: + params["q"] = test_case["q"] + elif "q" in config_test_data: + params["q"] = config_test_data["q"] + + # Add optional parameters if present in test case + if "dt" in test_case: + params["dt"] = test_case["dt"] + if "lang" in test_case: + params["lang"] = test_case["lang"] + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == expected_status, ( + f"Scenario: {scenario} - Expected status {expected_status}, " + f"got {response.status_code}. Response: {response.text}" + ) + + # Validate response schema for successful responses + if expected_status == 200: + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Scenario: {scenario} - Schema validation failed: " + f"{validation_result.get('message', 'Unknown error')}" + ) + + response_json = response.json() + assert "location" in response_json, ( + f"Scenario: {scenario} - Response missing 'location' field" + ) + assert "forecast" in response_json, ( + f"Scenario: {scenario} - Response missing 'forecast' field" + ) + + +class TestFutureWeatherOptionalParams: + """Test class for Future Weather API with optional parameters.""" + + def test_future_weather_with_dt_parameter( + self, api_client, config_test_data, schema_validator + ): + """ + Test Future Weather API with optional 'dt' (date) parameter. + + The date should be between 14 and 300 days from today. + """ + from datetime import datetime, timedelta + + # Calculate a valid future date (30 days from now) + future_date = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d") + + q_value = config_test_data.get("q", "London") + params = { + "q": q_value, + "dt": future_date + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}. " + f"Response: {response.text}" + ) + + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_future_weather_with_lang_parameter( + self, api_client, config_test_data, schema_validator + ): + """ + Test Future Weather API with optional 'lang' parameter. + + The lang parameter affects the 'condition:text' field language. + """ + q_value = config_test_data.get("q", "London") + params = { + "q": q_value, + "lang": "es" # Spanish + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}. " + f"Response: {response.text}" + ) + + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_future_weather_with_all_parameters( + self, api_client, config_test_data, schema_validator + ): + """ + Test Future Weather API with all parameters (required and optional). + """ + from datetime import datetime, timedelta + + future_date = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d") + q_value = config_test_data.get("q", "London") + + params = { + "q": q_value, + "dt": future_date, + "lang": "en" + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}. " + f"Response: {response.text}" + ) + + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + +class TestFutureWeatherBadRequest: + """Test class for 400 Bad Request scenarios.""" + + def test_missing_required_q_parameter(self, api_client, schema_validator): + """ + Test API response when required 'q' parameter is missing. + + Expected: 400 Bad Request with error code 1003 + Note: Without authentication, this might return 401 instead. + """ + params = {} # Missing required 'q' parameter + + response = api_client.get(ENDPOINT, params=params) + + # API might return 400 or 401 depending on validation order + assert response.status_code in [400, 401], ( + f"Expected status code 400 or 401, got {response.status_code}. " + f"Response: {response.text}" + ) + + if response.status_code == 400: + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + response_json = response.json() + error_data = response_json.get("error", response_json) + assert "code" in error_data or "message" in error_data, ( + "Error response should contain 'code' or 'message' field" + ) + + def test_invalid_location_parameter(self, api_client, schema_validator): + """ + Test API response with invalid location value. + + Expected: 400 Bad Request with error code 1006 (no location found) + """ + params = {"q": "InvalidLocationXYZ123456789"} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 400, ( + f"Expected status code 400, got {response.status_code}. " + f"Response: {response.text}" + ) + + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_empty_q_parameter(self, api_client, schema_validator): + """ + Test API response when 'q' parameter is empty string. + + Expected: 400 Bad Request + """ + params = {"q": ""} + + response = api_client.get(ENDPOINT, params=params) + + # Empty q should result in 400 (parameter not provided or invalid) + assert response.status_code == 400, ( + f"Expected status code 400, got {response.status_code}. " + f"Response: {response.text}" + ) + + +class TestFutureWeatherEdgeCases: + """Test class for edge cases and boundary conditions.""" + + def test_q_parameter_with_coordinates( + self, api_client, config_test_data, schema_validator + ): + """ + Test API with latitude/longitude coordinates as 'q' parameter. + """ + params = {"q": "40.71,-74.01"} # New York coordinates + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}. " + f"Response: {response.text}" + ) + + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_q_parameter_with_zipcode( + self, api_client, config_test_data, schema_validator + ): + """ + Test API with US zipcode as 'q' parameter. + """ + params = {"q": "10001"} # New York zipcode + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}. " + f"Response: {response.text}" + ) + + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_q_parameter_with_ip_address( + self, api_client, config_test_data, schema_validator + ): + """ + Test API with IP address as 'q' parameter. + """ + params = {"q": "auto:ip"} # Auto-detect based on IP + + response = api_client.get(ENDPOINT, params=params) + + # This should work with valid API key + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_date_boundary_minimum( + self, api_client, config_test_data, schema_validator + ): + """ + Test API with date at minimum boundary (14 days from today). + """ + from datetime import datetime, timedelta + + min_date = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d") + q_value = config_test_data.get("q", "London") + + params = { + "q": q_value, + "dt": min_date + } + + response = api_client.get(ENDPOINT, params=params) + + # Should succeed with valid date in range + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_date_boundary_maximum( + self, api_client, config_test_data, schema_validator + ): + """ + Test API with date at maximum boundary (300 days from today). + """ + from datetime import datetime, timedelta + + max_date = (datetime.now() + timedelta(days=300)).strftime("%Y-%m-%d") + q_value = config_test_data.get("q", "London") + + params = { + "q": q_value, + "dt": max_date + } + + response = api_client.get(ENDPOINT, params=params) + + # Should succeed with valid date in range + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_date_out_of_range_past( + self, api_client, config_test_data, schema_validator + ): + """ + Test API with date less than 14 days from today (out of range). + """ + from datetime import datetime, timedelta + + past_date = (datetime.now() + timedelta(days=5)).strftime("%Y-%m-%d") + q_value = config_test_data.get("q", "London") + + params = { + "q": q_value, + "dt": past_date + } + + response = api_client.get(ENDPOINT, params=params) + + # Date out of range should return 400 + assert response.status_code == 400, ( + f"Expected status code 400 for out-of-range date, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_invalid_date_format( + self, api_client, config_test_data, schema_validator + ): + """ + Test API with invalid date format. + + Expected format is yyyy-MM-dd. + """ + q_value = config_test_data.get("q", "London") + + params = { + "q": q_value, + "dt": "15-01-2024" # Invalid format (dd-MM-yyyy instead of yyyy-MM-dd) + } + + response = api_client.get(ENDPOINT, params=params) + + # Invalid date format should return 400 + assert response.status_code == 400, ( + f"Expected status code 400 for invalid date format, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_special_characters_in_location( + self, api_client, schema_validator + ): + """ + Test API with special characters in location parameter. + """ + params = {"q": "São Paulo"} # Location with special character + + response = api_client.get(ENDPOINT, params=params) + + # Should handle special characters properly + assert response.status_code in [200, 400], ( + f"Expected status code 200 or 400, got {response.status_code}. " + f"Response: {response.text}" + ) + + +class TestFutureWeatherResponseValidation: + """Test class for detailed response structure validation.""" + + def test_location_object_structure( + self, api_client, config_test_data, schema_validator + ): + """ + Validate the structure of the 'location' object in response. + """ + q_value = config_test_data.get("q", "London") + params = {"q": q_value} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}" + ) + + response_json = response.json() + location = response_json.get("location", {}) + + # Verify expected location fields exist + expected_fields = ["name", "region", "country", "lat", "lon", "tz_id", "localtime"] + for field in expected_fields: + assert field in location, f"Location missing expected field: {field}" + + def test_forecast_object_structure( + self, api_client, config_test_data, schema_validator + ): + """ + Validate the structure of the 'forecast' object in response. + """ + q_value = config_test_data.get("q", "London") + params = {"q": q_value} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}" + ) + + response_json = response.json() + forecast = response_json.get("forecast", {}) + + assert "forecastday" in forecast, "Forecast missing 'forecastday' field" + assert isinstance(forecast["forecastday"], list), "forecastday should be a list" + + def test_forecastday_contains_required_fields( + self, api_client, config_test_data, schema_validator + ): + """ + Validate that forecastday items contain required fields. + """ + q_value = config_test_data.get("q", "London") + params = {"q": q_value} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status code 200, got {response.status_code}" + ) + + response_json = response.json() + forecastday = response_json.get("forecast", {}).get("forecastday", []) + + if forecastday: + day_item = forecastday[0] + expected_fields = ["date", "day", "astro", "hour"] + for field in expected_fields: + assert field in day_item, f"Forecastday item missing field: {field}" + + +class TestFutureWeatherUnauthorized: + """Test class for 401 Unauthorized scenarios (invalid API key).""" + + def test_invalid_api_key(self, api_host, config_test_data): + """ + Test API response with invalid API key. + + Expected: 401 Unauthorized with error code 2006 + """ + from conftest import APIClient + + # Create client with invalid API key + invalid_auth = {"ApiKeyAuth": "invalid_api_key_12345"} + client = APIClient(host=api_host, auth=invalid_auth) + + q_value = config_test_data.get("q", "London") + params = {"q": q_value} + + response = client.get(ENDPOINT, params=params) + + assert response.status_code == 401, ( + f"Expected status code 401, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_missing_api_key(self, api_host, config_test_data): + """ + Test API response when API key is not provided. + + Expected: 401 Unauthorized with error code 1002 + """ + from conftest import APIClient + + # Create client without API key + client = APIClient(host=api_host, auth={}) + + q_value = config_test_data.get("q", "London") + params = {"q": q_value} + + response = client.get(ENDPOINT, params=params) + + assert response.status_code == 401, ( + f"Expected status code 401, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_empty_api_key(self, api_host, config_test_data): + """ + Test API response when API key is empty string. + + Expected: 401 Unauthorized + """ + from conftest import APIClient + + # Create client with empty API key + empty_auth = {"ApiKeyAuth": ""} + client = APIClient(host=api_host, auth=empty_auth) + + q_value = config_test_data.get("q", "London") + params = {"q": q_value} + + response = client.get(ENDPOINT, params=params) + + assert response.status_code == 401, ( + f"Expected status code 401, got {response.status_code}. " + f"Response: {response.text}" + ) diff --git a/tests/WEATHER_API/test_history_json_get.py b/tests/WEATHER_API/test_history_json_get.py new file mode 100644 index 000000000..116dce6cb --- /dev/null +++ b/tests/WEATHER_API/test_history_json_get.py @@ -0,0 +1,742 @@ +# ********RoostGPT******** + +# Test generated by RoostGPT for test API-test-file using AI Type Claude AI and AI Model claude-opus-4-5-20251101 +# +# Test file generated for /history.json_get for http method type GET +# RoostTestHash=6ba88fbe10 +# +# + +# ********RoostGPT******** +""" +Test Suite for Weather API History Endpoint + +This module contains comprehensive pytest tests for the /v1/history.json endpoint. +The tests cover various scenarios including successful responses, error handling, +and schema validation. + +Setup: + 1. Ensure config.yml is properly configured with API host and authentication + 2. Set environment variables: API_HOST, key (for ApiKeyAuth) + 3. Place history_json.json in the same directory as this test file + 4. Install dependencies: pytest, requests, pyyaml, jsonschema + +Execution: + pytest test_history.py -v + pytest test_history.py -v -m smoke # Run only smoke tests +""" + +import json +import os +import pytest +from pathlib import Path + +# Import validator from validator.py +from validator import SwaggerSchemaValidator + + +# Constants +ENDPOINT = "/v1/history.json" +API_SPEC_PATH = "api.json" + + +@pytest.fixture(scope="module") +def endpoint_test_data(): + """Load test data from history_json.json file.""" + test_data_path = Path(__file__).parent / "history_json.json" + with open(test_data_path, "r") as f: + return json.load(f) + + +@pytest.fixture(scope="module") +def schema_validator(): + """Initialize the SwaggerSchemaValidator with the API spec.""" + spec_path = Path(__file__).parent / API_SPEC_PATH + return SwaggerSchemaValidator(str(spec_path)) + + +class TestHistoryAPISuccess: + """Test class for successful History API responses.""" + + @pytest.mark.smoke + def test_history_with_required_params_only(self, api_client, config_test_data, endpoint_test_data, schema_validator): + """ + Test History API with only required parameters (q and dt). + This is a smoke test covering the happy path with minimal required data. + """ + # Use first test case from endpoint_test_data for required params + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + expected_status = test_case.get("statusCode", 200) + assert response.status_code == expected_status, ( + f"Expected status {expected_status}, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", str(expected_status), response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + @pytest.mark.smoke + @pytest.mark.parametrize("test_case_index", range(2)) + def test_history_table_driven(self, api_client, endpoint_test_data, schema_validator, test_case_index): + """ + Table-driven test for History API using data from history_json.json. + Iterates through all test cases in the JSON file. + """ + if test_case_index >= len(endpoint_test_data): + pytest.skip(f"Test case index {test_case_index} out of range") + + test_case = endpoint_test_data[test_case_index] + + # Build params from test case - only include keys that exist + params = {} + + # Required parameters + if "q" in test_case: + params["q"] = test_case["q"] + if "dt" in test_case: + params["dt"] = test_case["dt"] + + # Optional parameters + if "unixdt" in test_case: + params["unixdt"] = test_case["unixdt"] + if "end_dt" in test_case: + params["end_dt"] = test_case["end_dt"] + if "unixend_dt" in test_case: + params["unixend_dt"] = test_case["unixend_dt"] + if "hour" in test_case: + params["hour"] = test_case["hour"] + if "lang" in test_case: + params["lang"] = test_case["lang"] + + response = api_client.get(ENDPOINT, params=params) + + expected_status = test_case.get("statusCode", 200) + assert response.status_code == expected_status, ( + f"Scenario: {test_case.get('scenario', 'Unknown')}. " + f"Expected status {expected_status}, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate response schema for successful responses + if expected_status == 200: + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", str(expected_status), response + ) + assert validation_result["valid"], ( + f"Schema validation failed for scenario '{test_case.get('scenario')}': " + f"{validation_result.get('message', 'Unknown error')}" + ) + + # Validate response contains expected structure + response_json = response.json() + assert "location" in response_json, "Response should contain 'location' object" + assert "forecast" in response_json, "Response should contain 'forecast' object" + + def test_history_with_all_optional_params(self, api_client, endpoint_test_data, schema_validator): + """ + Test History API with all optional parameters included. + Uses test case that has optional parameters defined. + """ + # Find test case with optional parameters + test_case = None + for tc in endpoint_test_data: + if "end_dt" in tc or "hour" in tc or "lang" in tc: + test_case = tc + break + + if test_case is None: + pytest.skip("No test case with optional parameters found") + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt") + } + + # Add optional parameters if present + if "unixdt" in test_case: + params["unixdt"] = test_case["unixdt"] + if "end_dt" in test_case: + params["end_dt"] = test_case["end_dt"] + if "unixend_dt" in test_case: + params["unixend_dt"] = test_case["unixend_dt"] + if "hour" in test_case: + params["hour"] = test_case["hour"] + if "lang" in test_case: + params["lang"] = test_case["lang"] + + response = api_client.get(ENDPOINT, params=params) + + expected_status = test_case.get("statusCode", 200) + assert response.status_code == expected_status, ( + f"Expected status {expected_status}, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", str(expected_status), response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + +class TestHistoryAPIValidation: + """Test class for History API validation and error scenarios.""" + + def test_missing_required_param_q(self, api_client, endpoint_test_data, schema_validator): + """ + Test that missing required parameter 'q' returns 400 error. + Error code 1003: Parameter 'q' not provided. + """ + test_case = endpoint_test_data[0] + + # Only provide 'dt', missing 'q' + params = { + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + # API spec indicates 400 for missing 'q' parameter + assert response.status_code == 400, ( + f"Expected status 400 for missing 'q' param, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Error schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + # Verify error message content + response_json = response.json() + assert "error" in response_json or "code" in response_json or "message" in response_json, ( + "Error response should contain error details" + ) + + def test_missing_required_param_dt(self, api_client, endpoint_test_data, schema_validator): + """ + Test that missing required parameter 'dt' returns 400 error. + """ + test_case = endpoint_test_data[0] + + # Only provide 'q', missing 'dt' + params = { + "q": test_case.get("q") + } + + response = api_client.get(ENDPOINT, params=params) + + # API spec indicates 400 for invalid/missing parameters + assert response.status_code == 400, ( + f"Expected status 400 for missing 'dt' param, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_missing_all_required_params(self, api_client, schema_validator): + """ + Test that missing all required parameters returns appropriate error. + Note: Without 'q' parameter, API returns 400 (Error code 1003). + """ + params = {} + + response = api_client.get(ENDPOINT, params=params) + + # Missing required params should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for missing all required params, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_invalid_location_param_q(self, api_client, endpoint_test_data, schema_validator): + """ + Test that invalid location in 'q' parameter returns 400 error. + Error code 1006: No location found matching parameter 'q'. + """ + test_case = endpoint_test_data[0] + + params = { + "q": "InvalidLocationXYZ12345NonExistent", + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + # Invalid location should return 400 (Error code 1006) + assert response.status_code == 400, ( + f"Expected status 400 for invalid location, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Error schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_invalid_date_format(self, api_client, endpoint_test_data, schema_validator): + """ + Test that invalid date format in 'dt' parameter returns 400 error. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": "invalid-date-format" + } + + response = api_client.get(ENDPOINT, params=params) + + # Invalid date format should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for invalid date format, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_date_before_minimum_allowed(self, api_client, endpoint_test_data, schema_validator): + """ + Test that date before 1st Jan 2010 returns appropriate error. + API spec states: Date on or after 1st Jan, 2010. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": "2009-12-31" # Date before minimum allowed + } + + response = api_client.get(ENDPOINT, params=params) + + # Date before minimum should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for date before minimum, got {response.status_code}. " + f"Response: {response.text}" + ) + + +class TestHistoryAPIBoundaryConditions: + """Test class for boundary conditions and edge cases.""" + + def test_hour_boundary_minimum(self, api_client, endpoint_test_data, schema_validator): + """ + Test hour parameter with minimum valid value (0 for midnight). + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt"), + "hour": 0 # Minimum valid hour + } + + response = api_client.get(ENDPOINT, params=params) + + # Valid hour should return 200 + assert response.status_code == 200, ( + f"Expected status 200 for valid hour=0, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_hour_boundary_maximum(self, api_client, endpoint_test_data, schema_validator): + """ + Test hour parameter with maximum valid value (23 for 11 PM). + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt"), + "hour": 23 # Maximum valid hour + } + + response = api_client.get(ENDPOINT, params=params) + + # Valid hour should return 200 + assert response.status_code == 200, ( + f"Expected status 200 for valid hour=23, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_hour_invalid_above_maximum(self, api_client, endpoint_test_data, schema_validator): + """ + Test hour parameter with value above maximum (24 or higher). + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt"), + "hour": 25 # Invalid hour above maximum + } + + response = api_client.get(ENDPOINT, params=params) + + # Invalid hour should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for invalid hour=25, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_hour_invalid_negative(self, api_client, endpoint_test_data, schema_validator): + """ + Test hour parameter with negative value. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt"), + "hour": -1 # Invalid negative hour + } + + response = api_client.get(ENDPOINT, params=params) + + # Invalid hour should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for invalid hour=-1, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_end_dt_greater_than_dt(self, api_client, endpoint_test_data, schema_validator): + """ + Test that end_dt greater than dt works correctly. + """ + # Find test case with end_dt + test_case = None + for tc in endpoint_test_data: + if "end_dt" in tc: + test_case = tc + break + + if test_case is None: + pytest.skip("No test case with end_dt found") + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt"), + "end_dt": test_case.get("end_dt") + } + + response = api_client.get(ENDPOINT, params=params) + + expected_status = test_case.get("statusCode", 200) + assert response.status_code == expected_status, ( + f"Expected status {expected_status}, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_end_dt_before_dt(self, api_client, endpoint_test_data, schema_validator): + """ + Test that end_dt before dt returns error. + API spec states: 'end_dt' should be greater than 'dt' parameter. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": "2024-02-20", + "end_dt": "2024-02-15" # end_dt before dt + } + + response = api_client.get(ENDPOINT, params=params) + + # Invalid date range should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for end_dt before dt, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_date_range_exceeds_30_days(self, api_client, endpoint_test_data, schema_validator): + """ + Test that date range exceeding 30 days returns error. + API spec states: difference should not be more than 30 days. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": "2024-01-01", + "end_dt": "2024-02-15" # More than 30 days difference + } + + response = api_client.get(ENDPOINT, params=params) + + # Date range exceeding 30 days should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for date range > 30 days, got {response.status_code}. " + f"Response: {response.text}" + ) + + +class TestHistoryAPIResponseStructure: + """Test class for validating response structure and content.""" + + @pytest.mark.smoke + def test_response_contains_location_object(self, api_client, endpoint_test_data, schema_validator): + """ + Test that successful response contains location object with expected fields. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status 200, got {response.status_code}. Response: {response.text}" + ) + + response_json = response.json() + + # Validate location object exists + assert "location" in response_json, "Response should contain 'location' object" + + location = response_json["location"] + + # Validate expected location fields based on API spec + expected_location_fields = ["name", "region", "country", "lat", "lon", "tz_id", "localtime"] + for field in expected_location_fields: + assert field in location, f"Location should contain '{field}' field" + + @pytest.mark.smoke + def test_response_contains_forecast_object(self, api_client, endpoint_test_data, schema_validator): + """ + Test that successful response contains forecast object with expected structure. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status 200, got {response.status_code}. Response: {response.text}" + ) + + response_json = response.json() + + # Validate forecast object exists + assert "forecast" in response_json, "Response should contain 'forecast' object" + + forecast = response_json["forecast"] + + # Validate forecastday array exists + assert "forecastday" in forecast, "Forecast should contain 'forecastday' array" + assert isinstance(forecast["forecastday"], list), "'forecastday' should be an array" + assert len(forecast["forecastday"]) > 0, "'forecastday' array should not be empty" + + def test_forecastday_contains_expected_fields(self, api_client, endpoint_test_data, schema_validator): + """ + Test that forecastday objects contain expected fields. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status 200, got {response.status_code}. Response: {response.text}" + ) + + response_json = response.json() + forecastday = response_json["forecast"]["forecastday"][0] + + # Validate expected forecastday fields based on API spec + expected_fields = ["date", "day", "astro", "hour"] + for field in expected_fields: + assert field in forecastday, f"Forecastday should contain '{field}' field" + + def test_day_object_contains_weather_data(self, api_client, endpoint_test_data, schema_validator): + """ + Test that day object contains weather data fields. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status 200, got {response.status_code}. Response: {response.text}" + ) + + response_json = response.json() + day = response_json["forecast"]["forecastday"][0]["day"] + + # Validate expected day fields based on API spec + expected_day_fields = [ + "maxtemp_c", "maxtemp_f", "mintemp_c", "mintemp_f", + "avgtemp_c", "avgtemp_f", "condition" + ] + for field in expected_day_fields: + assert field in day, f"Day object should contain '{field}' field" + + def test_condition_object_structure(self, api_client, endpoint_test_data, schema_validator): + """ + Test that condition object has expected structure. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status 200, got {response.status_code}. Response: {response.text}" + ) + + response_json = response.json() + condition = response_json["forecast"]["forecastday"][0]["day"]["condition"] + + # Validate condition fields based on API spec + expected_condition_fields = ["text", "icon", "code"] + for field in expected_condition_fields: + assert field in condition, f"Condition should contain '{field}' field" + + +class TestHistoryAPILocationTypes: + """Test class for different location input types.""" + + def test_location_by_city_name(self, api_client, endpoint_test_data, schema_validator): + """ + Test History API with city name as location. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), # City name from test data + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + expected_status = test_case.get("statusCode", 200) + assert response.status_code == expected_status, ( + f"Expected status {expected_status}, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_location_by_coordinates(self, api_client, endpoint_test_data, schema_validator): + """ + Test History API with latitude/longitude coordinates. + """ + test_case = endpoint_test_data[0] + + params = { + "q": "51.5074,-0.1278", # London coordinates + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + # Coordinates should work and return 200 + assert response.status_code == 200, ( + f"Expected status 200 for coordinates, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_location_by_ip_address(self, api_client, endpoint_test_data, schema_validator): + """ + Test History API with IP address as location. + """ + test_case = endpoint_test_data[0] + + params = { + "q": "auto:ip", # Auto-detect by IP + "dt": test_case.get("dt") + } + + response = api_client.get(ENDPOINT, params=params) + + # IP-based location should work and return 200 + assert response.status_code == 200, ( + f"Expected status 200 for IP location, got {response.status_code}. " + f"Response: {response.text}" + ) + + +class TestHistoryAPILanguageSupport: + """Test class for language parameter support.""" + + def test_language_parameter_english(self, api_client, endpoint_test_data, schema_validator): + """ + Test History API with English language parameter. + """ + # Find test case with lang parameter or use first one + test_case = None + for tc in endpoint_test_data: + if "lang" in tc: + test_case = tc + break + + if test_case is None: + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt"), + "lang": "en" + } + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status 200 for lang=en, got {response.status_code}. " + f"Response: {response.text}" + ) + + def test_language_parameter_invalid(self, api_client, endpoint_test_data, schema_validator): + """ + Test History API with invalid language parameter. + """ + test_case = endpoint_test_data[0] + + params = { + "q": test_case.get("q"), + "dt": test_case.get("dt"), + "lang": "invalid_lang_code_xyz" + } + + response = api_client.get(ENDPOINT, params=params) + + # Invalid language might still return 200 with default language + # or return 400 depending on API implementation + assert response.status_code in [200, 400], ( + f"Expected status 200 or 400 for invalid lang, got {response.status_code}. " + f"Response: {response.text}" + ) diff --git a/tests/WEATHER_API/test_ip_json_get.py b/tests/WEATHER_API/test_ip_json_get.py new file mode 100644 index 000000000..eabf491af --- /dev/null +++ b/tests/WEATHER_API/test_ip_json_get.py @@ -0,0 +1,333 @@ +# ********RoostGPT******** + +# Test generated by RoostGPT for test API-test-file using AI Type Claude AI and AI Model claude-opus-4-5-20251101 +# +# Test file generated for /ip.json_get for http method type GET +# RoostTestHash=5693e62dd1 +# +# + +# ********RoostGPT******** +""" +IP Lookup API Test Suite + +This module contains comprehensive pytest tests for the IP Lookup API endpoint. +The tests cover various scenarios including successful lookups, error handling, +and schema validation. + +Setup: + 1. Ensure config.yml is properly configured with API host and authentication + 2. Set environment variables: API_HOST, key (for ApiKeyAuth) + 3. Place ip_json.json in the same directory as this test file + 4. Run tests: pytest test_ip_lookup.py -v + 5. Run smoke tests only: pytest test_ip_lookup.py -v -m smoke +""" + +import json +import os +import pytest +from pathlib import Path +from validator import SwaggerSchemaValidator + + +# Load test data from JSON file +def load_endpoint_test_data(): + """Load test data from ip_json.json file.""" + test_data_path = Path(__file__).parent / "ip_json.json" + with open(test_data_path, "r") as f: + return json.load(f) + + +ENDPOINT_TEST_DATA = load_endpoint_test_data() + +# API endpoint constant +IP_LOOKUP_ENDPOINT = "/v1/ip.json" + +# Initialize schema validator +SPEC_PATH = Path(__file__).parent / "api.json" + + +@pytest.fixture(scope="module") +def schema_validator(): + """Initialize SwaggerSchemaValidator with API spec.""" + return SwaggerSchemaValidator(str(SPEC_PATH)) + + +@pytest.fixture(scope="function") +def test_data_from_json(): + """Fixture to provide test data from JSON file.""" + return ENDPOINT_TEST_DATA + + +class TestIPLookupSuccess: + """Test class for successful IP Lookup API scenarios.""" + + @pytest.mark.smoke + @pytest.mark.parametrize( + "test_case", + ENDPOINT_TEST_DATA, + ids=[f"scenario_{i}_{tc['scenario']}" for i, tc in enumerate(ENDPOINT_TEST_DATA)] + ) + def test_ip_lookup_success_scenarios( + self, api_client, schema_validator, test_case + ): + """ + Test IP Lookup API with valid parameters from JSON test data. + + This test iterates through all test cases defined in ip_json.json + and validates successful responses against the API schema. + """ + # Prepare request parameters from test data + params = {"q": test_case["q"]} + expected_status = test_case["statusCode"] + + # Make API request + response = api_client.get(IP_LOOKUP_ENDPOINT, params=params) + + # Assert status code matches expected + assert response.status_code == expected_status, ( + f"Expected status {expected_status}, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + IP_LOOKUP_ENDPOINT, + "get", + str(expected_status), + response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + @pytest.mark.smoke + def test_ip_lookup_with_valid_ip_address(self, api_client, schema_validator): + """ + Test IP Lookup API with a valid IP address. + + Uses the first test case from JSON data to ensure required parameter is provided. + """ + # Use first test case data for required parameter + test_case = ENDPOINT_TEST_DATA[0] + params = {"q": test_case["q"]} + + response = api_client.get(IP_LOOKUP_ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status 200, got {response.status_code}. Response: {response.text}" + ) + + # Validate response contains expected fields + response_json = response.json() + expected_fields = ["ip", "type", "continent_code", "continent_name", + "country_code", "country_name", "city", "lat", "lon"] + + for field in expected_fields: + assert field in response_json, f"Expected field '{field}' not found in response" + + # Validate schema + validation_result = schema_validator.validate_schema_by_response( + IP_LOOKUP_ENDPOINT, "get", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + +class TestIPLookupErrorHandling: + """Test class for IP Lookup API error scenarios.""" + + def test_ip_lookup_missing_required_param_q(self, api_client, schema_validator): + """ + Test IP Lookup API without required 'q' parameter. + + Expected: 400 Bad Request with error code 1003 + """ + # Make request without 'q' parameter + response = api_client.get(IP_LOOKUP_ENDPOINT, params={}) + + # API should return 400 for missing required parameter + assert response.status_code == 400, ( + f"Expected status 400 for missing 'q' param, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + IP_LOOKUP_ENDPOINT, "get", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + # Verify error response structure + response_json = response.json() + assert "error" in response_json or "code" in response_json, ( + "Error response should contain 'error' or 'code' field" + ) + + def test_ip_lookup_empty_q_parameter(self, api_client, schema_validator): + """ + Test IP Lookup API with empty 'q' parameter. + + Expected: 400 Bad Request + """ + params = {"q": ""} + + response = api_client.get(IP_LOOKUP_ENDPOINT, params=params) + + # Empty parameter should result in 400 + assert response.status_code == 400, ( + f"Expected status 400 for empty 'q' param, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + IP_LOOKUP_ENDPOINT, "get", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + def test_ip_lookup_invalid_location(self, api_client, schema_validator): + """ + Test IP Lookup API with invalid/non-existent location. + + Expected: 400 Bad Request with error code 1006 + """ + params = {"q": "invalidlocation12345xyz"} + + response = api_client.get(IP_LOOKUP_ENDPOINT, params=params) + + # Invalid location should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for invalid location, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + IP_LOOKUP_ENDPOINT, "get", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + +class TestIPLookupBoundaryConditions: + """Test class for boundary conditions and edge cases.""" + + @pytest.mark.parametrize("special_input", [ + " ", # Whitespace only + "!@#$%^&*()", # Special characters + "a" * 500, # Very long string + ]) + def test_ip_lookup_special_inputs( + self, api_client, schema_validator, special_input + ): + """ + Test IP Lookup API with special/edge case inputs. + + These inputs should result in 400 Bad Request as they are not valid + IP addresses or locations. + """ + params = {"q": special_input} + + response = api_client.get(IP_LOOKUP_ENDPOINT, params=params) + + # Special inputs should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for special input '{special_input[:20]}...', " + f"got {response.status_code}. Response: {response.text}" + ) + + def test_ip_lookup_numeric_input(self, api_client, schema_validator): + """ + Test IP Lookup API with numeric-only input that's not a valid IP. + + Expected: 400 Bad Request + """ + params = {"q": "12345"} + + response = api_client.get(IP_LOOKUP_ENDPOINT, params=params) + + # Numeric input that's not a valid IP should return 400 + assert response.status_code == 400, ( + f"Expected status 400 for numeric input, got {response.status_code}. " + f"Response: {response.text}" + ) + + +class TestIPLookupResponseValidation: + """Test class for detailed response validation.""" + + @pytest.mark.smoke + def test_ip_lookup_response_field_types(self, api_client, schema_validator): + """ + Test that IP Lookup API response fields have correct types. + + Uses test data from JSON file for the request. + """ + test_case = ENDPOINT_TEST_DATA[0] + params = {"q": test_case["q"]} + + response = api_client.get(IP_LOOKUP_ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected status 200, got {response.status_code}. Response: {response.text}" + ) + + response_json = response.json() + + # Validate field types based on API spec + if "ip" in response_json: + assert isinstance(response_json["ip"], str), "Field 'ip' should be string" + + if "type" in response_json: + assert isinstance(response_json["type"], str), "Field 'type' should be string" + + if "lat" in response_json and response_json["lat"] is not None: + assert isinstance(response_json["lat"], (int, float)), "Field 'lat' should be number" + + if "lon" in response_json and response_json["lon"] is not None: + assert isinstance(response_json["lon"], (int, float)), "Field 'lon' should be number" + + if "geoname_id" in response_json and response_json["geoname_id"] is not None: + assert isinstance(response_json["geoname_id"], int), "Field 'geoname_id' should be integer" + + if "localtime_epoch" in response_json and response_json["localtime_epoch"] is not None: + assert isinstance(response_json["localtime_epoch"], int), "Field 'localtime_epoch' should be integer" + + def test_ip_lookup_all_test_data_scenarios( + self, api_client, schema_validator, test_data_from_json + ): + """ + Test IP Lookup API iterating through all test data scenarios. + + This test validates each scenario from the JSON test data file. + """ + for idx, test_case in enumerate(test_data_from_json): + params = {"q": test_case["q"]} + expected_status = test_case["statusCode"] + scenario = test_case["scenario"] + + response = api_client.get(IP_LOOKUP_ENDPOINT, params=params) + + assert response.status_code == expected_status, ( + f"Scenario {idx} '{scenario}': Expected status {expected_status}, " + f"got {response.status_code}. Response: {response.text}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + IP_LOOKUP_ENDPOINT, + "get", + str(expected_status), + response + ) + assert validation_result["valid"], ( + f"Scenario {idx} '{scenario}': Schema validation failed: " + f"{validation_result.get('message')}" + ) diff --git a/tests/WEATHER_API/test_marine_json_get.py b/tests/WEATHER_API/test_marine_json_get.py new file mode 100644 index 000000000..68f1699ef --- /dev/null +++ b/tests/WEATHER_API/test_marine_json_get.py @@ -0,0 +1,487 @@ +# ********RoostGPT******** + +# Test generated by RoostGPT for test API-test-file using AI Type Claude AI and AI Model claude-opus-4-5-20251101 +# +# Test file generated for /marine.json_get for http method type GET +# RoostTestHash=fb78c63880 +# +# + +# ********RoostGPT******** +""" +Marine Weather API Test Suite + +This module contains comprehensive pytest tests for the Marine Weather API endpoint. +Tests cover success scenarios, error handling, and schema validation. + +Setup: +1. Ensure config.yml is properly configured with API host and authentication +2. Place marine_json.json test data file in the same directory as this test file +3. Run tests with: pytest test_marine.py -v +4. Run smoke tests only: pytest test_marine.py -v -m smoke +""" + +import json +import os +import pytest +from pathlib import Path + +from validator import SwaggerSchemaValidator + + +# Load test data from JSON file +def load_endpoint_test_data(): + """Load test data from marine_json.json file.""" + test_data_path = Path(__file__).parent / "marine_json.json" + with open(test_data_path, "r") as f: + return json.load(f) + + +ENDPOINT_TEST_DATA = load_endpoint_test_data() + +# API endpoint constant +MARINE_ENDPOINT = "/v1/marine.json" + +# Initialize schema validator +SPEC_PATH = Path(__file__).parent / "api.json" + + +@pytest.fixture(scope="module") +def schema_validator(): + """Create schema validator instance for the module.""" + return SwaggerSchemaValidator(str(SPEC_PATH)) + + +@pytest.fixture(scope="function") +def marine_test_data(config_test_data): + """ + Fixture to provide test data for marine endpoint. + Merges config_test_data with endpoint-specific test data. + """ + return config_test_data.copy() + + +class TestMarineWeatherAPISuccess: + """Test class for successful Marine Weather API responses.""" + + @pytest.mark.smoke + @pytest.mark.parametrize("test_case", ENDPOINT_TEST_DATA) + def test_marine_weather_success_scenarios( + self, api_client, schema_validator, marine_test_data, test_case + ): + """ + Test Marine Weather API with various success scenarios from test data. + + This test iterates through all test cases in marine_json.json and validates: + - Response status code matches expected + - Response schema is valid according to OpenAPI spec + """ + # Build query parameters from test case + params = {} + + # Required parameter: q + if "q" in test_case: + params["q"] = test_case["q"] + + # Required parameter: days (default to 1 if not provided in test case) + if "days" in test_case: + params["days"] = test_case["days"] + else: + # days is required, provide default + params["days"] = 1 + + # Optional parameters + if "dt" in test_case: + params["dt"] = test_case["dt"] + if "unixdt" in test_case: + params["unixdt"] = test_case["unixdt"] + if "hour" in test_case: + params["hour"] = test_case["hour"] + if "lang" in test_case: + params["lang"] = test_case["lang"] + + expected_status = test_case.get("statusCode", 200) + scenario = test_case.get("scenario", "Unknown scenario") + + # Make API request + response = api_client.get(MARINE_ENDPOINT, params=params) + + # Assert status code + assert response.status_code == expected_status, ( + f"Scenario: {scenario} - Expected status {expected_status}, " + f"got {response.status_code}. Response: {response.text}" + ) + + # Validate response schema for success responses + if expected_status == 200: + validation_result = schema_validator.validate_schema_by_response( + MARINE_ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed for scenario '{scenario}': " + f"{validation_result.get('message', 'Unknown error')}" + ) + + # Verify response contains expected structure + response_json = response.json() + assert "location" in response_json, "Response should contain 'location' object" + assert "forecast" in response_json, "Response should contain 'forecast' object" + + +class TestMarineWeatherAPIRequiredParams: + """Test class for required parameter validation.""" + + def test_missing_q_parameter_returns_400(self, api_client, schema_validator): + """ + Test that missing 'q' parameter returns 400 error. + + According to API spec, 'q' is required and missing it should return + error code 1003: Parameter 'q' not provided. + """ + params = {"days": 1} # Missing required 'q' parameter + + response = api_client.get(MARINE_ENDPOINT, params=params) + + # API returns 400 for missing required parameter 'q' + assert response.status_code == 400, ( + f"Expected 400 for missing 'q' parameter, got {response.status_code}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + MARINE_ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Error response schema validation failed: {validation_result.get('message')}" + ) + + # Verify error structure + response_json = response.json() + assert "error" in response_json or "code" in response_json or "message" in response_json + + def test_missing_days_parameter_returns_400(self, api_client, schema_validator): + """ + Test that missing 'days' parameter returns 400 error. + + According to API spec, 'days' is required. + """ + params = {"q": "48.8567,2.3508"} # Missing required 'days' parameter + + response = api_client.get(MARINE_ENDPOINT, params=params) + + # API should return 400 for missing required parameter + assert response.status_code == 400, ( + f"Expected 400 for missing 'days' parameter, got {response.status_code}" + ) + + +class TestMarineWeatherAPIInvalidParams: + """Test class for invalid parameter validation.""" + + def test_invalid_location_returns_400(self, api_client, schema_validator): + """ + Test that invalid location returns 400 error. + + Error code 1006: No location found matching parameter 'q' + """ + params = { + "q": "invalid_location_xyz_123", + "days": 1 + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + # API returns 400 for invalid location + assert response.status_code == 400, ( + f"Expected 400 for invalid location, got {response.status_code}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + MARINE_ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Error response schema validation failed: {validation_result.get('message')}" + ) + + @pytest.mark.parametrize("invalid_days", [0, 8, -1, 100]) + def test_invalid_days_parameter_boundary( + self, api_client, schema_validator, invalid_days + ): + """ + Test boundary conditions for 'days' parameter. + + According to API spec, days should be between 1 and 7. + Values outside this range should return 400. + """ + params = { + "q": "48.8567,2.3508", + "days": invalid_days + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + # API should return 400 for invalid days value + assert response.status_code == 400, ( + f"Expected 400 for days={invalid_days}, got {response.status_code}" + ) + + def test_invalid_hour_parameter(self, api_client, schema_validator): + """ + Test invalid hour parameter (outside 0-23 range). + """ + params = { + "q": "48.8567,2.3508", + "days": 1, + "hour": 25 # Invalid hour + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + # API should handle invalid hour gracefully + # Could return 400 or ignore invalid hour + assert response.status_code in [200, 400], ( + f"Expected 200 or 400 for invalid hour, got {response.status_code}" + ) + + def test_invalid_date_format(self, api_client, schema_validator): + """ + Test invalid date format for 'dt' parameter. + + Date should be in yyyy-MM-dd format. + """ + params = { + "q": "48.8567,2.3508", + "days": 1, + "dt": "22-07-2024" # Invalid format (should be 2024-07-22) + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + # API should return 400 for invalid date format + assert response.status_code == 400, ( + f"Expected 400 for invalid date format, got {response.status_code}" + ) + + +class TestMarineWeatherAPIOptionalParams: + """Test class for optional parameter handling.""" + + def test_with_dt_parameter(self, api_client, schema_validator): + """ + Test Marine API with optional 'dt' parameter. + """ + params = { + "q": "48.8567,2.3508", + "days": 1, + "dt": "2024-01-15" + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + # Should return 200 or 400 depending on date validity + assert response.status_code in [200, 400], ( + f"Unexpected status code: {response.status_code}" + ) + + def test_with_hour_parameter(self, api_client, schema_validator): + """ + Test Marine API with optional 'hour' parameter. + """ + params = { + "q": "48.8567,2.3508", + "days": 1, + "hour": 12 + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200 with hour parameter, got {response.status_code}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + MARINE_ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + def test_with_lang_parameter(self, api_client, schema_validator): + """ + Test Marine API with optional 'lang' parameter. + """ + params = { + "q": "48.8567,2.3508", + "days": 1, + "lang": "en" + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200 with lang parameter, got {response.status_code}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + MARINE_ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + @pytest.mark.parametrize("days_value", [1, 2, 3, 4, 5, 6, 7]) + def test_valid_days_range(self, api_client, schema_validator, days_value): + """ + Test all valid values for 'days' parameter (1-7). + """ + params = { + "q": "48.8567,2.3508", + "days": days_value + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200 for days={days_value}, got {response.status_code}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + MARINE_ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed for days={days_value}: " + f"{validation_result.get('message')}" + ) + + +class TestMarineWeatherAPIResponseStructure: + """Test class for validating response structure.""" + + def test_location_object_structure(self, api_client, schema_validator): + """ + Test that location object contains expected fields. + """ + params = { + "q": "48.8567,2.3508", + "days": 1 + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + assert response.status_code == 200 + + response_json = response.json() + location = response_json.get("location", {}) + + # Verify location fields exist + expected_fields = ["name", "region", "country", "lat", "lon", "tz_id", "localtime"] + for field in expected_fields: + assert field in location, f"Location should contain '{field}' field" + + def test_forecast_object_structure(self, api_client, schema_validator): + """ + Test that forecast object contains expected structure. + """ + params = { + "q": "48.8567,2.3508", + "days": 1 + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + assert response.status_code == 200 + + response_json = response.json() + forecast = response_json.get("forecast", {}) + + # Verify forecast structure + assert "forecastday" in forecast, "Forecast should contain 'forecastday' array" + + forecastday = forecast.get("forecastday", []) + assert isinstance(forecastday, list), "forecastday should be an array" + + if forecastday: + day_data = forecastday[0] + assert "date" in day_data, "Forecast day should contain 'date'" + assert "day" in day_data, "Forecast day should contain 'day' object" + assert "hour" in day_data, "Forecast day should contain 'hour' array" + + +class TestMarineWeatherAPIEdgeCases: + """Test class for edge cases and boundary conditions.""" + + def test_empty_q_parameter(self, api_client, schema_validator): + """ + Test with empty 'q' parameter. + """ + params = { + "q": "", + "days": 1 + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + # Should return 400 for empty q parameter + assert response.status_code == 400, ( + f"Expected 400 for empty q parameter, got {response.status_code}" + ) + + def test_special_characters_in_q(self, api_client, schema_validator): + """ + Test with special characters in 'q' parameter. + """ + params = { + "q": "", + "days": 1 + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + # Should return 400 for invalid location + assert response.status_code == 400, ( + f"Expected 400 for special characters, got {response.status_code}" + ) + + def test_latitude_longitude_format(self, api_client, schema_validator): + """ + Test with latitude/longitude coordinate format. + """ + params = { + "q": "48.8567,2.3508", # Paris coordinates + "days": 1 + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200 for lat/lon format, got {response.status_code}" + ) + + # Validate response schema + validation_result = schema_validator.validate_schema_by_response( + MARINE_ENDPOINT, "GET", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + def test_hour_boundary_values(self, api_client, schema_validator): + """ + Test hour parameter with boundary values (0 and 23). + """ + for hour in [0, 23]: + params = { + "q": "48.8567,2.3508", + "days": 1, + "hour": hour + } + + response = api_client.get(MARINE_ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Expected 200 for hour={hour}, got {response.status_code}" + ) diff --git a/tests/WEATHER_API/test_search_json_get.py b/tests/WEATHER_API/test_search_json_get.py new file mode 100644 index 000000000..f433c6b77 --- /dev/null +++ b/tests/WEATHER_API/test_search_json_get.py @@ -0,0 +1,387 @@ +# ********RoostGPT******** + +# Test generated by RoostGPT for test API-test-file using AI Type Claude AI and AI Model claude-opus-4-5-20251101 +# +# Test file generated for /search.json_get for http method type GET +# RoostTestHash=72ffe69306 +# +# + +# ********RoostGPT******** +""" +Test Suite for WeatherAPI Search/Autocomplete API + +Endpoint: /v1/search.json +Method: GET +Security: ApiKeyAuth (query parameter) + +Setup: +1. Ensure config.yml has valid API host and ApiKeyAuth key +2. Set environment variables API_HOST and key if needed +3. Place search_json.json in the same directory as this test file +4. Run tests with: pytest test_search.py -v + +Response Status Codes: +- 200: OK - Returns array of location objects +- 400: Bad Request - Missing/invalid 'q' parameter +- 401: Unauthorized - Missing/invalid API key +- 403: Forbidden - API key quota exceeded or disabled +""" + +import json +import os +import pytest +from pathlib import Path +from validator import SwaggerSchemaValidator + + +# Load test data from JSON file +TEST_DATA_FILE = Path(__file__).parent / "search_json.json" + + +def load_endpoint_test_data(): + """Load test data from search_json.json file.""" + with open(TEST_DATA_FILE, "r") as f: + return json.load(f) + + +ENDPOINT_TEST_DATA = load_endpoint_test_data() + +# API endpoint constant +ENDPOINT = "/v1/search.json" + + +@pytest.fixture(scope="module") +def validator(): + """Initialize schema validator with API spec.""" + spec_path = Path(__file__).parent / "api.json" + return SwaggerSchemaValidator(str(spec_path)) + + +@pytest.fixture(scope="function") +def test_scenarios(): + """Provide test scenarios from JSON file.""" + return ENDPOINT_TEST_DATA + + +class TestSearchAutocompleteAPI: + """Test class for Search/Autocomplete API endpoint.""" + + @pytest.mark.smoke + def test_search_success_scenarios(self, api_client, validator, config_test_data): + """ + Test successful search responses using table-driven data. + + Iterates through all test scenarios from search_json.json + and validates 200 OK responses. + """ + for scenario in ENDPOINT_TEST_DATA: + if scenario.get("statusCode") == 200: + # Use q from test data + params = {"q": scenario["q"]} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200, ( + f"Scenario '{scenario.get('scenario')}' failed: " + f"Expected 200, got {response.status_code}" + ) + + # Validate response schema + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed for scenario '{scenario.get('scenario')}': " + f"{validation_result.get('message')}" + ) + + # Validate response is an array + response_data = response.json() + assert isinstance(response_data, list), ( + f"Expected array response for scenario '{scenario.get('scenario')}'" + ) + + # Validate each location object has required fields + if response_data: + for location in response_data: + assert "id" in location, "Location missing 'id' field" + assert "name" in location, "Location missing 'name' field" + assert "region" in location, "Location missing 'region' field" + assert "country" in location, "Location missing 'country' field" + assert "lat" in location, "Location missing 'lat' field" + assert "lon" in location, "Location missing 'lon' field" + assert "url" in location, "Location missing 'url' field" + + def test_search_missing_required_param_q(self, api_client, validator): + """ + Test 400 error when required 'q' parameter is not provided. + + Error code 1003: Parameter 'q' not provided. + """ + # Make request without 'q' parameter + response = api_client.get(ENDPOINT, params={}) + + assert response.status_code == 400, ( + f"Expected 400 for missing 'q' param, got {response.status_code}" + ) + + # Validate error response schema + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + # Validate error response structure + error_data = response.json() + assert "error" in error_data or "code" in error_data or "message" in error_data + + def test_search_empty_q_parameter(self, api_client, validator): + """ + Test 400 error when 'q' parameter is empty. + + Error code 1003 or 1006 expected. + """ + params = {"q": ""} + + response = api_client.get(ENDPOINT, params=params) + + # Empty q should return 400 + assert response.status_code == 400, ( + f"Expected 400 for empty 'q' param, got {response.status_code}" + ) + + # Validate error response schema + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + def test_search_no_location_found(self, api_client, validator): + """ + Test 400 error when no location matches the query. + + Error code 1006: No location found matching parameter 'q' + """ + # Use a query that is unlikely to match any location + params = {"q": "xyznonexistentlocation12345"} + + response = api_client.get(ENDPOINT, params=params) + + # No matching location should return 400 with error code 1006 + assert response.status_code == 400, ( + f"Expected 400 for non-existent location, got {response.status_code}" + ) + + # Validate error response schema + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + @pytest.mark.parametrize("query_value", [ + "51.52,-0.12", # Latitude/Longitude + "10001", # US Zipcode + ]) + def test_search_various_query_formats(self, api_client, validator, query_value): + """ + Test search with various query formats as per API documentation. + + Supports: US Zipcode, UK Postcode, Canada Postalcode, + IP address, Latitude/Longitude, city name + """ + params = {"q": query_value} + + response = api_client.get(ENDPOINT, params=params) + + # Should return 200 for valid query formats + assert response.status_code in [200, 400], ( + f"Unexpected status code {response.status_code} for query '{query_value}'" + ) + + if response.status_code == 200: + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + def test_search_special_characters_in_query(self, api_client, validator): + """ + Test search with special characters in query parameter. + + Boundary test for edge case input handling. + """ + params = {"q": "São Paulo"} # City with special characters + + response = api_client.get(ENDPOINT, params=params) + + # Should handle special characters gracefully + assert response.status_code in [200, 400], ( + f"Unexpected status code {response.status_code}" + ) + + if response.status_code == 200: + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", "200", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message')}" + ) + + def test_search_whitespace_only_query(self, api_client, validator): + """ + Test 400 error when 'q' parameter contains only whitespace. + """ + params = {"q": " "} + + response = api_client.get(ENDPOINT, params=params) + + # Whitespace-only should be treated as invalid + assert response.status_code == 400, ( + f"Expected 400 for whitespace-only 'q', got {response.status_code}" + ) + + def test_search_response_array_structure(self, api_client, validator): + """ + Test that successful response returns properly structured array. + """ + # Use first successful scenario from test data + success_scenario = next( + (s for s in ENDPOINT_TEST_DATA if s.get("statusCode") == 200), + None + ) + + if success_scenario is None: + pytest.skip("No success scenario in test data") + + params = {"q": success_scenario["q"]} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == 200 + + response_data = response.json() + assert isinstance(response_data, list), "Response should be an array" + + # Validate each item structure + for item in response_data: + assert isinstance(item.get("id"), int), "id should be integer" + assert isinstance(item.get("name"), str), "name should be string" + assert isinstance(item.get("region"), str), "region should be string" + assert isinstance(item.get("country"), str), "country should be string" + assert isinstance(item.get("lat"), (int, float)), "lat should be number" + assert isinstance(item.get("lon"), (int, float)), "lon should be number" + assert isinstance(item.get("url"), str), "url should be string" + + def test_search_all_scenarios_from_json(self, api_client, validator): + """ + Table-driven test iterating through all scenarios in search_json.json. + """ + for scenario in ENDPOINT_TEST_DATA: + expected_status = scenario.get("statusCode") + query = scenario.get("q") + scenario_name = scenario.get("scenario", "Unknown scenario") + + params = {"q": query} + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code == expected_status, ( + f"Scenario '{scenario_name}': Expected {expected_status}, " + f"got {response.status_code}" + ) + + # Validate response schema based on status code + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", str(expected_status), response + ) + assert validation_result["valid"], ( + f"Scenario '{scenario_name}' schema validation failed: " + f"{validation_result.get('message')}" + ) + + +class TestSearchAPIBoundaryConditions: + """Test boundary conditions for Search API.""" + + def test_search_very_long_query(self, api_client, validator): + """ + Test behavior with very long query string. + + Boundary test for maximum input length handling. + """ + # Create a very long query string + long_query = "London" * 100 + params = {"q": long_query} + + response = api_client.get(ENDPOINT, params=params) + + # Should handle gracefully - either 200 with no results or 400 + assert response.status_code in [200, 400], ( + f"Unexpected status code {response.status_code} for long query" + ) + + def test_search_numeric_query(self, api_client, validator): + """ + Test search with numeric-only query (zipcode format). + """ + params = {"q": "90210"} # Beverly Hills zipcode + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code in [200, 400], ( + f"Unexpected status code {response.status_code}" + ) + + if response.status_code == 200: + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", "200", response + ) + assert validation_result["valid"] + + def test_search_ip_address_query(self, api_client, validator): + """ + Test search with IP address as query. + """ + params = {"q": "8.8.8.8"} # Google DNS IP + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code in [200, 400], ( + f"Unexpected status code {response.status_code}" + ) + + if response.status_code == 200: + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", "200", response + ) + assert validation_result["valid"] + + def test_search_coordinates_query(self, api_client, validator): + """ + Test search with latitude/longitude coordinates. + """ + params = {"q": "48.8566,2.3522"} # Paris coordinates + + response = api_client.get(ENDPOINT, params=params) + + assert response.status_code in [200, 400], ( + f"Unexpected status code {response.status_code}" + ) + + if response.status_code == 200: + validation_result = validator.validate_schema_by_response( + ENDPOINT, "get", "200", response + ) + assert validation_result["valid"] + + response_data = response.json() + assert isinstance(response_data, list) diff --git a/tests/WEATHER_API/test_timezone_json_get.py b/tests/WEATHER_API/test_timezone_json_get.py new file mode 100644 index 000000000..713f1885d --- /dev/null +++ b/tests/WEATHER_API/test_timezone_json_get.py @@ -0,0 +1,410 @@ +# ********RoostGPT******** + +# Test generated by RoostGPT for test API-test-file using AI Type Claude AI and AI Model claude-opus-4-5-20251101 +# +# Test file generated for /timezone.json_get for http method type GET +# RoostTestHash=df6b1e5ce9 +# +# + +# ********RoostGPT******** +""" +Test Suite for Time Zone API Endpoint + +This module contains comprehensive pytest tests for the /v1/timezone.json endpoint. +Tests cover successful responses, error scenarios, and schema validation. + +Setup: + 1. Ensure config.yml is properly configured with API host and authentication + 2. Set environment variables: API_HOST, key (for ApiKeyAuth) + 3. Place timezone_json.json in the same directory as this test file + 4. Install dependencies: pytest, requests, pyyaml, jsonschema + +Execution: + pytest test_timezone.py -v + pytest test_timezone.py -v -m smoke # Run only smoke tests +""" + +import json +import os +import pytest +from pathlib import Path + +# Import validator from validator.py +from validator import SwaggerSchemaValidator + +# Constants +ENDPOINT = "/v1/timezone.json" +API_SPEC_PATH = "api.json" + +# Load test data from JSON file +TEST_DATA_FILE = Path(__file__).parent / "timezone_json.json" + + +@pytest.fixture(scope="module") +def endpoint_test_data(): + """Load endpoint-specific test data from JSON file.""" + with open(TEST_DATA_FILE, "r") as f: + return json.load(f) + + +@pytest.fixture(scope="module") +def schema_validator(): + """Initialize the SwaggerSchemaValidator with API spec.""" + return SwaggerSchemaValidator(API_SPEC_PATH) + + +@pytest.fixture(scope="function") +def test_params(config_test_data, endpoint_test_data, request): + """ + Merge config_test_data with endpoint_test_data for each test scenario. + Override default config values with scenario-specific values. + """ + # Get scenario index from test parameter if available + scenario_index = getattr(request, "param", 0) + scenario_data = endpoint_test_data[scenario_index] if scenario_index < len(endpoint_test_data) else {} + + # Merge config_test_data with scenario_data (scenario takes precedence) + merged = {**config_test_data, **scenario_data} + return merged + + +class TestTimezoneAPISuccess: + """Test class for successful Time Zone API responses.""" + + @pytest.mark.smoke + @pytest.mark.parametrize("scenario_index", range(2)) # Based on JSON file having 2 entries + def test_timezone_success_scenarios( + self, api_client, schema_validator, endpoint_test_data, config_test_data, scenario_index + ): + """ + Test successful timezone API responses using table-driven data. + + This test iterates through all scenarios defined in timezone_json.json + and validates: + - Response status code matches expected + - Response schema matches OpenAPI specification + """ + # Get scenario data from JSON file + scenario = endpoint_test_data[scenario_index] + + # Merge with config_test_data (scenario overrides defaults) + test_data = {**config_test_data, **scenario} + + # Extract required parameter from test data + query_param = test_data.get("q") + expected_status = test_data.get("statusCode") + scenario_name = test_data.get("scenario", "Unknown scenario") + + # Make API request with required parameter + params = {"q": query_param} + response = api_client.get(ENDPOINT, params=params) + + # Assert status code + assert response.status_code == expected_status, ( + f"Scenario: {scenario_name} - " + f"Expected status {expected_status}, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate response schema for successful responses + if expected_status == 200: + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", str(expected_status), response + ) + assert validation_result["valid"], ( + f"Schema validation failed for scenario '{scenario_name}': " + f"{validation_result.get('message', 'Unknown error')}" + ) + + # Verify response contains expected fields + response_json = response.json() + expected_fields = ["name", "region", "country", "lat", "lon", "tz_id", "localtime_epoch", "localtime"] + for field in expected_fields: + assert field in response_json, f"Missing expected field '{field}' in response" + + +class TestTimezoneAPIErrors: + """Test class for Time Zone API error responses.""" + + def test_missing_required_parameter_q(self, api_client, schema_validator): + """ + Test API response when required parameter 'q' is not provided. + + Expected: 400 Bad Request with error code 1003 + """ + # Make request without required 'q' parameter + response = api_client.get(ENDPOINT, params={}) + + # API returns 400 for missing required parameter + assert response.status_code == 400, ( + f"Expected status 400 for missing 'q' parameter, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + # Verify error response structure + response_json = response.json() + assert "error" in response_json or "code" in response_json or "message" in response_json, ( + "Error response should contain error details" + ) + + def test_invalid_location_parameter(self, api_client, schema_validator): + """ + Test API response when 'q' parameter contains invalid/non-existent location. + + Expected: 400 Bad Request with error code 1006 (No location found) + """ + # Use a clearly invalid location string + params = {"q": "xyznonexistentlocation12345"} + response = api_client.get(ENDPOINT, params=params) + + # API returns 400 for invalid location + assert response.status_code == 400, ( + f"Expected status 400 for invalid location, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_empty_q_parameter(self, api_client, schema_validator): + """ + Test API response when 'q' parameter is empty string. + + Expected: 400 Bad Request + """ + params = {"q": ""} + response = api_client.get(ENDPOINT, params=params) + + # Empty parameter should result in 400 error + assert response.status_code == 400, ( + f"Expected status 400 for empty 'q' parameter, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + +class TestTimezoneAPIAuthentication: + """Test class for authentication-related error responses.""" + + def test_missing_api_key(self, api_host, schema_validator): + """ + Test API response when API key is not provided. + + Expected: 401 Unauthorized with error code 1002 + """ + import requests + + # Make request without API key + url = f"{api_host.rstrip('/')}/{ENDPOINT.lstrip('/')}" + params = {"q": "London"} + response = requests.get(url, params=params, timeout=30) + + # API returns 401 for missing API key + assert response.status_code == 401, ( + f"Expected status 401 for missing API key, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "401", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_invalid_api_key(self, api_host, schema_validator): + """ + Test API response when invalid API key is provided. + + Expected: 401 Unauthorized with error code 2006 + """ + import requests + + # Make request with invalid API key + url = f"{api_host.rstrip('/')}/{ENDPOINT.lstrip('/')}" + params = {"q": "London", "key": "invalid_api_key_12345"} + response = requests.get(url, params=params, timeout=30) + + # API returns 401 for invalid API key + assert response.status_code == 401, ( + f"Expected status 401 for invalid API key, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "401", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + +class TestTimezoneAPIBoundaryConditions: + """Test class for boundary conditions and edge cases.""" + + @pytest.mark.parametrize("location_type,location_value", [ + ("zipcode", "10001"), # US Zipcode + ("postcode", "SW1A 1AA"), # UK Postcode + ("coordinates", "40.71,-74.01"), # Latitude/Longitude + ("ip_address", "8.8.8.8"), # IP address + ]) + def test_various_location_formats( + self, api_client, schema_validator, location_type, location_value + ): + """ + Test API with various supported location formats. + + The API supports: US Zipcode, UK Postcode, Canada Postalcode, + IP address, Latitude/Longitude, and city name. + """ + params = {"q": location_value} + response = api_client.get(ENDPOINT, params=params) + + # These should return either 200 (success) or 400 (location not found) + # depending on the validity of the location + assert response.status_code in [200, 400], ( + f"Unexpected status {response.status_code} for {location_type}: {location_value}. " + f"Response: {response.text}" + ) + + # Validate response schema based on status code + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", str(response.status_code), response + ) + assert validation_result["valid"], ( + f"Schema validation failed for {location_type}: " + f"{validation_result.get('message', 'Unknown error')}" + ) + + def test_special_characters_in_query(self, api_client, schema_validator): + """ + Test API behavior with special characters in query parameter. + + Expected: 400 Bad Request (invalid location) + """ + params = {"q": "!@#$%^&*()"} + response = api_client.get(ENDPOINT, params=params) + + # Special characters should result in 400 error + assert response.status_code == 400, ( + f"Expected status 400 for special characters, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + def test_very_long_query_parameter(self, api_client, schema_validator): + """ + Test API behavior with excessively long query parameter. + + Expected: 400 Bad Request + """ + # Create a very long string + long_query = "a" * 1000 + params = {"q": long_query} + response = api_client.get(ENDPOINT, params=params) + + # Long query should result in 400 error + assert response.status_code == 400, ( + f"Expected status 400 for very long query, got {response.status_code}. " + f"Response: {response.text}" + ) + + # Validate error response schema + validation_result = schema_validator.validate_schema_by_response( + ENDPOINT, "GET", "400", response + ) + assert validation_result["valid"], ( + f"Schema validation failed: {validation_result.get('message', 'Unknown error')}" + ) + + +class TestTimezoneAPIResponseValidation: + """Test class for detailed response validation.""" + + @pytest.mark.smoke + def test_response_data_types(self, api_client, endpoint_test_data, config_test_data): + """ + Test that response fields have correct data types as per API spec. + """ + # Use first scenario from test data + scenario = endpoint_test_data[0] + test_data = {**config_test_data, **scenario} + + params = {"q": test_data.get("q")} + response = api_client.get(ENDPOINT, params=params) + + if response.status_code == 200: + response_json = response.json() + + # Validate data types according to API spec + if "name" in response_json: + assert isinstance(response_json["name"], str), "name should be string" + if "region" in response_json: + assert isinstance(response_json["region"], str), "region should be string" + if "country" in response_json: + assert isinstance(response_json["country"], str), "country should be string" + if "lat" in response_json: + assert isinstance(response_json["lat"], (int, float)), "lat should be number" + if "lon" in response_json: + assert isinstance(response_json["lon"], (int, float)), "lon should be number" + if "tz_id" in response_json: + assert isinstance(response_json["tz_id"], str), "tz_id should be string" + if "localtime_epoch" in response_json: + assert isinstance(response_json["localtime_epoch"], int), "localtime_epoch should be integer" + if "localtime" in response_json: + assert isinstance(response_json["localtime"], str), "localtime should be string" + + @pytest.mark.smoke + def test_response_contains_location_data(self, api_client, endpoint_test_data, config_test_data): + """ + Test that successful response contains meaningful location data. + """ + # Use first scenario from test data + scenario = endpoint_test_data[0] + test_data = {**config_test_data, **scenario} + + params = {"q": test_data.get("q")} + response = api_client.get(ENDPOINT, params=params) + + if response.status_code == 200: + response_json = response.json() + + # Verify location coordinates are within valid ranges + if "lat" in response_json: + assert -90 <= response_json["lat"] <= 90, "Latitude should be between -90 and 90" + if "lon" in response_json: + assert -180 <= response_json["lon"] <= 180, "Longitude should be between -180 and 180" + + # Verify timezone ID is not empty + if "tz_id" in response_json: + assert len(response_json["tz_id"]) > 0, "tz_id should not be empty" diff --git a/tests/WEATHER_API/timezone_json.json b/tests/WEATHER_API/timezone_json.json new file mode 100644 index 000000000..c09f2c32b --- /dev/null +++ b/tests/WEATHER_API/timezone_json.json @@ -0,0 +1,52 @@ +[ + { + "q": "London", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "New York", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "48.8566,2.3522", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "Tokyo, Japan", + "statusCode": 200, + "scenario": "Successful responses: OK" + }, + { + "q": "", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "xyznonexistentlocation12345", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "!!!@@@###", + "statusCode": 400, + "scenario": "Client error responses: Bad Request" + }, + { + "q": "Sydney", + "statusCode": 401, + "scenario": "Client error responses: Unauthorized" + }, + { + "q": "Paris", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + }, + { + "q": "Berlin", + "statusCode": 403, + "scenario": "Client error responses: Forbidden" + } +] \ No newline at end of file diff --git a/tests/WEATHER_API/validator.py b/tests/WEATHER_API/validator.py new file mode 100644 index 000000000..2f965738e --- /dev/null +++ b/tests/WEATHER_API/validator.py @@ -0,0 +1,202 @@ + +import json +import yaml +from jsonschema import ( + Draft202012Validator, + Draft7Validator, + Draft4Validator, + ValidationError, +) +from referencing import Registry, Resource +from typing import Dict, Any +import requests + + +class SwaggerSchemaValidator: + """ + Validates JSON, XML, and text responses + """ + + def __init__(self, swagger_source: str): + self.spec = self._load_spec(swagger_source) + self.is_swagger2 = False + self.schemas = self._extract_schemas() + self.registry = Registry() + + for name, schema in self.schemas.items(): + pointer = ( + f"#/definitions/{name}" if self.is_swagger2 + else f"#/components/schemas/{name}" + ) + + wrapped = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + **schema, + } + self.registry = self.registry.with_resource( + pointer, + Resource.from_contents(wrapped) + ) + + def _load_spec(self, source: str) -> Dict[str, Any]: + if source.startswith(("http://", "https://")): + resp = requests.get(source) + resp.raise_for_status() + text = resp.text + + try: + return yaml.safe_load(text) + except yaml.YAMLError: + try: + return json.loads(text) + except json.JSONDecodeError: + raise ValueError("URL does not contain valid YAML or JSON") + + with open(source, "r") as f: + text = f.read() + + if source.endswith((".yaml", ".yml")): + return yaml.safe_load(text) + if source.endswith(".json"): + return json.loads(text) + + raise ValueError("File must be YAML or JSON") + + def _extract_schemas(self): + if "components" in self.spec and "schemas" in self.spec["components"]: + self.is_swagger2 = False + return self.spec["components"]["schemas"] + + if "definitions" in self.spec: + self.is_swagger2 = True + return self.spec["definitions"] + + raise ValueError("No schemas found under components/schemas or definitions") + + def get_version(self): + return self.spec.get("openapi") or self.spec.get("swagger") or "" + + def select_validator(self): + v = self.get_version() + + if v.startswith("2."): + return Draft4Validator + if v.startswith("3.0"): + return Draft7Validator + if v.startswith("3.1"): + return Draft202012Validator + + return Draft202012Validator + + def resolve_ref(self, ref): + if ref.startswith("#/"): + parts = ref.lstrip("#/").split("/") + node = self.spec + for p in parts: + node = node[p] + return node + + raise ValueError(f"External refs not supported: {ref}") + + def deref(self, schema): + if isinstance(schema, dict): + if "$ref" in schema: + resolved = self.resolve_ref(schema["$ref"]) + return self.deref(resolved) + return {k: self.deref(v) for k, v in schema.items()} + + if isinstance(schema, list): + return [self.deref(v) for v in schema] + + return schema + + def detect_format(self, response): + ctype = response.headers.get("Content-Type", "").lower() + if "json" in ctype: + return "json" + if "xml" in ctype: + return "xml" + if "text" in ctype: + return "text" + return "binary" + + def parse_body(self, response, fmt): + if fmt == "json": + return json.loads(response.text) + + if fmt == "xml": + import xmltodict + return xmltodict.parse(response.text) + + if fmt == "text": + return response.text + + return response.content + + def extract_schema_for_media_type(self, response_block, content_type): + content = response_block.get("content", {}) + + if content_type in content: + return content[content_type].get("schema") + + if "json" in content_type: + for k, v in content.items(): + if k == "application/json" or k.endswith("+json"): + return v.get("schema") + + if "xml" in content_type: + for k, v in content.items(): + if "xml" in k: + return v.get("schema") + + if "text/plain" in content: + return content["text/plain"].get("schema") + + return None + + + def validate_schema_by_response(self, endpoint, method, status_code, response): + fmt = self.detect_format(response) + + paths = self.spec.get("paths", {}) + op = paths.get(endpoint, {}).get(method.lower()) + + if not op: + return {"valid": False, "message": f"Method {method} not found at path {endpoint}"} + + responses = op.get("responses", {}) + response_block = responses.get(status_code) + + if not response_block: + return {"valid": False, "message": f"No response block for {status_code}"} + + ctype = response.headers.get("Content-Type", "").split(";")[0].strip() + + if "content" in response_block: + schema = self.extract_schema_for_media_type(response_block, ctype) + else: + schema = response_block.get("schema") + + if schema is None: + return {"valid": True, "message": "No schema defined for this content type"} + + try: + data = self.parse_body(response, fmt) + except Exception as e: + return {"valid": False, "message": f"Body parsing failed: {e}"} + + schema = self.deref(schema) + + validator_cls = self.select_validator() + validator = validator_cls(schema, registry=self.registry) + + try: + validator.validate(data) + return {"valid": True} + except ValidationError as e: + return { + "valid": False, + "message": e.message, + "path": list(e.path), + "schema_path": list(e.schema_path), + }