diff --git a/challenger/Procfile b/challenger/Procfile deleted file mode 100644 index a771bb7c..00000000 --- a/challenger/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: java $JAVA_OPTS -cp target/classes:target/dependency/*:target/* uk.co.compendiumdev.challenge.ChallengeMain -model=challengeapi -noshutdown -multiplayer -cloudstorage diff --git a/challenger/deploy.txt b/challenger/deploy.txt deleted file mode 100644 index 08c85ad8..00000000 --- a/challenger/deploy.txt +++ /dev/null @@ -1,32 +0,0 @@ -Build the full project - -`mvn package` - -Getting build errors? - -What does `mvn -version` tell you about the Java version being used? - -Switch to the right java version: - -export JAVA_HOME=`/usr/libexec/java_home -v 11.0` - -i.e. https://medium.com/@devkosal/switching-java-jdk-versions-on-macos-80bc868e686a - -For heroku: - -NOTE: need to have `heroku plugins:install java` for the `deploy:jar` to be recognised - more info https://devcenter.heroku.com/articles/deploying-executable-jar-files - -NOTE: if you can't deploy then check the Heroku Stack. Heroku prevents deployment if the stack is out of date, but doesn't generate an error message like "The stack is out of date, you cannot deploy". Instead it will write something like "ERROR: status code: 422, reason phrase: Unprocessable Entity". Check information on stack upgrades here https://devcenter.heroku.com/articles/upgrading-to-the-latest-stack but usually you just have to do `heroku stack:set heroku-22 -a apichallenges` - - -Deploy the challenger app full jar - -- `heroku login` -- have the deployment page open in case we need to rollback - - https://dashboard.heroku.com/apps/apichallenges/activity -- view the logs to watch release progress - - https://dashboard.heroku.com/apps/apichallenges/logs -- cd challenger -- `heroku deploy:jar target/apichallenges.jar --app apichallenges` -- change ChallengerAuto to point to apichallenges.eviltester.com and run the tests -- roll back on any issues diff --git a/challenger/docs/seo-metadata-frontmatter.md b/challenger/docs/seo-metadata-frontmatter.md deleted file mode 100644 index 0f331ea8..00000000 --- a/challenger/docs/seo-metadata-frontmatter.md +++ /dev/null @@ -1,61 +0,0 @@ -# SEO Front Matter Guide - -Use these front-matter fields for markdown pages under `resources/content/**`. - -## Recommended minimum - -```yaml -title: Human-friendly page title -seo_title: Search-focused page title -description: Meta description for search snippets -lastmod: 2026-02-18 -``` - -## Optional SEO and social fields - -```yaml -seo_description: Optional override for meta description text -meta_robots: index,follow # e.g. noindex,nofollow for non-public pages -canonical: https://apichallenges.eviltester.com/path -og_image: /images/social/apichallenges-og-1200x630.png -og_image_alt: Image alt text for social previews -og_type: article # use website for home/index style pages -twitter_card: summary_large_image -twitter_site: @your_handle -schema_type: Article # defaults to Article, WebPage for index template -schema_author: Your Name -schema_publisher: API Challenges -schema_image: /images/social/apichallenges-og-1200x630.png -schema_breadcrumb_enabled: true # set false to suppress BreadcrumbList JSON-LD -schema_howto_enabled: true # enable/disable HowTo generation for this page -schema_howto_steps: Step one||Step two||Step three -schema_video_enabled: true # set false to suppress VideoObject when a YouTube video is present -schema_video_id: OpisB0UZq0c # force specific YouTube id when needed -``` - -## Notes - -- If `seo_description` is not set, `description` is used. -- `lastmod` is used as the page update date for sitemap `` and schema `dateModified`. -- If `lastmod` is not set, schema `dateModified` falls back to `date`. -- If `og_image` is not set, the global default social image is used. -- If `meta_robots` is not set, `index,follow` is used. -- `seo_title` is used for the HTML `` tag; `title` remains the human-facing page label. -- If `schema_type` is not set, content pages default to `Article` and index pages default to `WebPage`. -- Default schema author and publisher are read from resource files: - - `src/main/resources/seo/schema-author.properties` - - `src/main/resources/seo/schema-publisher.properties` -- `schema_author` and `schema_publisher` in front matter override the resource defaults on a per-page basis. -- Page-level schema overrides are available for content-specific schema behavior: - - `schema_breadcrumb_enabled: false` disables `BreadcrumbList` - - `schema_howto_enabled: true|false` forces `HowTo` on/off - - `schema_howto_steps` provides explicit `HowToStep` names separated by `||` (required to emit HowTo) - - `schema_video_enabled: true|false` forces `VideoObject` on/off - - `schema_video_id` sets the YouTube id used in `VideoObject` -- Entity depth can be configured in resource files, e.g.: - - author: `jobTitle`, `sameAs` (comma separated) - - publisher: `legalName`, `contactType`, `email`, `telephone`, `sameAs` (comma separated) -- Content-specific schema is added automatically: - - `BreadcrumbList` for content pages - - `HowTo` only when `schema_howto_steps` is provided (recommended on solutions/tutorials) - - `VideoObject` when a YouTube video is detected in page content diff --git a/challenger/info/00_welcome.md b/challenger/info/00_welcome.md deleted file mode 100644 index 3dcf8639..00000000 --- a/challenger/info/00_welcome.md +++ /dev/null @@ -1,96 +0,0 @@ -## Welcome to the API Challenges - -Welcome to the API Challenges. - -A learning environment to experiment, explore, and play with API Testing. - -In this video I will quickly give you an overview of the features and how to use the API Challenges. - -## API Challenges GUI - -[show main index page, click on to entities explorer, and click through] - -You can see we have a GUI, with instructions, and we can view all the data in the system. - -The GUI is read only, because the main purpose is to explore the API for this application. - -## API Documentation - -[show documentation] - -The API is documented, listing the API End points and showing sample data. - -But like all API documentation we have to understand how APIs work to make sense of it, but the documentation does show the URIs we can call, the methods we can use, and the data formats used. - -## The Challenges - -[show challenges] - -We also have challenges. To help guide you through the process of exploring and experimenting with the API we have a set of challenges that you can try. This helps gamify the learning experience and you can see the progress you are making as you complete each challenge. - -## Multi-User - -This is a multi-user application. That means: - -- other people are using the same data -- so don't add any secret data -- don't use this to store any personal todo items -- and be considerate of other users, so only add family friendly text into the system - -We want this tool to be easily available to as many people to learn from as possible, but if the system is abused then we may have to revisit these policies. - -Also, because it is multi-user the system data resets itself every 10 minutes. - -## The API - -The API is accessible through normal API calls, and you probably want to use a standard API front end to interact with it. - -I tend to use Insomnia, PAW or PostMan but any API Front end should work. - -I can make API calls to the system to create and amend data. - -And if these calls are successful then I will see the results in the Entities Explorer view of the application as well. - -The API Documentation should give you the information you need to use the API. - -## The Challenges - -[show the challenges list] - -The challenges are a list of things to do with the API. - -And if you have downloaded the application and are running it locally then your progress against these challenges will be automatically tracked. - -If you are using the cloud hosted multi-user version, then you'll have to register a challenger session, and your challenge completion status will be stored on the cloud so you can revisit it. - -## What Next? - -That was just a quick overview. - -You can find more information at [eviltester.com/apichallenges](https://eviltester.com/apichallenges). - -Including download links and instructions to use the application locally on your machine without any interaction with other people, and this makes it easier to do the challenges. - -And there will be more information and examples of how to use the challenges and register as a challenger to track your progress. - -So, now, explore the site. Read the documentation. Experiment with the API. And practice your API Testing. - - -## Video - -> API Challenges - How to Learn API Testing - -[youtu.be/7HN9f5JLt0g](https://youtu.be/7HN9f5JLt0g) - -The API Challenges application helps you train and practice Testing REST APIs. - -Find the application links and information: - -- https://eviltester.com/apichallenges - - ---- - -Ad free version of video released to Patreon supporters: - -- https://www.patreon.com/posts/39769995 \ No newline at end of file diff --git a/challenger/info/10_how_to_register_cloud_challenger.md b/challenger/info/10_how_to_register_cloud_challenger.md deleted file mode 100644 index 8521f412..00000000 --- a/challenger/info/10_how_to_register_cloud_challenger.md +++ /dev/null @@ -1,105 +0,0 @@ -# How to Register as a Cloud Challenger - -If you are using the API Challenges locally you don't have to register as a challenger, that is done for you. - -But if you want to use the cloud hosted multi-user version then you need to register as a challenger. - -And to do that... - -## POST /challenger - -We need to make an API call to the `/challenger` uri. - -In my API testing tool. I will issue a POST request to `https://apichallenges.eviltester.com/challenger` - -- _Issue POST Request_ - -~~~~~~~~ -POST https://apichallenges.eviltester.com/challenger -~~~~~~~~ - -And if I receive a `201` response then that means that I have created a challenger session. - -- _View response_ - -~~~~~~~~ -Date: Tue, 21 Jul 2020 11:05:21 GMT -X-CHALLENGER: 9a32ef9f-0ebd-4b30-b975-314460bfd1d1 -Location: /gui/challenges/9a32ef9f-0ebd-4b30-b975-314460bfd1d1 -Content-Type: text/html;charset=utf-8 -Transfer-Encoding: chunked -Server: Jetty(9.4.12.v20180830) -~~~~~~~~ - -The data will be deleted from memory periodically. - -Progress can be stored in the browser local storage, or use the API to save and restore progress at your convenience. - -## Use the `X-CHALLENGER` header - -In the response there should be an `X-CHALLENGER` header. - -This is my unique session id, and if I use this as a header in all my requests then every challenge I complete will be tracked. - -I will make a request to the `/challenges` URI which lets me see the status of the challenges. - -And I will add the `X-CHALLENGER` header with the session I was just allocated. - -- _Issue GET /challenges with `X-CHALLENGER` header_ - -~~~~~~~~~ -GET https://apichallenges.eviltester.com/challenges -~~~~~~~~~ - -## See Status in the GUI - -I can see all the challenges in the GUI. - -- _visit https://apichallenges.eviltester.com/gui/challenges_ - -And if don't have a session then the GUI will tell me. - -I can visit the challenges status for my session. - -- _visit https://apichallenges.eviltester.com/gui/challenges/{guid}_ - -When I do this, my GUID will also be stored in local storage. - -This makes it easier for me to remember and restore my session. - -The challenger progress and todo data can also be saved in local storage and loaded back into the system later using the UI or the API. - -## Summary - -Basically to track my challenges in multi-user mode. - -- `POST /challenger` -- use the `X-CHALLENGER` header in all requests -- _visit https://apichallenges.eviltester.com/gui/challenges/{guid}_ -- save/restore the challenger progress and data in local storage from the GUI buttons - -You don't have to do any of this if you run the application locally in single user mode. But we want to try and make it easy for as many people as possible to experiment without installing anything locally. - -The sessions on the cloud will be deleted from memory when not used for 10 minutes. - ---- - -## Video - -> How to track your API Challenge status completion in multi-user mode - -[youtu.be/XBsM9f9xrhI](https://youtu.be/XBsM9f9xrhI) - -The API Challenges application for helping you train and practice in Testing REST APIs requires creating a session for tracking challenges in multi-user mode. - -- `POST /challenger` -- use the `X-CHALLENGER` header in all requests -- visit https://apichallenges.eviltester.com/gui/challenges/{guid} - -More information on the API Challenges can be found at: - -https://www.eviltester.com/apichallenges - ---- - -Ad free version of video released to Patreon supporters: https://www.patreon.com/posts/39770116 \ No newline at end of file diff --git a/challenger/info/20_how_to_download_run_single_player.md b/challenger/info/20_how_to_download_run_single_player.md deleted file mode 100644 index 61bf556a..00000000 --- a/challenger/info/20_how_to_download_run_single_player.md +++ /dev/null @@ -1,124 +0,0 @@ -# How to Download And Run Challenges Single Player Mode - -## Downloading - -To download: - -- Visit the releases page - - https://github.com/eviltester/thingifier/releases -- Look for the `apichallenger.jar` -- Download `apichallenger.jar` - -Alternative Route: - -- Visit https://eviltester.com/apichallenges -- there will be a link to the latest release -- click on that -- Download `apichallenger.jar` - -You can run this from anywhere, so copy the `.jar` file into a directory of your choosing. - -## Running - -We need to have java installed. And most computers should have this by default. - -So we'll assume you have it, and if you don't then download and install it from either of... - -- https://java.com -- https://adoptopenjdk.net - -In a terminal window, or a command prompt then run the app with `java -jar apichallenger.jar` - -We should see a bunch of information displayed in the command line, and one of the output lines is the URL that the application is running on. - -### Running on Windows - -- visit the folder you copied the `.jar` file in using Explorer -- in the path input field in explorer type `cmd` -- at the command line type `java -jar apichallenger.jar` - -### Running on Mac - -- open `terminal` -- use `cd` to navigate to the folder e.g. assuming you copied `apichallenger.jar` to a folder on your desktop - - `cd Desktop` - - `cd apichallenger` -- `java -jar apichallenger.jar` - -### Double Click - -If you double click the application, it will probably run but you won't see it running. - -If you visit the GUI then you'll see if it is working or not. - -### Visit The GUI - -- Open a browser -- `http://localhost:4567` -- you should see the GUI - -### Stopping the App - -In single user mode you can stop the app by: - -- `http://localhost:4567/shutdown` - -Or, `cntrl+c` at the command line to stop the running app. - -### Using the API - -Make your api calls to `http://localhost:4567/{API}` - -e.g. - -- `http://localhost:4567/todos` -- `http://localhost:4567/challenges` - -### Tracking Challenges - -Challenges are automatically tracked in single user mode. - -And written to a file: - -- ` rest-api-challenges-single-player.data.txt` - -Stored in the same directory as the `apichallenger.jar` - -This file will be automatically loaded when the application is started. - -You don't need to `POST /challenger`. - -### Example solving a challenge - -- If I use Insomnia, -- to make a GET request to `http://localhost:4567` -- then visit the GUI `http://localhost:4567/gui/challenges` -- then we should see the challenge that I just completed -- and if I close the app and start it again -- then my challenge status has been tracked in the file - - --- - -## Video - -> API Challenges - How to Run the Application locally - -[youtu.be/FIYKZiTDoAI](https://youtu.be/FIYKZiTDoAI) - -The API Challenges application has a cloud version, but because it is multi-user it can be useful to run locally so you don't compete with other people for data. - -This video shows how to: - -- download the jar file -- run the jar file -- stop the app - -With instructions for both Mac and Windows. - -Find the application links and more information at: - -- https://eviltester.com/apichallenges - - - - Patreon ad free video: https://www.patreon.com/posts/39849331 \ No newline at end of file diff --git a/challenger/info/challenges/debrief/post_challenger_201.md b/challenger/info/challenges/debrief/post_challenger_201.md deleted file mode 100644 index c014b63a..00000000 --- a/challenger/info/challenges/debrief/post_challenger_201.md +++ /dev/null @@ -1,121 +0,0 @@ -# POST /challenger (201) - -> Issue a POST request on the `/challenger` end point, with no body, to create a new challenger session. Use the generated X-CHALLENGER header in future requests to track challenge completion. - -- This challenge is essential if you want to persist your sessions in multi-user mode -- This challenge is optional if you want to work in single-user mode - -## Debrief - -### APIs are not always intuitive - -This is the first challenge that many people will do, because they want to save their challenges. Unfortunately it isn't the most natural place to start when learning about APIs. - -But that is a useful point to debrief on, because APIs are not always the most accessible way to learn and interact with an application. - -Because they are designed for 'programs', 'systems', or 'applications' to interact with, not necessarily people. - -And sometimes we have to use APIs in certain ways, because they tell us to. APIs don't usually offer as much flexibility in how we interact with them. Unlike GUIs which often have more than one way to achieve a result. With an API there is one way to do things. - -And so with the challenges app, if we want to persist our sessions then we have to comply with the process: - -- `POST /challenger` -- use the `X-CHALLENGER` header in all future requests. - -### What does this do? Why is this a POST? - -If I was describing this to a person, I might say: - -- "I need the system to give me an X-CHALLENGER code, so that I can track my challenge session" -- "I need to get an X-CHALLENGER code to track my challenges" - -What we are actually saying to the system is: - -- "I need you to create a challenger session, and tell me what it is" - -We are asking the system to make a change on the server side, in response to this request. - -We use a `POST` request to do that. - -If I used a `GET` request, and the system created a `challenger` session in response then that is not in the spirit of the HTTP specification. - -`GET` should be cacheable, and return information, not create information as a side-effect. - -`POST` is used to create information on the server. - -## Status Code - -- `201` Created -- What other status codes could be provided? - - API documentation doesn't say - - Assume: 405 - - https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 - - https://httpstatuses.com/405 - -## What are the headers in the response? - -The response has two important headers for us. - -- `X-CHALLENGER` -- `LOCATION` - -Notes: - -- `X-CHALLENGER` is a custom header. -- applications are free to create custom headers -- `X-...` used to be the standard recommended approach -- Based on this 'standard' it is now a deprecated recommendation, but still used - - https://tools.ietf.org/html/rfc6648 - - "this is a matter for the designers of those protocols" - -Notes: `Location` - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location -- used with 303 and 201 -- if a browser then it will follow Location header -- semantics are 'looser' for APIs - -## Cookies? - -- Cookies are not used in APIs as frequently as GUI -- Cookies can often be scraped from GUI and passed to API - - not always the best approach - - "The Pulper" practice app can use cookies to access the data, if you want to practice with cookies then try that. - - https://www.eviltester.com/page/tools/thepulper/ - - - -## Video - -> API Challenges - POST Challenger 201 Debrief - -[youtu.be/YJDyCow1QTE](https://youtu.be/YJDyCow1QTE) - -A debrief and lessons learned on the "POST Challenger 201" - -- What is a POST Request? -- How does POST Request differe from a GET request? -- How to identify a Custom HTTP Header? -- What is a Custom HTTP Header? -- Standards for Custom HTTP Headers? -- What is a 201 status code? -- Dangers of duplicate headers. -- How to learn about status codes? -- What is a Location header? - -Useful links: - -- https://tools.ietf.org/html/rfc6648 -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201 -- https://httpstatuses.com/201 -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 -- https://httpstatuses.com/405 -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location - -Find the application links and more information at: - -- https://eviltester.com/apichallenges - ---- - -Patreon ad free video version: https://www.patreon.com/posts/39882304 \ No newline at end of file diff --git a/challenger/info/challenges/how-to/post_challenger_201.md b/challenger/info/challenges/how-to/post_challenger_201.md deleted file mode 100644 index 50274caa..00000000 --- a/challenger/info/challenges/how-to/post_challenger_201.md +++ /dev/null @@ -1,95 +0,0 @@ -# POST /challenger (201) - -> Issue a POST request on the `/challenger` end point, with no body, to create a new challenger session. Use the generated X-CHALLENGER header in future requests to track challenge completion. - -- This challenge is essential if you want to persist your sessions in multi-user mode -- This challenge is optional if you want to work in single-user mode - -## CURL - -~~~~~~~~ -curl --request POST \ - --url http://localhost:4567/challenger \ - -v -~~~~~~~~ - -~~~~~~~~ -* Trying ::1... -* TCP_NODELAY set -* Connected to localhost (::1) port 4567 (#0) -> POST /challenger HTTP/1.1 -> Host: localhost:4567 -> User-Agent: curl/7.64.1 -> Accept: */* -> -< HTTP/1.1 201 Created -< Date: Tue, 28 Jul 2020 15:11:56 GMT -< X-CHALLENGER: rest-api-challenges-single-player -< Location: /gui/challenges -< Content-Type: text/html;charset=utf-8 -< Transfer-Encoding: chunked -< Server: Jetty(9.4.z-SNAPSHOT) -< -* Connection #0 to host localhost left intact -* Closing connection 0 -~~~~~~~~ - -## Basic Instructions - -- Issue a POST request to end point "/challenger" - - if running locally that would be - - 'http://localhost:4567/challenger' - - if running in the cloud that would be - - 'https://apichallenges.eviltester.com/challenger' -- The response will have an `X-CHALLENGER` header -- Use this in any future requests to track your progress -- The `LOCATION` header has a url to access your challenge status through the GUI - -## Insomnia - -~~~~~~~~ -POST /challenger HTTP/1.1 -Host: localhost:4567 -User-Agent: insomnia/6.5.4 -Accept: */* -Content-Length: 0 -~~~~~~~~ - - -~~~~~~~~ -HTTP/1.1 201 Created -Date: Tue, 28 Jul 2020 14:26:48 GMT -X-CHALLENGER: rest-api-challenges-single-player -Location: /gui/challenges -Content-Type: text/html;charset=utf-8 -Transfer-Encoding: chunked -Server: Jetty(9.4.z-SNAPSHOT) -~~~~~~~~ - - -_NOTE: the first version of apichallenger.jar had a bug where the `X-CHALLENGER` header would be repeated. In theory this wouldn't make any real difference, but these are the types of bugs which can cause applications issues, particularly if the values were different. It is always worth reading the headers carefully to make sure they are as we expect. This type of issue is more severe on the server side, so is worth testing for. It can cause the server side to only validate one of the headers, but potentially allow unvalidated data to be processed. This can also be quite hard to generate because often our tooling prevents us from generating invalid requests._ - - ---- - -## Video - -> API Challenges - How to Solve POST Challenger 201 in Insomnia - -[youtu.be/tNGuZMQgHxw](https://youtu.be/tNGuZMQgHxw) - -Learn how to solve the first API Challenge, which requires: - -- POST an HTTP Request -- identify a Custom HTTP Header -- Use a custom HTTP Header in a request - -This video shows how to complete the challenge using Insomnia. - -Find the application links and more information at: - -- https://eviltester.com/apichallenges - ---- - -Patreon ad free (Insomnia version): https://www.patreon.com/posts/39882254 \ No newline at end of file diff --git a/challenger/info/todo.md b/challenger/info/todo.md deleted file mode 100644 index a3ef2473..00000000 --- a/challenger/info/todo.md +++ /dev/null @@ -1,39 +0,0 @@ -- [Welcome - Overview](00_welcome.md) - -Multi-User - -- [How to Register on the Cloud](10_how_to_register_cloud_challenger.md) - -Single-Player - -- [How to download and run single player mode](20_how_to_download_run_single_player.md) - -Technology - -- What is...? - - -## Tooling - Insomnia - -- general tooling tips and videos - - create a workspace - - environment variables -- How to Solve with Insomnia - - [POST Challenger 201](how-to/post_challenger_201.md) - -- how to solve challenge X with tool X - -## Tooling - Postman - -## Tooling - etc. - -## Challenge Debriefs - -Debrief of challenge: - -- [POST Challenger 201](debrief/post_challenger_201.md) -- Debrief of challenge X - -## Automating - -- with library X ... \ No newline at end of file diff --git a/challenger/pom.xml b/challenger/pom.xml deleted file mode 100644 index 4c442e0c..00000000 --- a/challenger/pom.xml +++ /dev/null @@ -1,175 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - <parent> - <artifactId>thingifier-root</artifactId> - <groupId>uk.co.compendiumdev.thingifier</groupId> - <version>1.5.6-SNAPSHOT</version> - </parent> - <modelVersion>4.0.0</modelVersion> - - <artifactId>challenger</artifactId> - <version>1.1.0</version> - - <dependencies> - <dependency> - <groupId>uk.co.compendiumdev</groupId> - <artifactId>thingifier</artifactId> - <version>${thingifier.version}</version> - </dependency> - - <dependency> - <groupId>org.xerial</groupId> - <artifactId>sqlite-jdbc</artifactId> - <version>${sqlite-jdbc-version}</version> - </dependency> - - <dependency> - <groupId>io.swagger.core.v3</groupId> - <artifactId>swagger-models</artifactId> - <version>${swagger-models-version}</version> - </dependency> - <dependency> - <groupId>io.swagger.core.v3</groupId> - <artifactId>swagger-core</artifactId> - <version>${swagger-models-version}</version> - </dependency> - <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk --> - <!-- experimental cloud storage of challenger sessions --> - <!-- todo: split into two apps, one aws enabled, one with local persistance only --> - <!-- 180 meg! - --> - <!-- - <dependency> - <groupId>com.amazonaws</groupId> - <artifactId>aws-java-sdk-s3</artifactId> - <version>1.12.261</version> - </dependency> - --> - - <dependency> - <groupId>io.github.classgraph</groupId> - <artifactId>classgraph</artifactId> - <version>4.8.168</version> - </dependency> - - <dependency> - <groupId>org.commonmark</groupId> - <artifactId>commonmark</artifactId> - <version>0.21.0</version> - </dependency> - - <dependency> - <groupId>org.commonmark</groupId> - <artifactId>commonmark-ext-gfm-tables</artifactId> - <version>0.21.0</version> - </dependency> - - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-api</artifactId> - <version>${junit.jupiter.version}</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-engine</artifactId> - <version>${junit.jupiter.version}</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-params</artifactId> - <version>${junit.jupiter.version}</version> - <scope>test</scope> - </dependency> - - <!-- logging - -https://betterstack.com/community/guides/logging/how-to-start-logging-with-java/#2-mask-the-logging-framework-with-slf4j -https://www.slf4j.org/manual.html ---> - - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - <version>${slf4j-version}</version> - </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-jdk14</artifactId> - <version>${slf4j-version}</version> - </dependency> - - </dependencies> - - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-surefire-plugin</artifactId> - <version>3.0.0-M4</version> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <version>3.3</version> - <configuration> - <source>${java.version}</source> <!-- 11 required to use HttpClient --> - <target>${java.version}</target> - </configuration> - </plugin> - <plugin> - <artifactId>maven-assembly-plugin</artifactId> - <executions> - <execution> - <phase>package</phase> - <goals> - <goal>single</goal> - </goals> - </execution> - </executions> - <configuration> - <descriptorRefs> - <!-- This tells Maven to include all dependencies --> - <descriptorRef>jar-with-dependencies</descriptorRef> - </descriptorRefs> - <!-- rename the 'full' jar --> - <finalName>apichallenges</finalName> - <appendAssemblyId>false</appendAssemblyId> - <archive> - <manifest> - <mainClass>uk.co.compendiumdev.challenge.ChallengeMain</mainClass> - </manifest> - </archive> - </configuration> - </plugin> - </plugins> - </build> -</project> - -<!-- - -1.0.3 - -- fixed YouTube video for solution 24 (it was dupe of 23!) -- added all solution writeups and videos -- added mirror mode -- added simulation mode - -1.0.2 - -- Added Ids to the displayed challenges list to match the Solution tutorial numbers -- Challenges list now has hints and solution links to available blog posts and youtube videos - -1.0.1 - -- highlighting of completed challenges -- swagger file download from the API Documentation page - -1.0.0 - -- initial release - ---> diff --git a/challenger/readme.md b/challenger/readme.md deleted file mode 100644 index 7ae1ad92..00000000 --- a/challenger/readme.md +++ /dev/null @@ -1,75 +0,0 @@ -# API Self Teaching Challenges - -From the commandline, run the app: - -- `java -jar apichallenges.jar` - -This will start an API and GUI running on: - -- http://localhost:4567 - -If you visit the URL in a browser then you will see the GUI where you can: - -- read the API reference documentation -- browse the items in the application -- see the API Challenges - -You can run it in multi-user mode locally by using the command line parameter: - -`-multiplayer` - -e.g. - -- `java -jar apichallenges.jar -multiplayer` - -This could be useful for team workshops and learning sessions. - -## Practice Testing - -If you just want to practice testing and use it as an API for a todos application then you probably want to remove the limit on the number of todos. - -Do this by starting with the command line parameter: - -`-unlimitedtodos` - -## More information - -More information can be found at [eviltester.com/apichallenges](https://eviltester.com/apichallenges) - -## Challenges - -The challenges can be completed by issuing API requests to the API. - -e.g. `GET http://localhost:4567/todos` would complete the challenge to "GET the list of todos" - -You can also `GET http://localhost:4567/challenges` to get the list of challenges and their status as an API call. - -## Version Tracking - -20240411 - -- removed the S3 integration, so this is a breaking change for some people -- added the simple api in its own api path -- ui to surface all modes and sub info - -v1.1 - 20240128 - -- in multiuser mode now all the user sessions are independent with a limit of 100 todos -- there are learning tools like mirror mode and simulator - -v1.0 - 20200727 - -- ready for initial public release -- fix bug to allow tracking of x-challenger session as a challenge - -v0.1 - -- initial development of challenges -- s3 storage of multi-user cloud challenge sessions -- file persistence for local release - -## How Tos - -- [About the application](https://github.com/eviltester/thingifier/blob/master/challenger/info/00_welcome.md) -- [Creating a Multi-User Session](https://github.com/eviltester/thingifier/blob/master/challenger/info/10_how_to_register_cloud_challenger.md) - diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/CHALLENGE.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/CHALLENGE.java deleted file mode 100644 index c3bafc0c..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/CHALLENGE.java +++ /dev/null @@ -1,65 +0,0 @@ -package uk.co.compendiumdev.challenge; - -public enum CHALLENGE { - GET_CHALLENGES, - GET_TODOS, - GET_TODO, - GET_TODO_404, - POST_TODOS, - POST_UPDATE_TODO, - POST_TODOS_BAD_DONE_STATUS, - DELETE_A_TODO, - DELETE_ALL_TODOS, - GET_TODOS_FILTERED, - GET_TODOS_NOT_PLURAL_404, - OPTIONS_TODOS, - GET_HEAD_TODOS, - POST_TODOS_415, - GET_ACCEPT_XML, - GET_ACCEPT_JSON, - GET_ACCEPT_ANY_DEFAULT_JSON, - GET_ACCEPT_XML_PREFERRED, - GET_JSON_BY_DEFAULT_NO_ACCEPT, - GET_UNSUPPORTED_ACCEPT_406, - POST_CREATE_XML, - POST_CREATE_JSON, - GET_HEARTBEAT_204, - DELETE_HEARTBEAT_405, - POST_CREATE_JSON_ACCEPT_XML, - POST_CREATE_XML_ACCEPT_JSON, - TRACE_HEARTBEAT_501, - PATCH_HEARTBEAT_500, - CREATE_SECRET_TOKEN_401, - CREATE_SECRET_TOKEN_201, - GET_SECRET_NOTE_401, - GET_SECRET_NOTE_403, - POST_SECRET_NOTE_403, - POST_SECRET_NOTE_401, - POST_SECRET_NOTE_200, - GET_SECRET_NOTE_200, - CREATE_NEW_CHALLENGER, - GET_SECRET_NOTE_BEARER_200, - POST_SECRET_NOTE_BEARER_200, - POST_ALL_TODOS, - POST_TODOS_TOO_LONG_TITLE_LENGTH, - POST_TODOS_TOO_LONG_DESCRIPTION_LENGTH, - GET_RESTORE_EXISTING_CHALLENGER, - POST_RESTORE_EXISTING_CHALLENGER, - POST_TODOS_TOO_LONG_PAYLOAD_SIZE, - POST_TODOS_INVALID_EXTRA_FIELD, - POST_MAX_OUT_TITLE_DESCRIPTION_LENGTH, - PUT_TODOS_400, - POST_TODOS_404, - PUT_TODOS_FULL_200, - PUT_TODOS_PARTIAL_200, - PUT_TODOS_MISSING_TITLE_400, - PUT_TODOS_400_NO_AMEND_ID, - OVERRIDE_PATCH_HEARTBEAT_500, - OVERRIDE_DELETE_HEARTBEAT_405, - OVERRIDE_TRACE_HEARTBEAT_501, - GET_RESTORABLE_CHALLENGER_PROGRESS_STATUS, - PUT_RESTORABLE_CHALLENGER_PROGRESS_STATUS, - PUT_NEW_RESTORED_CHALLENGER_PROGRESS_STATUS, - GET_RESTORABLE_TODOS, - PUT_RESTORABLE_TODOS; -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java deleted file mode 100644 index 7ba1f04c..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java +++ /dev/null @@ -1,135 +0,0 @@ -package uk.co.compendiumdev.challenge; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.challenge.apimodel.ChallengeApiModel; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.httpserver.MainImplementation; -import uk.co.compendiumdev.thingifier.adapter.httpserver.ThingifierHttpApiRoutings; -import uk.co.compendiumdev.thingifier.core.repository.ThingStoreProviderConfig; - -public class ChallengeMain { - - static ChallengeRouteHandler challenger; - static MainImplementation app; - - public static void main(String[] args) { - - Logger logger = LoggerFactory.getLogger(ChallengeMain.class); - - logger.info("Starting Challenger"); - - app = new MainImplementation(); - ThingStoreProviderConfig repositoryConfig = ThingStoreProviderConfig.fromArgs(args); - logger.info("Using Thingifier repository {}", repositoryConfig.describe()); - Thingifier thingifier = new ChallengeApiModel().get(repositoryConfig.createProvider()); - app.registerModel("challengeapi", thingifier); - - // add any additional thingifier configurations here if more needed than model has defined - app.setDefaultsFromArgs(args); - - app.configurePortAndDefaultRoutes(); - app.setupBuiltInConfigurableRoutes(); - - new IndexNowRouteHandler().configureRoutes(); - - // create a default configuration class which we pass into the ChallengeRouteHandler and - // configure via args - ChallengerConfig config = new ChallengerConfig(); - config.setSimulationRepositoryFromArgs(args); - logger.info( - "Using Simulation repository {}", - config.getSimulationRepositoryConfig().describe()); - - for (String arg : args) { - if (arg.toLowerCase().startsWith("-multiplayer") - || arg.toLowerCase().startsWith("-multiuser")) { - logger.info("Running in multiplayer mode"); - config.setToMultiPlayerMode(); - } - - if (arg.toLowerCase().startsWith("-cloudstorage")) { - logger.info("Setting persistence mechanism to cloud"); - config.setToCloudPersistenceMode(); - } - - if (arg.toLowerCase().startsWith("-guikeepalive")) { - logger.info("Setting GUI to keep session alive through XHR"); - config.setGuiToKeepSessionAlive(); - } - - if (arg.toLowerCase().startsWith("-memory") - || arg.toLowerCase().startsWith("-nostorage")) { - logger.info("Setting persistence mechanism to no persistence"); - config.setToNoPersistenceMode(); - } - - if (arg.toLowerCase().startsWith("-enableadminapi")) { - logger.info("Enabling Admin Api"); - config.enableAdminApi(); - } - - if (arg.toLowerCase().startsWith("-unlimitedtodos")) { - // remove the limit on number of todos - logger.info("Enabling Unlimited TODO Instances"); - thingifier.getDefinitionNamed("todo").setNoMaxInstanceLimit(); - } - } - - thingifier.apiConfig().setApiToAllowRobotsIndexingResponses(false); - thingifier.apiConfig().setSupportsMultipleDatabases(true); - - // setup routes required for challenges - challenger = new ChallengeRouteHandler(thingifier, app.getApiDefn(), config); - - app.chooseThingifier(); - // can set profile by adding more configs, or just - // app.setProfileToUse(aProfile) - app.configureThingifierWithProfile(); - - app.setupDefaultGui(); - app.getGuiManagement().setCanonicalHost("https://apichallenges.eviltester.com"); - app.getGuiManagement() - .appendToCustomHeadContent( - """ - <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png"> - <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"> - <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"> - <link rel="manifest" href="/favicon/site.webmanifest"> - <link rel="stylesheet" href="/css/toc.css"> - <link rel="stylesheet" href="/css/content.css"> - """); - - challenger.setupGui(app.getGuiManagement()); - challenger.configureRoutes(); - - if (challenger.isSinglePlayerMode()) { - logger.info("Running in Single User Mode"); - challenger - .getThingifier() - .ensureCreatedAndPopulatedInstanceDatabaseNamed(Challengers.SINGLE_PLAYER_GUID); - } - - final ThingifierHttpApiRoutings restServer = app.startRestServer(); - - app.addBuiltInArgConfiguredHooks(); - - challenger.addHooks(restServer); - } - - public static ChallengeRouteHandler getChallenger() { - return challenger; - } - - public static void stop() { - if (challenger != null) { - challenger.close(); - } - if (app != null) { - app.close(); - } - challenger = null; - app = null; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeRouteHandler.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeRouteHandler.java deleted file mode 100644 index f3afc12c..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeRouteHandler.java +++ /dev/null @@ -1,176 +0,0 @@ -package uk.co.compendiumdev.challenge; - -import uk.co.compendiumdev.challenge.challengehooks.*; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitions; -import uk.co.compendiumdev.challenge.challengesrouting.*; -import uk.co.compendiumdev.challenge.gui.ChallengerWebGUI; -import uk.co.compendiumdev.challenge.persistence.PersistenceLayer; -import uk.co.compendiumdev.challenge.practicemodes.mirror.MirrorRoutes; -import uk.co.compendiumdev.challenge.practicemodes.simpleapi.SimpleApiRoutes; -import uk.co.compendiumdev.challenge.practicemodes.simulation.SimulationRoutes; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.httpserver.ThingifierHttpApiRoutings; -import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; -import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; - -public class ChallengeRouteHandler { - private final Thingifier thingifier; - // List<RoutingDefinition> routes; - - ThingifierApiDocumentationDefn apiChallengesDocumentationDefn; - ThingifierApiDocumentationDefn mirrorModeDocumentationDefn; - - ChallengeDefinitions challengeDefinitions; - Challengers challengers; - private boolean single_player_mode; - private final ChallengerConfig config; - PersistenceLayer persistenceLayer; - // when set gui makes a call every 5 mins to keep session alive - private boolean guiStayAlive; - private DefaultGUIHTML guiTemplates; - private SimulationRoutes simulationRoutes; - - // not needed when storing data - - public ChallengeRouteHandler( - Thingifier thingifier, - ThingifierApiDocumentationDefn apiDefn, - ChallengerConfig config) { - - this.config = config; - this.apiChallengesDocumentationDefn = apiDefn; - apiDefn.setThingifier(thingifier); - apiDefn.setSeoTitle("API Challenges API Documentation | API Challenges"); - apiDefn.setSeoDescription( - "Explore API Challenges endpoint documentation with request formats, payload examples, and expected responses for practical API testing."); - apiDefn.setMetaRobots("index,follow"); - apiDefn.setOgType("website"); - apiDefn.setTwitterCard("summary_large_image"); - - this.mirrorModeDocumentationDefn = new ThingifierApiDocumentationDefn(); - mirrorModeDocumentationDefn.setTitle("Mirror Mode"); - mirrorModeDocumentationDefn.setDescription("Mirror HTTP Requests"); - mirrorModeDocumentationDefn.setSeoTitle("Mirror Mode API Documentation | API Challenges"); - mirrorModeDocumentationDefn.setSeoDescription( - "Review Mirror Mode endpoint documentation to inspect reflected HTTP requests, headers, and payload behavior for debugging and learning."); - mirrorModeDocumentationDefn.setMetaRobots("noindex,follow"); - mirrorModeDocumentationDefn.setOgType("website"); - mirrorModeDocumentationDefn.setTwitterCard("summary_large_image"); - mirrorModeDocumentationDefn.addServer( - "https://apichallenges.eviltester.com", "cloud hosted version"); - mirrorModeDocumentationDefn.addServer("http://localhost:4567", "local execution"); - mirrorModeDocumentationDefn.setVersion("1.0.0"); - - apiDefn.addServer("https://apichallenges.eviltester.com", "cloud hosted version"); - apiDefn.addServer("http://localhost:4567", "local execution"); - apiDefn.setVersion("1.0.0"); - - single_player_mode = config.single_player_mode; - persistenceLayer = config.persistenceLayer; - guiStayAlive = config.guiStayAlive; - - challengeDefinitions = new ChallengeDefinitions(config); - this.thingifier = thingifier; - challengers = - new Challengers( - thingifier.getERmodel(), challengeDefinitions.getDefinedChallenges()); - challengers.setPersistenceLayer(persistenceLayer); - if (!single_player_mode) { - challengers.setMultiPlayerMode(); - } - - if (single_player_mode) { - // auto load any single player challenger details in single player mode - persistenceLayer.tryToLoadChallenger(challengers, challengers.SINGLE_PLAYER_GUID); - } - - challengers.setApiConfig(thingifier.apiConfig()); - - if (config.isAdminApiEnabled) { - enableAdminApi(); - } - - this.guiTemplates = new DefaultGUIHTML(); - } - - public boolean isSinglePlayerMode() { - return single_player_mode; - } - - public ChallengeRouteHandler configureRoutes() { - - new ChallengerTrackingRoutes() - .configure( - challengers, - single_player_mode, - apiChallengesDocumentationDefn, - persistenceLayer, - thingifier, - challengeDefinitions); - new ChallengesRoutes() - .configure( - challengers, - single_player_mode, - apiChallengesDocumentationDefn, - challengeDefinitions); - new HeartBeatRoutes().configure(apiChallengesDocumentationDefn); - new AuthRoutes().configure(challengers, apiChallengesDocumentationDefn); - - // Mirror routes should not show up in the apichallenges apiDefn - new MirrorRoutes().configure(mirrorModeDocumentationDefn, guiTemplates); - - // Simulation routes should not show - simulationRoutes = - new SimulationRoutes(guiTemplates, config.getSimulationRepositoryConfig()); - simulationRoutes.configure(); - - new SimpleApiRoutes(guiTemplates).configure(); - - return this; - } - - public void addHooks(final ThingifierHttpApiRoutings apiRoutings) { - - // TODO: these internal HTTP hooks are registered through server-level before/after hooks. - // They can run for every HTTP request handled by the server, not just requests for this - // Thingifier API routing. Prefer route-scoped internal HTTP hooks or move this behavior - // into the API bridge boundary when it only applies to this Thingifier. - apiRoutings.registerInternalHttpResponseHook( - new ChallengerInternalHTTPResponseHook(challengers)); - apiRoutings.registerInternalHttpRequestHook( - new ChallengerInternalHTTPRequestHook(challengers)); - - // These hooks run inside the Thingifier API bridge for this routing instance, so they are - // scoped to this Thingifier's API request/response processing. - apiRoutings.registerHttpApiRequestHook(new ChallengerApiRequestHook(challengers)); - apiRoutings.registerHttpApiResponseHook( - new ChallengerApiResponseHook(challengers, thingifier)); - } - - public void setupGui(DefaultGUIHTML guiManagement) { - this.guiTemplates = guiManagement; - new ChallengerWebGUI(guiManagement, guiStayAlive) - .setup(challengers, challengeDefinitions, persistenceLayer, single_player_mode); - } - - public Challengers getChallengers() { - return challengers; - } - - public Thingifier getThingifier() { - return thingifier; - } - - public void close() { - if (simulationRoutes != null) { - simulationRoutes.close(); - } - thingifier.close(); - } - - private void enableAdminApi() { - thingifier.apiConfig().adminConfig().enableAdminSearch(); - thingifier.apiConfig().adminConfig().enableAdminDataClear(); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengerAuthData.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengerAuthData.java deleted file mode 100644 index 5d79a2f5..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengerAuthData.java +++ /dev/null @@ -1,157 +0,0 @@ -package uk.co.compendiumdev.challenge; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import uk.co.compendiumdev.challenge.challengers.Challengers; - -public class ChallengerAuthData { - - private String xAuthToken; - private final long extratime; - private String xChallenger; - private long lastAccessed; - private long expiresin; - private String secretNote; - private Map<CHALLENGE, Boolean> challengeStatus; - - private ChallengerState state; - - public ChallengerAuthData(Collection<CHALLENGE> definedChallenges) { - this.xChallenger = UUID.randomUUID().toString(); - this.xAuthToken = UUID.randomUUID().toString(); - this.expiresin = 600000; // 10 * 60 * 1000; // 10 minutes - this.extratime = 30000; // 30 * 1000 - extra time on each HttpServerRequest - touch(); - this.secretNote = ""; - resetChallengesStatus(definedChallenges); - this.state = ChallengerState.NEW; - } - - private void resetChallengesStatus(Collection<CHALLENGE> definedChallenges) { - challengeStatus = new HashMap<>(); - // this should only be challenges defined - for (CHALLENGE challenge : definedChallenges) { - challengeStatus.put(challenge, false); - } - } - - public String getXAuthToken() { - touch(); - return xAuthToken; - } - - public String getNote() { - touch(); - return secretNote; - } - - public void setNote(final String note) { - touch(); - int maxLen = 100; - if (note == null) { - throw new RuntimeException("No Note Provided"); - } - if (note.length() <= maxLen) { - secretNote = note; - } else { - secretNote = note.substring(0, maxLen); - } - } - - public long getLastAccessed() { - return this.lastAccessed; - } - - public long expiresAt() { - return this.lastAccessed + expiresin; - } - - public void touch() { - lastAccessed = System.currentTimeMillis(); - expiresin = expiresin + extratime; - if (expiresin > 600000) { - expiresin = 600000; - } - } - - public String getXChallenger() { - return xChallenger; - } - - public Boolean statusOfChallenge(final CHALLENGE challenge) { - Boolean status = challengeStatus.get(challenge); - if (status == null) { - return false; - } - return challengeStatus.get(challenge); - } - - public void pass(final CHALLENGE id) { - // only update challenge if configured for it - if (challengeStatus.containsKey(id)) { - challengeStatus.put(id, true); - } - } - - public void setXChallengerGUID(final String guid) { - this.xChallenger = guid; - } - - public void setState(ChallengerState challengerState) { - this.state = challengerState; - } - - public ChallengerAuthData fromData( - ChallengerAuthData data, Collection<CHALLENGE> definedChallenges) { - // set from data but do not fully trust data - setNote(data.secretNote); - - // only allow setting the uuid if we are not in single player mode - if (!xChallenger.equals(Challengers.SINGLE_PLAYER_GUID)) { - try { - setXChallengerGUID(UUID.fromString(data.getXChallenger()).toString()); - } catch (Exception e) { - // could not convert to GUID so use the existing - } - } - - try { - xAuthToken = UUID.fromString(data.getXAuthToken()).toString(); - } catch (Exception e) { - // could not convert to GUID so use the existing - } - - state = data.getState(); - - resetChallengesStatus(definedChallenges); - // this should not be all challenges, only those which have been defined - - for (CHALLENGE challenge : definedChallenges) { - challengeStatus.put(challenge, data.statusOfChallenge(challenge)); - } - - return this; - } - - public ChallengerState getState() { - return this.state; - } - - public String asJson() { - // does not return everything as we don't restore everything - JsonElement tree = new Gson().toJsonTree(this); - tree.getAsJsonObject().remove("extratime"); - tree.getAsJsonObject().remove("lastAccessed"); - tree.getAsJsonObject().remove("expiresin"); - tree.getAsJsonObject().remove("state"); - return tree.toString(); - } - - public void setAsExpired() { - lastAccessed = lastAccessed - expiresin - expiresin; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengerConfig.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengerConfig.java deleted file mode 100644 index 87432a2f..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengerConfig.java +++ /dev/null @@ -1,104 +0,0 @@ -package uk.co.compendiumdev.challenge; - -import java.nio.file.Path; -import java.nio.file.Paths; -import uk.co.compendiumdev.challenge.persistence.PersistenceLayer; -import uk.co.compendiumdev.thingifier.core.repository.ThingStoreProviderConfig; - -public class ChallengerConfig { - - public static final String ARG_SIM_REPOSITORY = "-sim-repository"; - public static final String ARG_SIM_SQLITE_MEMORY = "-sim-sqlite-memory"; - public static final String ARG_SIM_SQLITE_DIRECTORY = "-sim-sqlite-directory"; - public static final String DEFAULT_SIM_SQLITE_DIRECTORY = "thingifier-sim-sqlite"; - - public boolean single_player_mode = true; - public boolean isAdminApiEnabled = false; - public boolean guiStayAlive = false; - public PersistenceLayer persistenceLayer = - new PersistenceLayer(PersistenceLayer.StorageType.LOCAL); - private ThingStoreProviderConfig simulationRepositoryConfig = - defaultSimulationRepositoryConfig(); - - public static ThingStoreProviderConfig defaultSimulationRepositoryConfig() { - return new ThingStoreProviderConfig( - ThingStoreProviderConfig.DEFAULT_REPOSITORY_MODE, - Paths.get(DEFAULT_SIM_SQLITE_DIRECTORY)); - } - - public void setSimulationRepositoryFromArgs(final String[] args) { - String repositoryMode = ThingStoreProviderConfig.DEFAULT_REPOSITORY_MODE; - if (hasArg(args, ARG_SIM_SQLITE_MEMORY)) { - repositoryMode = "sqlite-memory"; - } else { - String configuredMode = argValue(args, ARG_SIM_REPOSITORY); - if (configuredMode != null && !configuredMode.trim().isEmpty()) { - repositoryMode = configuredMode; - } - } - - String sqliteDirectory = argValue(args, ARG_SIM_SQLITE_DIRECTORY); - if (sqliteDirectory == null || sqliteDirectory.trim().isEmpty()) { - sqliteDirectory = DEFAULT_SIM_SQLITE_DIRECTORY; - } - - simulationRepositoryConfig = - new ThingStoreProviderConfig(repositoryMode, Path.of(sqliteDirectory)); - } - - public ThingStoreProviderConfig getSimulationRepositoryConfig() { - return simulationRepositoryConfig; - } - - public void setToMultiPlayerMode() { - single_player_mode = false; - } - - public void setToCloudPersistenceMode() { - persistenceLayer = new PersistenceLayer(PersistenceLayer.StorageType.CLOUD); - } - - public void setGuiToKeepSessionAlive() { - guiStayAlive = true; - } - - public void setToNoPersistenceMode() { - persistenceLayer = new PersistenceLayer(PersistenceLayer.StorageType.NONE); - } - - public void enableAdminApi() { - isAdminApiEnabled = true; - } - - private static String argValue(final String[] args, final String argName) { - if (args == null) { - return null; - } - - for (String arg : args) { - if (arg == null) { - continue; - } - if (arg.equalsIgnoreCase(argName)) { - return ""; - } - if (arg.toLowerCase().startsWith(argName.toLowerCase() + "=")) { - return arg.substring(arg.indexOf("=") + 1).trim(); - } - } - return null; - } - - private static boolean hasArg(final String[] args, final String argName) { - if (args == null) { - return false; - } - - for (String arg : args) { - if (arg != null && arg.equalsIgnoreCase(argName)) { - return true; - } - } - return false; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengerState.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengerState.java deleted file mode 100644 index 07b2bb08..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengerState.java +++ /dev/null @@ -1,6 +0,0 @@ -package uk.co.compendiumdev.challenge; - -public enum ChallengerState { - NEW, - LOADED_FROM_PERSISTENCE -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/IndexNowRouteHandler.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/IndexNowRouteHandler.java deleted file mode 100644 index a3816c4b..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/IndexNowRouteHandler.java +++ /dev/null @@ -1,83 +0,0 @@ -package uk.co.compendiumdev.challenge; - -import java.util.function.BiConsumer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpRouteHandler; -import uk.co.compendiumdev.thingifier.adapter.httpserver.ServerRoutes; - -public class IndexNowRouteHandler { - - private static final Logger logger = LoggerFactory.getLogger(IndexNowRouteHandler.class); - - static final String INDEX_NOW_KEY_LOCATION = "INDEX_NOW_KEY_LOCATION"; - static final String INDEX_NOW_KEY = "INDEX_NOW_KEY"; - - private final String keyLocation; - private final String key; - private final BiConsumer<String, HttpRouteHandler> getRouteRegistrar; - - public IndexNowRouteHandler() { - this( - System.getenv(INDEX_NOW_KEY_LOCATION), - System.getenv(INDEX_NOW_KEY), - ServerRoutes::get); - } - - IndexNowRouteHandler( - final String keyLocation, - final String key, - final BiConsumer<String, HttpRouteHandler> getRouteRegistrar) { - this.keyLocation = normalize(keyLocation); - this.key = normalize(key); - this.getRouteRegistrar = getRouteRegistrar; - } - - public boolean configureRoutes() { - - if (!isConfigurationValid()) { - logger.info( - "IndexNow endpoint not enabled because environment variables are missing or invalid"); - return false; - } - - getRouteRegistrar.accept( - keyLocation, - (request, response) -> { - if (response != null) { - response.type("text/plain; charset=utf-8"); - } - return key; - }); - - logger.info("IndexNow endpoint enabled at {}", keyLocation); - return true; - } - - private boolean isConfigurationValid() { - return key != null && keyLocation != null && isValidLocation(keyLocation); - } - - static boolean isValidLocation(final String location) { - if (location == null || location.isBlank()) { - return false; - } - - if (!location.startsWith("/") || !location.endsWith(".txt")) { - return false; - } - - return !location.contains("*") - && !location.contains("?") - && !location.contains("#") - && !location.contains(" ") - && !location.contains("\\"); - } - - private static String normalize(final String value) { - if (value == null || value.isBlank()) { - return null; - } - return value.trim(); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModel.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModel.java deleted file mode 100644 index 70391f00..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModel.java +++ /dev/null @@ -1,57 +0,0 @@ -package uk.co.compendiumdev.challenge.apimodel; - -import static uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType.STRING; - -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.definitions.validation.VRule; -import uk.co.compendiumdev.thingifier.core.repository.ThingStoreProvider; - -public class ChallengeApiModel { - public Thingifier get() { - return get(new EntityRelModel()); - } - - public Thingifier get(final ThingStoreProvider storeProvider) { - return get(new EntityRelModel(storeProvider)); - } - - public Thingifier get(final EntityRelModel entityRelModel) { - Thingifier todoList = new Thingifier(entityRelModel); - - StringBuilder para = new StringBuilder(); - - para.append("A Simple todo list"); - - todoList.setDocumentation("Simple Todo List", para.toString()); - - // can create a maximum of 20 todos in the challenge todos list - EntityDefinition todo = todoList.defineThing("todo", "todos", 20); - todo.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - todo.addFields( - Field.is("title", STRING) - .withExample("A title") - .makeMandatory() - .withValidation(VRule.notEmpty(), VRule.maximumLength(50)) - .withDescription("The title of the TODO Item"), - Field.is("doneStatus", FieldType.BOOLEAN) - .withDefaultValue("false") - .withDescription("Status to track the todo completion"), - Field.is("description", STRING) - .withExample("my description") - .withValidation(VRule.maximumLength(200)) - .withDescription("Longer description of the todo")); - - todoList.setDataGenerator(new TodoAPIDataPopulator()); - todoList.generateData(EntityRelModel.DEFAULT_DATABASE_NAME); - - todoList.apiConfig().setApiToShowPrimaryKeyHeaderInResponse(false); - todoList.apiConfig().statusCodes().setMaxRequestBodyLengthBytes(5000); - todoList.apiConfig().setReturnSingleGetItemsAsCollection(true); - - return todoList; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeThingifier.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeThingifier.java deleted file mode 100644 index d078a928..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeThingifier.java +++ /dev/null @@ -1,90 +0,0 @@ -package uk.co.compendiumdev.challenge.apimodel; - -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitions; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -public class ChallengeThingifier { - - public Thingifier challengeThingifier; - public EntityDefinition challengeDefn; - private ChallengeDefinitions challengeDefinitions; - - public ChallengeThingifier() { - createThingifier(); - } - - private void createThingifier() { - // fake the data storage - this.challengeThingifier = new Thingifier(); - - this.challengeThingifier.apiConfig().setApiToShowPrimaryKeyHeaderInResponse(true); - this.challengeThingifier.apiConfig().forParams().willEnforceFilteringThroughUrlParams(); - this.challengeThingifier.apiConfig().forParams().willAllowFilteringThroughUrlParams(); - this.challengeThingifier.apiConfig().setApiToAllowRobotsIndexingResponses(false); - this.challengeThingifier.apiConfig().setReturnSingleGetItemsAsCollection(true); - - this.challengeDefn = this.challengeThingifier.defineThing("challenge", "challenges"); - this.challengeDefn.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - this.challengeDefn.addFields( - Field.is("name", FieldType.STRING) - .withDefaultValue("") - .withDescription("title to describe the challenge"), - Field.is("description", FieldType.STRING) - .withDefaultValue("") - .withDescription("description of the challenge"), - Field.is("status", FieldType.BOOLEAN) - .withDefaultValue("false") - .withDescription("status to track if challenges is completed")); - } - - public void populateThingifierFrom(ChallengeDefinitions challengeDefinitions) { - this.challengeDefinitions = challengeDefinitions; - // create all instances from the definitions, then when we want to - // set all the status codes to the specific challenger status - ThingStore store = challengeThingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - for (ChallengeDefinitionData challenge : challengeDefinitions.getChallenges()) { - store.entities() - .create( - EntityInstanceDraft.forEntity(challengeDefn) - .withProtectedField("id", challenge.id) - .withField("name", challenge.name) - .withField("description", challenge.description)); - } - } - - public void populateThingifierFromStatus(ChallengerAuthData challenger) { - - ChallengerAuthData challengerToUse = challenger; - - if (challenger == null) { - // create one just to show no progress - challengerToUse = new ChallengerAuthData(challengeDefinitions.getDefinedChallenges()); - } - - ThingStore store = challengeThingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - for (ChallengeDefinitionData challenge : challengeDefinitions.getChallenges()) { - final EntityInstance instance = - store.entityQueries().findByField(challengeDefn, "id", challenge.id); - store.entities() - .patch( - instance, - EntityInstanceDraft.forEntity(challengeDefn) - .withField( - "status", - challengerToUse - .statusOfChallenge( - challengeDefinitions.getChallenge( - challenge.name)) - .toString())); - } - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/TodoAPIDataPopulator.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/TodoAPIDataPopulator.java deleted file mode 100644 index 4ce02e9d..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/TodoAPIDataPopulator.java +++ /dev/null @@ -1,33 +0,0 @@ -package uk.co.compendiumdev.challenge.apimodel; - -import uk.co.compendiumdev.thingifier.core.domain.datapopulator.RepositoryDataPopulator; -import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -public class TodoAPIDataPopulator implements RepositoryDataPopulator { - - @Override - public void populate(final ERSchema schema, final ThingStore store) { - String[] todos = { - "scan paperwork", - "file paperwork", - "process payments", - "escalate late payments", - "pay invoices", - "process payroll", - "train staff", - "schedule meeting", - "tidy meeting room", - "install webcam" - }; - - EntityDefinition todo = schema.getEntityDefinitionNamed("todo"); - - for (String todoItem : todos) { - store.entities() - .create(EntityInstanceDraft.forEntity(todo).withField("title", todoItem)); - } - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerApiRequestHook.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerApiRequestHook.java deleted file mode 100644 index e045290d..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerApiRequestHook.java +++ /dev/null @@ -1,77 +0,0 @@ -package uk.co.compendiumdev.challenge.challengehooks; - -import static uk.co.compendiumdev.thingifier.api.http.ThingifierHttpApi.HTTP_SESSION_HEADER_NAME; - -import java.util.List; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.thingifier.adapter.http.messagehooks.HttpApiRequestHook; -import uk.co.compendiumdev.thingifier.api.ermodelconversion.JsonThing; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; -import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; - -public class ChallengerApiRequestHook implements HttpApiRequestHook { - - private final Challengers challengers; - - public ChallengerApiRequestHook(Challengers challengers) { - this.challengers = challengers; - } - - @Override - public HttpApiResponse run(final HttpApiRequest request, final ThingifierApiConfig config) { - - ChallengerAuthData challenger = - challengers.getChallenger(request.getHeader("X-CHALLENGER")); - if (challenger == null) { - - // if there is no x-challenger and we are in multi-player mode then do not allow any - // POST, DELETE, PUT, PATCH through to the API as this would amend the default database - if (challengers.isMultiPlayerMode()) { - if (request.getVerb().equals(HttpApiRequest.VERB.POST) - || request.getVerb().equals(HttpApiRequest.VERB.PUT) - || request.getVerb().equals(HttpApiRequest.VERB.PATCH) - || request.getVerb().equals(HttpApiRequest.VERB.DELETE)) { - return new HttpApiResponse( - request.getHeaders(), - new ApiResponse( - 401, - true, - List.of( - "Cannot amend details. Missing a valid X-CHALLENGER header.")), - new JsonThing(challengers.getApiConfig().jsonOutput()), - challengers.getApiConfig()); - } - } - - // cannot track challenges - return null; - } - - // extend the life of the challenger - challenger.touch(); - - // trim the list of challengers - challengers.purgeOldAuthData(); - - // add challenger guid as session id to request - request.addHeader(HTTP_SESSION_HEADER_NAME, challenger.getXChallenger()); - - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().contentEquals("todos") - && request.getQueryParams().size() == 0) { - challengers.pass(challenger, CHALLENGE.GET_TODOS); - } - - if (request.getVerb() == HttpApiRequest.VERB.HEAD - && request.getPath().contentEquals("todos") - && request.getQueryParams().size() == 0) { - challengers.pass(challenger, CHALLENGE.GET_HEAD_TODOS); - } - - return null; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerApiResponseHook.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerApiResponseHook.java deleted file mode 100644 index b9ec358e..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerApiResponseHook.java +++ /dev/null @@ -1,339 +0,0 @@ -package uk.co.compendiumdev.challenge.challengehooks; - -import java.util.Collection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.messagehooks.HttpApiResponseHook; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; -import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.AcceptHeaderParser; -import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.ContentTypeHeaderParser; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.repository.EntityInstanceQuery; - -public class ChallengerApiResponseHook implements HttpApiResponseHook { - - Logger logger = LoggerFactory.getLogger(ChallengerApiResponseHook.class); - - private final Challengers challengers; - private final Thingifier thingifier; - - public ChallengerApiResponseHook(final Challengers challengers, Thingifier thingifier) { - this.challengers = challengers; - this.thingifier = thingifier; - } - - @Override - public HttpApiResponse run( - final HttpApiRequest request, - final HttpApiResponse response, - final ThingifierApiConfig config) { - - ChallengerAuthData challenger = - challengers.getChallenger(request.getHeader("X-CHALLENGER")); - - if (challenger == null) { - // cannot track challenges - return null; - } - - // READ - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.GET_TODO); - } - - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 404) { - challengers.pass(challenger, CHALLENGE.GET_TODO_404); - } - - final AcceptHeaderParser acceptParser = new AcceptHeaderParser(request.getHeader("accept")); - final ContentTypeHeaderParser contentTypeParser = - new ContentTypeHeaderParser(request.getHeader("content-type")); - - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().contentEquals("todos") - && request.getQueryParams().isEmpty() - && acceptParser.hasAskedForXML() - && response.getType().contentEquals("application/xml") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.GET_ACCEPT_XML); - } - - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().contentEquals("todos") - && request.getQueryParams().isEmpty() - && acceptParser.hasAskedForJSON() - && response.getType().contentEquals("application/json") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.GET_ACCEPT_JSON); - } - - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().contentEquals("todos") - && request.getQueryParams().isEmpty() - && acceptParser.missingAcceptHeader() - && response.getType().contentEquals("application/json") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.GET_JSON_BY_DEFAULT_NO_ACCEPT); - } - - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().contentEquals("todos") - && request.getQueryParams().isEmpty() - && !acceptParser.isSupportedHeader() - && response.getStatusCode() == 406) { - challengers.pass(challenger, CHALLENGE.GET_UNSUPPORTED_ACCEPT_406); - } - - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().contentEquals("todos") - && request.getQueryParams().isEmpty() - && acceptParser.hasAskedForANY() - && response.getType().contentEquals("application/json") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.GET_ACCEPT_ANY_DEFAULT_JSON); - } - - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().contentEquals("todos") - && request.getQueryParams().isEmpty() - && acceptParser.hasAskedForXML() - && acceptParser.hasAskedForJSON() - && acceptParser.hasAPreferenceForXml() - && response.getType().contentEquals("application/xml") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.GET_ACCEPT_XML_PREFERRED); - } - - if (request.getVerb() == HttpApiRequest.VERB.GET - && request.getPath().contentEquals("todos") - && request.getQueryParams().containsKey("doneStatus") - && request.getQueryParams().get("doneStatus").contentEquals("true") - && response.getStatusCode() == 200) { - // only pass if there are done and not done todos - final EntityInstance aDoneThing = findTodoByField(challenger, "doneStatus", "true"); - final EntityInstance aNotDoneThing = findTodoByField(challenger, "doneStatus", "false"); - if (aDoneThing != null && aNotDoneThing != null) { - challengers.pass(challenger, CHALLENGE.GET_TODOS_FILTERED); - } - } - - // CREATE - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && response.getStatusCode() == 201) { - challengers.pass(challenger, CHALLENGE.POST_TODOS); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 404) { - for (String errorMessage : response.apiResponse().getErrorMessages()) { - if (errorMessage.startsWith("No such todo entity instance with id ==")) { - challengers.pass(challenger, CHALLENGE.POST_TODOS_404); - } - } - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && response.getStatusCode() == 201) { - - try { - - String location = response.getHeaders().get("Location"); - String[] locationParts = location.split("/"); - - if (locationParts.length > 2) { - // to check it is an int - Integer.parseInt(locationParts[2]); - EntityInstance aTodo = findTodoByIdentifier(challenger, locationParts[2]); - if (aTodo != null - && aTodo.getFieldValue("title").asString().length() == 50 - && aTodo.getFieldValue("description").asString().length() == 200) { - challengers.pass( - challenger, CHALLENGE.POST_MAX_OUT_TITLE_DESCRIPTION_LENGTH); - } - } - } catch (Exception e) { - logger.warn("Error checking post todos 201 for max length ", e); - } - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && contentTypeParser.isXML() - && response.getType().contentEquals("application/xml") - && response.getStatusCode() == 201) { - challengers.pass(challenger, CHALLENGE.POST_CREATE_XML); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && contentTypeParser.isJSON() - && acceptParser.hasAskedForJSON() - && response.getType().contentEquals("application/json") - && response.getStatusCode() == 201) { - challengers.pass(challenger, CHALLENGE.POST_CREATE_JSON); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && contentTypeParser.isJSON() - && response.getType().contentEquals("application/xml") - && response.getStatusCode() == 201) { - challengers.pass(challenger, CHALLENGE.POST_CREATE_JSON_ACCEPT_XML); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && acceptParser.hasAskedForJSON() - && contentTypeParser.isXML() - && response.getType().contentEquals("application/json") - && response.getStatusCode() == 201) { - challengers.pass(challenger, CHALLENGE.POST_CREATE_XML_ACCEPT_JSON); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && response.getStatusCode() == 400 - && collate(response.apiResponse().getErrorMessages()) - .contains("Failed Validation: doneStatus should be BOOLEAN")) { - challengers.pass(challenger, CHALLENGE.POST_TODOS_BAD_DONE_STATUS); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && - // trap when creating or amending - request.getPath().startsWith("todo") - && response.getStatusCode() == 400 - && collate(response.apiResponse().getErrorMessages()) - .contains( - "Failed Validation: Maximum allowable length exceeded for title - maximum allowed is 50")) { - challengers.pass(challenger, CHALLENGE.POST_TODOS_TOO_LONG_TITLE_LENGTH); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && - // trap when creating or amending - request.getPath().startsWith("todo") - && response.getStatusCode() == 400 - && collate(response.apiResponse().getErrorMessages()) - .contains( - "Failed Validation: Maximum allowable length exceeded for description - maximum allowed is 200")) { - challengers.pass(challenger, CHALLENGE.POST_TODOS_TOO_LONG_DESCRIPTION_LENGTH); - } - - // POST to create too many todos - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().contentEquals("todos") - && response.getStatusCode() == 400 - && collate(response.apiResponse().getErrorMessages()) - .contains("ERROR: Cannot add instance, maximum limit of 20 reached")) { - challengers.pass(challenger, CHALLENGE.POST_ALL_TODOS); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && response.getStatusCode() == 415) { - challengers.pass(challenger, CHALLENGE.POST_TODOS_415); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && response.getStatusCode() == 413 - && collate(response.apiResponse().getErrorMessages()) - .toLowerCase() - .contains("request body too large")) { - challengers.pass(challenger, CHALLENGE.POST_TODOS_TOO_LONG_PAYLOAD_SIZE); - } - - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos") - && response.getStatusCode() == 400 - && collate(response.apiResponse().getErrorMessages()) - .contains("Could not find field:")) { - challengers.pass(challenger, CHALLENGE.POST_TODOS_INVALID_EXTRA_FIELD); - } - - // UPDATE - if (request.getVerb() == HttpApiRequest.VERB.POST - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.POST_UPDATE_TODO); - } - - // DELETE - if (request.getVerb() == HttpApiRequest.VERB.DELETE - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.DELETE_A_TODO); - } - - if (request.getVerb() == HttpApiRequest.VERB.DELETE - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 200 - && countTodos(challenger) == 0) { - challengers.pass(challenger, CHALLENGE.DELETE_ALL_TODOS); - } - - // do not interfere with api and return null - return null; - } - - private EntityInstance findTodoByField( - final ChallengerAuthData challenger, final String fieldName, final String fieldValue) { - EntityDefinition todo = todoDefinition(); - EntityInstanceQuery query = queryFor(challenger); - if (todo == null || query == null) { - return null; - } - return query.findByField(todo, fieldName, fieldValue); - } - - private EntityInstance findTodoByIdentifier( - final ChallengerAuthData challenger, final String identifier) { - EntityDefinition todo = todoDefinition(); - EntityInstanceQuery query = queryFor(challenger); - if (todo == null || query == null) { - return null; - } - return query.findByQueryIdentifier(todo, identifier); - } - - private int countTodos(final ChallengerAuthData challenger) { - EntityDefinition todo = todoDefinition(); - EntityInstanceQuery query = queryFor(challenger); - if (todo == null || query == null) { - return -1; - } - return query.count(todo); - } - - private EntityInstanceQuery queryFor(final ChallengerAuthData challenger) { - return thingifier.getStore(challenger.getXChallenger()).entityQueries(); - } - - private EntityDefinition todoDefinition() { - return thingifier.getDefinitionNamed("todo"); - } - - String collate(Collection<String> strings) { - StringBuilder collated = new StringBuilder(); - for (String string : strings) { - collated.append(string); - collated.append(" "); - } - return collated.toString(); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHTTPRequestHook.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHTTPRequestHook.java deleted file mode 100644 index 57241614..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHTTPRequestHook.java +++ /dev/null @@ -1,96 +0,0 @@ -package uk.co.compendiumdev.challenge.challengehooks; - -import static uk.co.compendiumdev.thingifier.api.http.ThingifierHttpApi.HTTP_SESSION_HEADER_NAME; - -import java.util.List; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.thingifier.adapter.httpserver.messagehooks.InternalHttpRequestHook; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpMethod; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpResponse; - -/* - This is an Internal HTTP request because it covers functionality for endpoints that do not - go through the normal API process i.e. heartbeat, challenges, challenger -*/ -public class ChallengerInternalHTTPRequestHook implements InternalHttpRequestHook { - private final Challengers challengers; - - public ChallengerInternalHTTPRequestHook(final Challengers challengers) { - this.challengers = challengers; - } - - @Override - public InternalHttpResponse run(final InternalHttpRequest request) { - - // TODO: fix hooks so that they only run on a specific thingifier basis. - // Until fixed so hooks only run on specific thingifiers, restrict this to Challenges API - // end points - List<String> validEndpointPrefixesToRunAgainst = - List.of("challenger", "todo", "todos", "challenges", "heartbeat", "secret"); - String[] pathSegments = request.getPath().split("/"); - if (!validEndpointPrefixesToRunAgainst.contains(pathSegments[0])) { - return null; - } - - ChallengerAuthData challenger = - challengers.getChallenger(request.getHeader("X-CHALLENGER")); - if (challenger == null) { - // cannot track challenges - return null; - } - - challenger.touch(); - challengers.purgeOldAuthData(); - - // add challenger guid as session id to request - request.addHeader(HTTP_SESSION_HEADER_NAME, challenger.getXChallenger()); - - InternalHttpMethod method = request.getVerb(); - String path = request.getPath(); - - if (method == InternalHttpMethod.GET && path.equals("challenges")) { - challengers.pass(challenger, CHALLENGE.GET_CHALLENGES); - } - - if (method == InternalHttpMethod.GET && path.equals("heartbeat")) { - challengers.pass(challenger, CHALLENGE.GET_HEARTBEAT_204); - } - - if (method == InternalHttpMethod.DELETE && path.equals("heartbeat")) { - challengers.pass(challenger, CHALLENGE.DELETE_HEARTBEAT_405); - } - - if (method == InternalHttpMethod.PATCH && path.equals("heartbeat")) { - challengers.pass(challenger, CHALLENGE.PATCH_HEARTBEAT_500); - } - - if (method == InternalHttpMethod.TRACE && path.equals("heartbeat")) { - challengers.pass(challenger, CHALLENGE.TRACE_HEARTBEAT_501); - } - - if (path.equals("heartbeat") - && request.getHeader("x-http-method-override").equalsIgnoreCase("patch")) { - challengers.pass(challenger, CHALLENGE.OVERRIDE_PATCH_HEARTBEAT_500); - } - - if (path.equals("heartbeat") - && request.getHeader("x-http-method-override").equalsIgnoreCase("delete")) { - challengers.pass(challenger, CHALLENGE.OVERRIDE_DELETE_HEARTBEAT_405); - } - - if (path.equals("heartbeat") - && request.getHeader("x-http-method-override").equalsIgnoreCase("trace")) { - challengers.pass(challenger, CHALLENGE.OVERRIDE_TRACE_HEARTBEAT_501); - } - - if (method == InternalHttpMethod.GET - && path.equals("challenger/" + challenger.getXChallenger())) { - challengers.pass(challenger, CHALLENGE.GET_RESTORABLE_CHALLENGER_PROGRESS_STATUS); - } - - return null; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHTTPResponseHook.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHTTPResponseHook.java deleted file mode 100644 index e8653432..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHTTPResponseHook.java +++ /dev/null @@ -1,348 +0,0 @@ -package uk.co.compendiumdev.challenge.challengehooks; - -import static uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpMethod.*; - -import java.util.List; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.ChallengerState; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.challenge.challengesrouting.XChallengerHeader; -import uk.co.compendiumdev.thingifier.adapter.httpserver.messagehooks.InternalHttpResponseHook; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpResponse; -import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.BearerAuthHeaderParser; - -public class ChallengerInternalHTTPResponseHook implements InternalHttpResponseHook { - - private final Challengers challengers; - - public ChallengerInternalHTTPResponseHook(final Challengers challengers) { - this.challengers = challengers; - } - - @Override - public void run(final InternalHttpRequest request, final InternalHttpResponse response) { - - // allow cross origin requests - // and swagger - // https://support.smartbear.com/swaggerhub/docs/en/edit-apis/cors-requirements-for--try-it-out-.html - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Headers", "*"); - response.setHeader("Access-Control-Allow-Credentials", "true"); - response.setHeader("Access-Control-Allow-Methods", "*"); - // this is necessary for swagger UI to show headers in the UI - response.setHeader("Access-Control-Expose-Headers", "*"); - if (request.getVerb() == OPTIONS - && request.getHeaders().headerExists("Access-Control-Allow-Methods")) { - response.setHeader( - "Access-Control-Allow-Methods", - request.getHeader("Access-Control-Allow-Methods")); - } - - // TODO: fix hooks so that they only run on a specific thingifier basis. - // Until fixed so hooks only run on specific thingifiers, restrict this to Challenges API - // end points - List<String> validEndpointPrefixesToRunAgainst = - List.of("challenger", "todo", "todos", "challenges", "heartbeat", "secret"); - String[] pathSegments = request.getPath().split("/"); - if (!validEndpointPrefixesToRunAgainst.contains(pathSegments[0])) { - return; - } - // boolean validEndpoint=false; - // for(String checkPrefix : validEndpointPrefixesToRunAgainst){ - // if(request.getPath().startsWith(checkPrefix)){ - // validEndpoint=true; - // break; - // } - // } - // if(!validEndpoint){ - // return; - // } - - ChallengerAuthData challenger = - challengers.getChallenger(request.getHeader("X-CHALLENGER")); - - // we can complete a challenge while the user is null - creating the user - if (request.getVerb() == POST - && request.getPath().contentEquals("challenger") - && response.getStatusCode() == 201) { - // challenger did not exist so we need to find it to pass the challenge - - String challengerId = response.getHeader("X-Challenger"); - challenger = challengers.getChallenger(challengerId); - if (challenger != null && challenger.getState() == ChallengerState.NEW) { - challengers.pass(challenger, CHALLENGE.CREATE_NEW_CHALLENGER); - } - if (challenger.getXChallenger().equals(Challengers.SINGLE_PLAYER_GUID)) { - // single player pass is simulated - challengers.pass(challenger, CHALLENGE.CREATE_NEW_CHALLENGER); - } - } - - if (request.getVerb() == GET - && request.getPath().startsWith("challenger/") - && response.getStatusCode() == 200) { - - String challengerId = response.getHeader("X-Challenger"); - challenger = challengers.getChallenger(challengerId); - if (challenger != null - && challenger.getState() == ChallengerState.LOADED_FROM_PERSISTENCE) { - challengers.pass(challenger, CHALLENGE.GET_RESTORE_EXISTING_CHALLENGER); - } - } - - if (request.getVerb() == POST - && request.getPath().startsWith("challenger") - && response.getStatusCode() == 200) { - - String givenChallengerId = request.getHeader("X-Challenger"); - String challengerId = response.getHeader("X-Challenger"); - challenger = challengers.getChallenger(challengerId); - if (challenger != null - && givenChallengerId.equals(challengerId) - && challenger.getState() == ChallengerState.LOADED_FROM_PERSISTENCE) { - challengers.pass(challenger, CHALLENGE.POST_RESTORE_EXISTING_CHALLENGER); - } - } - - if (request.getVerb() == PUT - && request.getPath().startsWith("challenger/") - && (response.getStatusCode() == 200)) { - - String challengerId = response.getHeader("X-Challenger"); - challenger = challengers.getChallenger(challengerId); - if (challenger != null) { - challengers.pass(challenger, CHALLENGE.PUT_RESTORABLE_CHALLENGER_PROGRESS_STATUS); - } - } - - if (request.getVerb() == PUT - && request.getPath().startsWith("challenger/") - && (response.getStatusCode() == 201)) { - - String challengerId = response.getHeader("X-Challenger"); - challenger = challengers.getChallenger(challengerId); - if (challenger != null) { - challengers.pass(challenger, CHALLENGE.PUT_NEW_RESTORED_CHALLENGER_PROGRESS_STATUS); - } - } - - if (request.getVerb() == GET - && request.getPath().startsWith("challenger/database/") - && (response.getStatusCode() == 200)) { - - String challengerId = response.getHeader("X-Challenger"); - challenger = challengers.getChallenger(challengerId); - if (challenger != null) { - challengers.pass(challenger, CHALLENGE.GET_RESTORABLE_TODOS); - } - } - - if (request.getVerb() == PUT - && request.getPath().startsWith("challenger/database/") - && (response.getStatusCode() == 204)) { - - String challengerId = response.getHeader("X-Challenger"); - challenger = challengers.getChallenger(challengerId); - if (challenger != null) { - challengers.pass(challenger, CHALLENGE.PUT_RESTORABLE_TODOS); - } - } - - if (challenger == null) { - if (!request.getPath().contentEquals("challenger") - && !request.getPath() - .contains( - "mirror/r") // exclude mirror endpoints from adding a challenger - ) { - - if (!response.getHeaders().headerExists("X-CHALLENGER")) { - XChallengerHeader.setResultHeaderBasedOnChallenger(response, challenger); - } - } - // cannot track challenges - if (response.getStatusCode() == 404 - && (response.getBody() == null || response.getBody().isEmpty())) { - setResponseAs404(request, response); - } - return; - } - - if (challenger != null) { - if (!response.getHeaders().headerExists("X-CHALLENGER")) { - if (!request.getPath().contains("mirror/r")) { - // exclude mirror endpoints from adding a challenger - XChallengerHeader.setResultHeaderBasedOnChallenger(response, challenger); - } - } - } - - // No endpoint defined so this 404 created by HTTP server routing - if (request.getVerb() == GET - && request.getPath().contentEquals("todo") - && response.getStatusCode() == 404) { - challengers.pass(challenger, CHALLENGE.GET_TODOS_NOT_PLURAL_404); - } - - if (request.getVerb() == OPTIONS - && request.getPath().contentEquals("todos") - && response.getStatusCode() == 204) { - // hack for backwards compatibility with initial solutions - response.setStatus(200); - challengers.pass(challenger, CHALLENGE.OPTIONS_TODOS); - } - - if (request.getVerb() == PUT - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 400) { - if (response.getBody().contains("Cannot create todo with PUT due to Auto fields id")) { - challengers.pass(challenger, CHALLENGE.PUT_TODOS_400); - } - } - - if (request.getVerb() == PUT - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 200) { - if (request.getBody().toLowerCase().contains("donestatus") - && request.getBody().toLowerCase().contains("description")) { - challengers.pass(challenger, CHALLENGE.PUT_TODOS_FULL_200); - } - } - - if (request.getVerb() == PUT - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 400) { - if (response.getBody().contains("title : field is mandatory")) { - challengers.pass(challenger, CHALLENGE.PUT_TODOS_MISSING_TITLE_400); - } - } - - if (request.getVerb() == PUT - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 200) { - if (!request.getBody().toLowerCase().contains("donestatus") - && !request.getBody().toLowerCase().contains("description")) { - challengers.pass(challenger, CHALLENGE.PUT_TODOS_PARTIAL_200); - } - } - - if (request.getVerb() == PUT - && request.getPath().matches("todos/.*") - && response.getStatusCode() == 400) { - if (response.getBody().contains("Can not amend id from")) { - challengers.pass(challenger, CHALLENGE.PUT_TODOS_400_NO_AMEND_ID); - } - } - - if (request.getVerb() == POST - && request.getPath().contentEquals("secret/token") - && request.getHeaders().headerExists("Authorization") - && request.getHeader("Authorization").length() > 10 - && response.getStatusCode() == 401) { - challengers.pass(challenger, CHALLENGE.CREATE_SECRET_TOKEN_401); - } - - if (request.getVerb() == POST - && request.getPath().contentEquals("secret/token") - && request.getHeaders().headerExists("Authorization") - && request.getHeader("Authorization").length() > 10 - && response.getStatusCode() == 201) { - challengers.pass(challenger, CHALLENGE.CREATE_SECRET_TOKEN_201); - } - - if (request.getVerb() == GET - && request.getPath().contentEquals("secret/note") - && request.getHeaders().headerExists("X-AUTH-TOKEN") - && request.getHeader("X-AUTH-TOKEN").length() > 1 - && response.getStatusCode() == 403) { - challengers.pass(challenger, CHALLENGE.GET_SECRET_NOTE_403); - } - - if (request.getVerb() == GET - && request.getPath().contentEquals("secret/note") - && !request.getHeaders().headerExists("X-AUTH-TOKEN") - && response.getStatusCode() == 401) { - challengers.pass(challenger, CHALLENGE.GET_SECRET_NOTE_401); - } - - if (request.getVerb() == POST - && request.getPath().contentEquals("secret/note") - && request.getHeaders().headerExists("X-AUTH-TOKEN") - && request.getHeader("X-AUTH-TOKEN").length() > 1 - && request.getBody().contains("\"note\"") - && response.getStatusCode() == 403) { - challengers.pass(challenger, CHALLENGE.POST_SECRET_NOTE_403); - } - - if (request.getVerb() == POST - && request.getPath().contentEquals("secret/note") - && !request.getHeaders().headerExists("X-AUTH-TOKEN") - && request.getBody().contains("\"note\"") - && response.getStatusCode() == 401) { - challengers.pass(challenger, CHALLENGE.POST_SECRET_NOTE_401); - } - - if (request.getVerb() == POST - && request.getPath().contentEquals("secret/note") - && request.getHeaders().headerExists("X-AUTH-TOKEN") - && request.getBody().contains("\"note\"") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.POST_SECRET_NOTE_200); - } - - if (request.getVerb() == GET - && request.getPath().contentEquals("secret/note") - && request.getHeaders().headerExists("X-AUTH-TOKEN") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.GET_SECRET_NOTE_200); - } - - if (request.getVerb() == GET - && request.getPath().contentEquals("secret/note") - && request.getHeaders().headerExists("Authorization") - && new BearerAuthHeaderParser(request.getHeader("Authorization")).isValid() - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.GET_SECRET_NOTE_BEARER_200); - } - - if (request.getVerb() == POST - && request.getPath().contentEquals("secret/note") - && request.getHeaders().headerExists("Authorization") - && new BearerAuthHeaderParser(request.getHeader("Authorization")).isValid() - && request.getBody().contains("\"note\"") - && response.getStatusCode() == 200) { - challengers.pass(challenger, CHALLENGE.POST_SECRET_NOTE_BEARER_200); - } - - if (response.getStatusCode() == 404 - && (response.getBody() == null || response.getBody().isEmpty())) { - setResponseAs404(request, response); - } - } - - private void setResponseAs404(InternalHttpRequest request, InternalHttpResponse response) { - - if (request.getAcceptHeader() != null && !request.getAcceptHeader().isEmpty()) { - if (request.getAcceptHeader().contains("html")) { - // treat as a GUI request and redirect - response.setStatus(307); - response.setHeader("Location", "/gui/404/" + request.getPath()); - return; - } - if (request.getAcceptHeader().startsWith("application/")) { - if (request.getAcceptHeader().endsWith("xml")) { - response.setType("application/xml"); - response.setBody( - "<errorMessages><message>404 resource Unknown</message></errorMessages>"); - return; - } - if (request.getAcceptHeader().endsWith("json")) { - response.setType("application/json"); - response.setBody("{\"errorMessages\":[\"404 resource Unknown\"]}"); - return; - } - } - } - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengers/Challengers.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengers/Challengers.java deleted file mode 100644 index 3868fd87..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengers/Challengers.java +++ /dev/null @@ -1,194 +0,0 @@ -package uk.co.compendiumdev.challenge.challengers; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.ChallengerState; -import uk.co.compendiumdev.challenge.persistence.PersistenceLayer; -import uk.co.compendiumdev.challenge.persistence.PersistenceResponse; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; - -public class Challengers { - - Logger logger = LoggerFactory.getLogger(Challengers.class); - - private final EntityRelModel erModel; - private boolean singlePlayerMode; - Map<String, ChallengerAuthData> authData; - public ChallengerAuthData SINGLE_PLAYER; - public static final String SINGLE_PLAYER_GUID = "rest-api-challenges-single-player"; - public ChallengerAuthData DEFAULT_PLAYER_DATA; - PersistenceLayer persistenceLayer; - private ThingifierApiConfig apiConfig; - private final Collection<CHALLENGE> definedChallenges; - - public Challengers(EntityRelModel erModel, Collection<CHALLENGE> definedChallenges) { - authData = new ConcurrentHashMap<>(); - this.definedChallenges = definedChallenges; - SINGLE_PLAYER = new ChallengerAuthData(this.definedChallenges); - SINGLE_PLAYER.setXChallengerGUID(SINGLE_PLAYER_GUID); - DEFAULT_PLAYER_DATA = new ChallengerAuthData(this.definedChallenges); - this.singlePlayerMode = true; - this.erModel = erModel; - } - - public Collection<CHALLENGE> getDefinedChallenges() { - return definedChallenges; - } - - public void setMultiPlayerMode() { - singlePlayerMode = false; - } - - public boolean isMultiPlayerMode() { - return !singlePlayerMode; - } - - public boolean isSinglePlayerMode() { - return singlePlayerMode; - } - - public boolean inMemory(final String challengerGuid) { - if (challengerGuid == null || challengerGuid.trim().isEmpty()) { - return false; - } - - ChallengerAuthData challenger; - if (singlePlayerMode) { - challenger = SINGLE_PLAYER; - } else { - challenger = authData.get(challengerGuid); - } - return challenger != null; - } - - public ChallengerAuthData getChallenger(final String challengerGuid) { - if (singlePlayerMode) { - return SINGLE_PLAYER; - } - - if (challengerGuid == null || challengerGuid.trim().isEmpty()) { - return null; - } - - ChallengerAuthData challenger = authData.get(challengerGuid); - - // if(challengerGuid.equals(SINGLE_PLAYER_GUID)){ - // return SINGLE_PLAYER; - // } - - if (challenger == null) { - // we don't have challenger in memory, are they available in persistent store? - if (persistenceLayer != null) { - final PersistenceResponse response = - persistenceLayer.tryToLoadChallenger(this, challengerGuid.trim()); - if (response.isSuccess()) { - if (authData.containsKey(challengerGuid)) { - challenger = authData.get(challengerGuid); - challenger.setState(ChallengerState.LOADED_FROM_PERSISTENCE); - } - } - // todo: surface persistence errors i.e. response.getErrorMessage() - } - } - - return challenger; - } - - public EntityRelModel getErModel() { - return this.erModel; - } - - public void purgeOldAuthData() { - - if (singlePlayerMode) { - return; - } - - List<String> deleteMe = new ArrayList<>(); - final long cutOffTime = System.currentTimeMillis(); - for (ChallengerAuthData data : authData.values()) { - if (data.expiresAt() < cutOffTime) { - logger.warn("PURGING AUTH: {}", data.getXChallenger()); - deleteMe.add(data.getXChallenger()); - } else { - logger.info( - "PURGE: {} expires in {}", - data.getXChallenger(), - cutOffTime - data.expiresAt()); - } - } - - for (String deleteKey : deleteMe) { - delete(deleteKey); - if (erModel != null) { - if (erModel.getDatabaseNames().contains(deleteKey)) { - logger.warn("DELETING DATABASE: {}", deleteKey); - erModel.deleteInstanceDatabase(deleteKey); - } - } - } - logger.info("CURRENT Challenger count: {}", authData.values().size()); - if (erModel != null) { - logger.info("CURRENT database count: {}", erModel.getDatabaseNames().size()); - } - } - - public ChallengerAuthData createNewChallenger() { - ChallengerAuthData newChallenger = new ChallengerAuthData(definedChallenges); - newChallenger.setState(ChallengerState.NEW); - put(newChallenger); - return newChallenger; - } - - public void put(final ChallengerAuthData challenger) { - // todo: this should really check for single player mode and not just trust the GUID - if (challenger.getXChallenger().contentEquals(SINGLE_PLAYER_GUID)) { - SINGLE_PLAYER = challenger; // we just loaded the single player session - } else { - authData.put(challenger.getXChallenger(), challenger); - } - } - - public void persistChallengerState(final ChallengerAuthData challenger) { - if (persistenceLayer != null) { - String databaseName = challenger.getXChallenger(); - persistenceLayer.saveChallengerStatus( - challenger, erModel.exportInstanceDataAsJson(databaseName)); - } - } - - public void pass(final ChallengerAuthData challenger, final CHALLENGE challengeId) { - if (challenger != null) { - // todo: possibly only update challenge status if not already set - // if(!challenger.statusOfChallenge(challengeId)) { - challenger.pass(challengeId); - persistChallengerState(challenger); - // } - } - } - - public void setPersistenceLayer(final PersistenceLayer persistenceLayer) { - this.persistenceLayer = persistenceLayer; - } - - public void setApiConfig(ThingifierApiConfig apiConfig) { - this.apiConfig = apiConfig; - } - - public ThingifierApiConfig getApiConfig() { - return this.apiConfig; - } - - public void delete(String xChallenger) { - authData.remove(xChallenger); - } - - public Set<String> getChallengerGuids() { - return authData.keySet(); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeData.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeData.java deleted file mode 100644 index 54d8176f..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeData.java +++ /dev/null @@ -1,16 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges; - -public class ChallengeData { - - public final String id; - public final String name; - public final String description; - public Boolean status; - - public ChallengeData(String id, String name, String description) { - this.id = id; - this.name = name; - this.description = description; - this.status = false; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeDefinitionData.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeDefinitionData.java deleted file mode 100644 index 1bd38e40..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeDefinitionData.java +++ /dev/null @@ -1,50 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges; - -import java.util.ArrayList; -import java.util.List; - -public class ChallengeDefinitionData { - - public final String id; - public final String name; - public final String description; - public Boolean status; - public List<ChallengeSolutionLink> solutions = new ArrayList<>(); - public List<ChallengeHint> hints = new ArrayList<>(); - - public ChallengeDefinitionData(String id, String name, String description) { - this.id = id; - this.name = name; - this.description = description; - this.status = false; - } - - public void addSolutionLink( - final String linkText, final String linkType, final String linkData) { - solutions.add(new ChallengeSolutionLink(linkText, linkType, linkData)); - } - - public boolean hasSolutionLinks() { - return solutions.size() > 0; - } - - public void addSolutions(final List<ChallengeSolutionLink> solutions) { - this.solutions.addAll(solutions); - } - - public void addHint(final String hintText, final String hintLink) { - hints.add(new ChallengeHint(hintText, hintLink)); - } - - public boolean hasHints() { - return hints.size() > 0; - } - - public void addHints(final List<ChallengeHint> hints) { - this.hints.addAll(hints); - } - - public void addHint(final String hintText) { - hints.add(new ChallengeHint(hintText, "")); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeDefinitions.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeDefinitions.java deleted file mode 100644 index 81c9b013..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeDefinitions.java +++ /dev/null @@ -1,478 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges; - -import java.util.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerConfig; -import uk.co.compendiumdev.challenge.challenges.definitions.*; - -public class ChallengeDefinitions { - - Logger logger = LoggerFactory.getLogger(ChallengeDefinitions.class); - - private final List<ChallengeSection> sections; - Map<CHALLENGE, ChallengeDefinitionData> challengeData; - List<ChallengeDefinitionData> orderedChallenges; - - public Collection<ChallengeDefinitionData> getChallenges() { - return orderedChallenges; - } - - public Collection<CHALLENGE> getDefinedChallenges() { - return challengeData.keySet(); - } - - public Collection<ChallengeSection> getChallengeSections() { - return sections; - } - - public ChallengeDefinitions(ChallengerConfig config) { - - challengeData = new HashMap<>(); - orderedChallenges = new ArrayList<>(); - sections = new ArrayList<>(); - - int challengeOrder = 1; - - ChallengeSection getStarted = - new ChallengeSection( - "Getting Started", - "If you want to track your challenge progress, in multi-user mode then you need to solve the challenges in this section to generate a unique ID that we can associate your progress with."); - sections.add(getStarted); - - // create a challenger to persist challenge sessions - storeChallengeAs( - CHALLENGE.CREATE_NEW_CHALLENGER, - ChallengerChallenges.createChallenger201(challengeOrder++), - getStarted); - - ChallengeSection firstChallenge = - new ChallengeSection( - "First Real Challenge", - "For your first challenge, get the list of challenges. You'll be able to use this to see your progress in your API Client, as well as using the GUI."); - sections.add(firstChallenge); - - // GET all challenges as a list - storeChallengeAs( - CHALLENGE.GET_CHALLENGES, - GetChallenges.getChallenges200(challengeOrder++), - firstChallenge); - - ChallengeSection getChallenges = - new ChallengeSection( - "GET Challenges", - "To retrieve, or read information from an API we issue GET requests. This section has a bunch of GET request challenges to try out."); - sections.add(getChallenges); - - storeChallengeAs( - CHALLENGE.GET_TODOS, GetChallenges.getTodos200(challengeOrder++), getChallenges); - storeChallengeAs( - CHALLENGE.GET_TODOS_NOT_PLURAL_404, - GetChallenges.getTodos404(challengeOrder++), - getChallenges); - storeChallengeAs( - CHALLENGE.GET_TODO, GetChallenges.getTodo200(challengeOrder++), getChallenges); - storeChallengeAs( - CHALLENGE.GET_TODO_404, GetChallenges.getTodo404(challengeOrder++), getChallenges); - storeChallengeAs( - CHALLENGE.GET_TODOS_FILTERED, - GetChallenges.getTodosFiltered200(challengeOrder++), - getChallenges); - - // HEAD - ChallengeSection headChallenges = - new ChallengeSection( - "HEAD Challenges", - "A HEAD request, is like a GET request, but only returns the headers and status code."); - sections.add(headChallenges); - - storeChallengeAs( - CHALLENGE.GET_HEAD_TODOS, - HeadChallenges.headTodos200(challengeOrder++), - headChallenges); - - // CREATE with POST - ChallengeSection postCreateChallenges = - new ChallengeSection( - "Creation Challenges with POST", - "A POST request can be used to create and update data, these challenges are to 'create' data."); - sections.add(postCreateChallenges); - - storeChallengeAs( - CHALLENGE.POST_TODOS, - PostChallenges.postTodos201(challengeOrder++), - postCreateChallenges); - storeChallengeAs( - CHALLENGE.POST_TODOS_BAD_DONE_STATUS, - PostChallenges.postTodosBadDoneStatus400(challengeOrder++), - postCreateChallenges); - storeChallengeAs( - CHALLENGE.POST_TODOS_TOO_LONG_TITLE_LENGTH, - PostChallenges.postTodosTitleTooLong400(challengeOrder++), - postCreateChallenges); - storeChallengeAs( - CHALLENGE.POST_TODOS_TOO_LONG_DESCRIPTION_LENGTH, - PostChallenges.postTodosDescriptionTooLong400(challengeOrder++), - postCreateChallenges); - storeChallengeAs( - CHALLENGE.POST_MAX_OUT_TITLE_DESCRIPTION_LENGTH, - PostChallenges.postTodosMaxTitleDescriptionTooLong400(challengeOrder++), - postCreateChallenges); - storeChallengeAs( - CHALLENGE.POST_TODOS_TOO_LONG_PAYLOAD_SIZE, - PostChallenges.postTodosPayloadTooLong400(challengeOrder++), - postCreateChallenges); - storeChallengeAs( - CHALLENGE.POST_TODOS_INVALID_EXTRA_FIELD, - PostChallenges.postTodosInvalidExtraField400(challengeOrder++), - postCreateChallenges); - - // CREATE with PUT - ChallengeSection putCreateChallenges = - new ChallengeSection( - "Creation Challenges with PUT", - "A PUT request can often used to create and update data. The todo application we are using has automatically generated ids, so you cannot use PUT to create."); - sections.add(putCreateChallenges); - - storeChallengeAs( - CHALLENGE.PUT_TODOS_400, - PutChallenges.putTodosId400(challengeOrder++), - putCreateChallenges); - - // UPDATE with POST - ChallengeSection postUpdateChallenges = - new ChallengeSection( - "Update Challenges with POST", - "Use a POST request to amend something that already exists. These are 'partial' content updates so you usually don't need to have all details of the entity in the request, e.g. you could just update a title, or a description, or a status"); - sections.add(postUpdateChallenges); - - storeChallengeAs( - CHALLENGE.POST_UPDATE_TODO, - PostChallenges.postTodosId200(challengeOrder++), - postUpdateChallenges); - storeChallengeAs( - CHALLENGE.POST_TODOS_404, - PostChallenges.postTodosId404(challengeOrder++), - postUpdateChallenges); - - // UPDATE with PUT - ChallengeSection putUpdateChallenges = - new ChallengeSection( - "Update Challenges with PUT", - "A PUT request can be used to amend data. REST Put requests are idempotent, they provide the same result each time."); - sections.add(putUpdateChallenges); - - storeChallengeAs( - CHALLENGE.PUT_TODOS_FULL_200, - PutChallenges.putTodosIdFull200(challengeOrder++), - putUpdateChallenges); - storeChallengeAs( - CHALLENGE.PUT_TODOS_PARTIAL_200, - PutChallenges.putTodosIdPartial200(challengeOrder++), - putUpdateChallenges); - storeChallengeAs( - CHALLENGE.PUT_TODOS_MISSING_TITLE_400, - PutChallenges.putTodosIdNoTitle400(challengeOrder++), - putUpdateChallenges); - storeChallengeAs( - CHALLENGE.PUT_TODOS_400_NO_AMEND_ID, - PutChallenges.putTodosIdNonMatchedIdsAmend400(challengeOrder++), - putUpdateChallenges); - - // DELETE - ChallengeSection deleteChallenges = - new ChallengeSection( - "DELETE Challenges", - "Use a DELETE request to delete an entity. Since this is an extreme request, normally you have to be logged in or authenticated, but we wanted to make life easier for you so we cover authentication later. Anyone can delete To Do items without authentication in this system."); - sections.add(deleteChallenges); - - storeChallengeAs( - CHALLENGE.DELETE_A_TODO, - DeleteChallenges.deleteTodosId200(challengeOrder++), - deleteChallenges); - - // OPTIONS - ChallengeSection optionsChallenges = - new ChallengeSection( - "OPTIONS Challenges", - "Use an OPTIONS verb and check the `Allow` header, this will show you what verbs are allowed to be used on an endpoint. When you test APIs it is worth checking to see if all the verbs listed are allowed or not."); - sections.add(optionsChallenges); - - storeChallengeAs( - CHALLENGE.OPTIONS_TODOS, - OptionsChallenges.optionsTodos200(challengeOrder++), - optionsChallenges); - - ChallengeSection acceptChallenges = - new ChallengeSection( - "Accept Challenges", - "The `Accept` header, tells the server what format you want the response to be in. By changing the `Accept` header you can specify JSON or XML."); - sections.add(acceptChallenges); - - storeChallengeAs( - CHALLENGE.GET_ACCEPT_XML, - GetChallenges.getTodosAcceptXML200(challengeOrder++), - acceptChallenges); - storeChallengeAs( - CHALLENGE.GET_ACCEPT_JSON, - GetChallenges.getTodosAcceptJson200(challengeOrder++), - acceptChallenges); - storeChallengeAs( - CHALLENGE.GET_ACCEPT_ANY_DEFAULT_JSON, - GetChallenges.getTodosAcceptAny200(challengeOrder++), - acceptChallenges); - storeChallengeAs( - CHALLENGE.GET_ACCEPT_XML_PREFERRED, - GetChallenges.getTodosPreferAcceptXML200(challengeOrder++), - acceptChallenges); - storeChallengeAs( - CHALLENGE.GET_JSON_BY_DEFAULT_NO_ACCEPT, - GetChallenges.getTodosNoAccept200(challengeOrder++), - acceptChallenges); - storeChallengeAs( - CHALLENGE.GET_UNSUPPORTED_ACCEPT_406, - GetChallenges.getTodosUnavailableAccept406(challengeOrder++), - acceptChallenges); - - ChallengeSection contentTypeChallenges = - new ChallengeSection( - "Content-Type Challenges", - "The `Content-Type` header, tells the server what format type your 'body' content is, e.g. are you sending XML or JSON."); - sections.add(contentTypeChallenges); - - storeChallengeAs( - CHALLENGE.POST_CREATE_XML, - PostChallenges.postCreateTodoWithXMLAcceptXML(challengeOrder++), - contentTypeChallenges); - storeChallengeAs( - CHALLENGE.POST_CREATE_JSON, - PostChallenges.postCreateTodoWithJsonAcceptJson(challengeOrder++), - contentTypeChallenges); - storeChallengeAs( - CHALLENGE.POST_TODOS_415, - PostChallenges.postCreateUnsupportedContentType415(challengeOrder++), - contentTypeChallenges); - - // adjust the take a break challenges based on the app configuration - ChallengeSection restoreChallenger = - new ChallengeSection( - "Fancy a Break? Restore your session", - "Your challenge progress can be saved, and as long as you remember you challenger ID you can restore it. Leaving a challenger idle in the system for more than 10 minutes will remove the challenger from memory. Challenger status and the todos database can be saved to, and restored from, the browser localStorage."); - sections.add(restoreChallenger); - - // if persistence layer is set to cloud or file then the following apply - if (config.persistenceLayer.willAutoSaveChallengerStatusToPersistenceLayer() - && !config.single_player_mode) { - storeChallengeAs( - CHALLENGE.GET_RESTORE_EXISTING_CHALLENGER, - ChallengerChallenges.getRestoreExistingChallenger200(challengeOrder++), - restoreChallenger); - storeChallengeAs( - CHALLENGE.POST_RESTORE_EXISTING_CHALLENGER, - ChallengerChallenges.postRestoreExistingChallenger200(challengeOrder++), - restoreChallenger); - } - - // GET the restorable version of challenger progress via api - storeChallengeAs( - CHALLENGE.GET_RESTORABLE_CHALLENGER_PROGRESS_STATUS, - ChallengerChallenges.getRestorableExistingChallengerProgress200(challengeOrder++), - restoreChallenger); - // PUT to restore challenger progress via api - storeChallengeAs( - CHALLENGE.PUT_RESTORABLE_CHALLENGER_PROGRESS_STATUS, - ChallengerChallenges.putRestoreChallengerProgress200(challengeOrder++), - restoreChallenger); - - // the create with PUT is only valid in multi-user mode, pass in the mode and exclude this - // challenge - if (!config.single_player_mode) { - storeChallengeAs( - CHALLENGE.PUT_NEW_RESTORED_CHALLENGER_PROGRESS_STATUS, - ChallengerChallenges.putRestoreChallengerProgress201(challengeOrder++), - restoreChallenger); - } - - // GET the restorable version of todos database via api - storeChallengeAs( - CHALLENGE.GET_RESTORABLE_TODOS, - ChallengerChallenges.getRestorableTodos200(challengeOrder++), - restoreChallenger); - - // PUT to restore version of todos via api - storeChallengeAs( - CHALLENGE.PUT_RESTORABLE_TODOS, - ChallengerChallenges.putRestorableTodos204(challengeOrder++), - restoreChallenger); - - // POST mixed content and accept - ChallengeSection mixAcceptContentTypeChallenges = - new ChallengeSection( - "Mix Accept and Content-Type Challenges", - "We can mix the `Accept` and `Content-Type` headers so that we can send JSON but receive XML. These challenges encourage you to explore some combinations."); - sections.add(mixAcceptContentTypeChallenges); - - storeChallengeAs( - CHALLENGE.POST_CREATE_XML_ACCEPT_JSON, - PostChallenges.postTodosXmlToJson201(challengeOrder++), - mixAcceptContentTypeChallenges); - storeChallengeAs( - CHALLENGE.POST_CREATE_JSON_ACCEPT_XML, - PostChallenges.postTodosJsonToXml201(challengeOrder++), - mixAcceptContentTypeChallenges); - - ChallengeSection miscStatusCodes = - new ChallengeSection( - "Status Code Challenges", - "Status-codes are essential to understand, so we created some challenges that help you trigger more status codes. Remember to review httpstatuses.com to learn what the status codes mean."); - sections.add(miscStatusCodes); - - storeChallengeAs( - CHALLENGE.DELETE_HEARTBEAT_405, - StatusCodeChallenges.methodNotAllowed405UsingDelete(challengeOrder++), - miscStatusCodes); - storeChallengeAs( - CHALLENGE.PATCH_HEARTBEAT_500, - StatusCodeChallenges.serverError500UsingPatch(challengeOrder++), - miscStatusCodes); - storeChallengeAs( - CHALLENGE.TRACE_HEARTBEAT_501, - StatusCodeChallenges.notImplemented501UsingTrace(challengeOrder++), - miscStatusCodes); - storeChallengeAs( - CHALLENGE.GET_HEARTBEAT_204, - StatusCodeChallenges.noContent204UsingGet(challengeOrder++), - miscStatusCodes); - - ChallengeSection methodOverrideChallenges = - new ChallengeSection( - "HTTP Method Override Challenges", - "Some HTTP Clients can not send all verbs e.g. PATCH, DELETE, PUT. Use an X-HTTP-Method-Override header to simulate these with a POST request"); - sections.add(methodOverrideChallenges); - - storeChallengeAs( - CHALLENGE.OVERRIDE_DELETE_HEARTBEAT_405, - StatusCodeChallenges.overridePostToDeleteFor405(challengeOrder++), - methodOverrideChallenges); - storeChallengeAs( - CHALLENGE.OVERRIDE_PATCH_HEARTBEAT_500, - StatusCodeChallenges.overridePostToPatchFor500(challengeOrder++), - methodOverrideChallenges); - storeChallengeAs( - CHALLENGE.OVERRIDE_TRACE_HEARTBEAT_501, - StatusCodeChallenges.overridePostToTraceFor501(challengeOrder++), - methodOverrideChallenges); - - // authorization and authentication - ChallengeSection authenticationChallenges = - new ChallengeSection( - "Authentication Challenges", - "Authentication is telling the system who you are. In multi-user mode you are already doing that with the X-CHALLENGER header, but we have added an extra level of security on the /secret section. So first Authenticate with Basic Authentication to find out the token to use for authorisation for later challenges."); - sections.add(authenticationChallenges); - - storeChallengeAs( - CHALLENGE.CREATE_SECRET_TOKEN_401, - SecretTokenChallenges.createSecretTokenNotAuthenticated401(challengeOrder++), - authenticationChallenges); - storeChallengeAs( - CHALLENGE.CREATE_SECRET_TOKEN_201, - SecretTokenChallenges.createSecretTokenAuthenticated201(challengeOrder++), - authenticationChallenges); - - ChallengeSection authorizationChallenges = - new ChallengeSection( - "Authorization Challenges", - "Once the system knows who you are, authorization is if you have the correct level of access. In these challenges the authorization is granted using a custom API header X-AUTH-TOKEN or using a Bearer Authorization header."); - sections.add(authorizationChallenges); - - storeChallengeAs( - CHALLENGE.GET_SECRET_NOTE_403, - SecretTokenChallenges.forbiddenNotAuthorized403(challengeOrder++), - authorizationChallenges); - storeChallengeAs( - CHALLENGE.GET_SECRET_NOTE_401, - SecretTokenChallenges.invalidRequestNoAuthHeader401(challengeOrder++), - authorizationChallenges); - storeChallengeAs( - CHALLENGE.GET_SECRET_NOTE_200, - SecretTokenChallenges.authorizedGet200(challengeOrder++), - authorizationChallenges); - storeChallengeAs( - CHALLENGE.POST_SECRET_NOTE_200, - SecretTokenChallenges.authorizedUpdate200(challengeOrder++), - authorizationChallenges); - storeChallengeAs( - CHALLENGE.POST_SECRET_NOTE_401, - SecretTokenChallenges.postMissingTokenAuth401(challengeOrder++), - authorizationChallenges); - storeChallengeAs( - CHALLENGE.POST_SECRET_NOTE_403, - SecretTokenChallenges.postInvalidTokenAuth401(challengeOrder++), - authorizationChallenges); - storeChallengeAs( - CHALLENGE.GET_SECRET_NOTE_BEARER_200, - SecretTokenChallenges.getWithValidBearerToken200(challengeOrder++), - authorizationChallenges); - storeChallengeAs( - CHALLENGE.POST_SECRET_NOTE_BEARER_200, - SecretTokenChallenges.postUpdateWithValidBearerToken200(challengeOrder++), - authorizationChallenges); - - // misc - - ChallengeSection miscChallenges = - new ChallengeSection( - "Miscellaneous Challenges", - "We left these challenges to the end because they seemed fun, but... different."); - sections.add(miscChallenges); - - storeChallengeAs( - CHALLENGE.DELETE_ALL_TODOS, - MiscChallenges.deleteAllTodos200(challengeOrder++), - miscChallenges); - storeChallengeAs( - CHALLENGE.POST_ALL_TODOS, - MiscChallenges.postAllTodos201(challengeOrder), - miscChallenges); - - Set challengeNames = new HashSet(); - for (ChallengeDefinitionData challenge : orderedChallenges) { - logger.info("Setup Challenge: " + challenge.name); - challengeNames.add(challenge.name); - } - int sectionChallengesCount = 0; - for (ChallengeSection section : sections) { - sectionChallengesCount += section.getChallenges().size(); - } - if (sectionChallengesCount != orderedChallenges.size()) { - throw new RuntimeException( - "Number challenges in sections, does not match number of challenges" - + ", possibly forgot to add section or challenge to section"); - } - if (challengeNames.size() != orderedChallenges.size()) { - throw new RuntimeException( - "Number of names, does not match number of challenges" - + ", possible duplicate name"); - } - } - - private ChallengeDefinitionData storeChallengeAs( - final CHALLENGE id, final ChallengeDefinitionData challenge, ChallengeSection section) { - - challengeData.put(id, challenge); - orderedChallenges.add(challenge); - section.addChallenge(challenge); - - return challenge; - } - - public CHALLENGE getChallenge(final String name) { - for (Map.Entry<CHALLENGE, ChallengeDefinitionData> challenge : challengeData.entrySet()) { - if (challenge.getValue().name.contentEquals(name)) { - return challenge.getKey(); - } - } - return null; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeHint.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeHint.java deleted file mode 100644 index daaba771..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeHint.java +++ /dev/null @@ -1,11 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges; - -public class ChallengeHint { - public final String hintText; - public final String hintLink; - - public ChallengeHint(final String hintText, final String hintLink) { - this.hintText = hintText; - this.hintLink = hintLink; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeSection.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeSection.java deleted file mode 100644 index edaf9568..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeSection.java +++ /dev/null @@ -1,32 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges; - -import java.util.ArrayList; -import java.util.List; - -public class ChallengeSection { - private final String title; - private final String description; - private final ArrayList<ChallengeDefinitionData> challenges; - - public ChallengeSection(final String title, final String description) { - this.title = title; - this.description = description; - this.challenges = new ArrayList<ChallengeDefinitionData>(); - } - - public void addChallenge(final ChallengeDefinitionData challenge) { - challenges.add(challenge); - } - - public String getTitle() { - return title; - } - - public String getDescription() { - return description; - } - - public List<ChallengeDefinitionData> getChallenges() { - return this.challenges; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeSolutionLink.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeSolutionLink.java deleted file mode 100644 index fff5f709..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeSolutionLink.java +++ /dev/null @@ -1,41 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges; - -public class ChallengeSolutionLink { - - public final String linkText; - public final String linkType; - public final String linkData; - - public ChallengeSolutionLink( - final String linkText, final String linkType, final String linkUrl) { - this.linkText = linkText.trim(); - - if (linkType == null) { - this.linkType = ""; - } else { - this.linkType = linkType.trim().toUpperCase(); - } - - if (linkUrl == null) { - this.linkData = ""; - } else { - this.linkData = linkUrl.trim(); - } - } - - public String asHtmlAHref() { - if (linkType.equals("YOUTUBE")) { - return String.format( - "<a href='https://youtu.be/%s' target='_blank'>%s</a>", linkData, linkText); - } - if (linkData.isEmpty()) { - return linkText; - } - - String target = "target='_blank'"; - if (!linkData.startsWith("http")) { - target = ""; - } - return String.format("<a href='%s' %s>%s</a>", linkData, target, linkText); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/ChallengeRenderer.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/ChallengeRenderer.java deleted file mode 100644 index 68b1ebc3..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/ChallengeRenderer.java +++ /dev/null @@ -1,8 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -public class ChallengeRenderer { - - public static String renderChallengeNumber(int challengeOrder) { - return String.format("%02d", challengeOrder); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/ChallengerChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/ChallengerChallenges.java deleted file mode 100644 index e24dc0e4..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/ChallengerChallenges.java +++ /dev/null @@ -1,192 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class ChallengerChallenges { - - public static ChallengeDefinitionData createChallenger201(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /challenger (201)", - "Issue a POST request on the `/challenger` end point, with no body, to create a new challenger session. Use the generated X-CHALLENGER header in future requests to track challenge completion."); - aChallenge.addHint( - "In multi-user mode, you need to create an X-CHALLENGER Session in order to complete any challenges or make PUT, POST, DELETE requests", - "/gui/multiuser"); - aChallenge.addSolutionLink( - "Send request using POST to /challenger endpoint. The response has an X-CHALLENGER header, add this header X-CHALLENGER and the GUID value to all future requests.", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/create-session/post-challenger-201"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw"); - return aChallenge; - } - - // challenge to GET /challenger/{guid} and restore a challenger session - public static ChallengeDefinitionData getRestoreExistingChallenger200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /challenger/guid (200)", - "Issue a GET request on the `/challenger` end point with an existing challenger GUID to restore that challenger's progress into memory."); - aChallenge.addHint( - "In multi-user mode, you need to create an X-CHALLENGER Session first and let it go idle so it is removed in the 10 minute purge", - "/gui/multiuser"); - aChallenge.addHint("Remember to add the X-CHALLENGER header to track your progress", ""); - aChallenge.addHint("Add the guid in the URL as the last part of the path", ""); - - aChallenge.addSolutionLink( - "GET /challenger/{guid} for a challenger previously saved in the persistence store", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/manage-session/save-restore-session"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw"); - return aChallenge; - } - - // challenge to POST /challenger with X-CHALLENGER header of existing challenger to restore a - // challenger - - public static ChallengeDefinitionData postRestoreExistingChallenger200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /challenger (existing X-CHALLENGER)", - "Issue a POST request on the `/challenger` end point, with an existing challenger GUID as the X-CHALLENGER header to restore that challenger's progress into memory."); - aChallenge.addHint( - "In multi-user mode, you need to create an X-CHALLENGER Session first and let it go idle so it is removed in the 10 minute purge", - "/gui/multiuser"); - - aChallenge.addSolutionLink( - "POST /challenger with the challenger GUID in the X-CHALLENGER header for a challenger previously saved in the persistence store", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/manage-session/save-restore-session"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw"); - - return aChallenge; - } - - public static ChallengeDefinitionData getRestorableExistingChallengerProgress200( - int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /challenger/guid (existing X-CHALLENGER)", - "Issue a GET request on the `/challenger/{guid}` end point, with an existing challenger GUID. This will return the progress data payload that can be used to later restore your progress to this status."); - aChallenge.addHint("A challenger must have been created already for this to work", ""); - aChallenge.addHint("Remember to add the X-CHALLENGER header to track your progress", ""); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/manage-session/save-restore-session"); - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw"); - - return aChallenge; - } - - public static ChallengeDefinitionData putRestoreChallengerProgress200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "PUT /challenger/guid RESTORE", - "Issue a PUT request on the `/challenger/{guid}` end point, with an existing challenger GUID to restore that challenger's progress into memory."); - aChallenge.addHint("Use the challenger payload returned from the earlier GET request", ""); - aChallenge.addHint("Remember to add the X-CHALLENGER header to track your progress", ""); - aChallenge.addHint( - "The challenger should already exist in memory and this will restore status to an earlier point", - ""); - - aChallenge.addSolutionLink( - "Using the payload from the earlier 'GET /challenger/guid' request, use PUT to reset the challenger progress", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/manage-session/save-restore-session"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw"); - - return aChallenge; - } - - public static ChallengeDefinitionData putRestoreChallengerProgress201(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "PUT /challenger/guid CREATE", - "Issue a PUT request on the `/challenger/{guid}` end point, with a challenger GUID not currently in memory to restore that challenger's progress into memory."); - aChallenge.addHint("Use the challenger payload returned from the earlier GET request", ""); - aChallenge.addHint("Remember to add the X-CHALLENGER header to track your progress", ""); - aChallenge.addHint( - "This will create the Challenger in memory because it should not already exist", - ""); - - aChallenge.addSolutionLink( - "Using the payload from the earlier 'GET /challenger/guid' request, use PUT to reset the challenger progress", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/manage-session/save-restore-session"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw"); - - return aChallenge; - } - - public static ChallengeDefinitionData getRestorableTodos200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /challenger/database/guid (200)", - "Issue a GET request on the `/challenger/database/{guid}` end point, to retrieve the current todos database for the user. You can use this to restore state later."); - aChallenge.addHint("Remember to add the X-CHALLENGER header to track your progress", ""); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/manage-session/save-restore-session"); - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw"); - - return aChallenge; - } - - public static ChallengeDefinitionData putRestorableTodos204(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "PUT /challenger/database/guid (Update)", - "Issue a PUT request on the `/challenger/database/{guid}` end point, with a payload to restore the Todos database in memory."); - aChallenge.addHint( - "Use the Todos database payload returned from the earlier GET request", ""); - aChallenge.addHint("Remember to add the X-CHALLENGER header to track your progress", ""); - - aChallenge.addSolutionLink( - "Using the payload from the earlier 'GET /challenger/database/guid' request, use PUT to reset the challenger todos data", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/manage-session/save-restore-session"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw"); - - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/DeleteChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/DeleteChallenges.java deleted file mode 100644 index e0de1024..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/DeleteChallenges.java +++ /dev/null @@ -1,24 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class DeleteChallenges { - - public static ChallengeDefinitionData deleteTodosId200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "DELETE /todos/{id} (200)", - "Issue a DELETE request to successfully delete a todo"); - - aChallenge.addHint( - "Make sure you don't use {id} in the url, replace that with the id of a todo e.g. /todos/1"); - aChallenge.addHint("Make sure a todo with the id exists prior to issuing the request"); - aChallenge.addHint("Check it was deleted by issuing a GET or HEAD on the /todos/{id}"); - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/delete/delete-todos-id-200"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "6MXTkaXn9qU"); - - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/GetChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/GetChallenges.java deleted file mode 100644 index 3379aca4..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/GetChallenges.java +++ /dev/null @@ -1,221 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class GetChallenges { - - public static ChallengeDefinitionData getChallenges200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /challenges (200)", - "Issue a GET request on the `/challenges` end point"); - - aChallenge.addHint( - "Remember to add the X-CHALLENGER header so you see the progress of the challenges for your session.", - ""); - aChallenge.addHint( - "If you issue a GET request without an X-CHALLENGER header you will see the default challenge values.", - ""); - aChallenge.addHint("By default the response body will be JSON format.", ""); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/first-challenge/get-challenges-200"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "DrAjk2NaPRo"); - - return aChallenge; - } - - public static ChallengeDefinitionData getTodos200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos (200)", - "Issue a GET request on the `/todos` end point"); - - aChallenge.addHint( - "Remember to add the X-CHALLENGER header so you see the data for your session.", - ""); - aChallenge.addHint( - "If you issue a GET request without an X-CHALLENGER header you will see the default todo values.", - ""); - aChallenge.addHint("By default the response body will be JSON format.", ""); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/get/get-todos-200"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "OpisB0UZq0c"); - - return aChallenge; - } - - public static ChallengeDefinitionData getTodos404(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todo (404) not plural", - "Issue a GET request on the `/todo` end point should 404 because nouns should be plural"); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/get/get-todo-404"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "gAJzqgcN9dc"); - - return aChallenge; - } - - public static ChallengeDefinitionData getTodo200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos/{id} (200)", - "Issue a GET request on the `/todos/{id}` end point to return a specific todo"); - - aChallenge.addHint( - "Make sure you don't use {id} in the url, replace that with the id of a todo e.g. /todos/1"); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/get/get-todos-id-200"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "JDbbSY3U_rY"); - - return aChallenge; - } - - public static ChallengeDefinitionData getTodo404(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos/{id} (404)", - "Issue a GET request on the `/todos/{id}` end point for a todo that does not exist"); - - aChallenge.addHint( - "Make sure you don't use {id} in the url, replace that with the id of a todo e.g. /todos/1"); - aChallenge.addHint("Make sure the id is an integer e.g. /todos/1"); - aChallenge.addHint("Make sure you are using the /todos end point e.g. /todos/1"); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/get/get-todos-id-404"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "1S5kpd8-xfM"); - - return aChallenge; - } - - public static ChallengeDefinitionData getTodosFiltered200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos (200) ?filter", - "Issue a GET request on the `/todos` end point with a query filter to get only todos which are 'done'. There must exist both 'done' and 'not done' todos, to pass this challenge."); - - aChallenge.addHint("A query filter is a URL parameter using the field name and a value"); - aChallenge.addHint( - "A URL parameter is added to the end of a url with a ? e.g. /todos?id=1"); - aChallenge.addHint( - "To filter on 'done' we use the 'doneStatus' field ? e.g. ?doneStatus=true"); - aChallenge.addHint("Make sure there are todos which are done, and not yet done"); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/get/get-todos-200-filter"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "G-sLuhyPMuw"); - return aChallenge; - } - - /* - ACCEPT HEADERS - */ - // GET accept type - // specify accept type - XML - // specify accept type - JSON - // specify accept type - */* (ANY) to get default - // specify multiple accept type with a preference for XML - should receive XML - // none specified - get default - // cannot supply accepted type 406 - - public static ChallengeDefinitionData getTodosAcceptXML200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos (200) XML", - "Issue a GET request on the `/todos` end point with an `Accept` header of `application/xml` to receive results in XML format"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/accept-header/get-todos-200-xml"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "cLeEuZm2VG8"); - return aChallenge; - } - - public static ChallengeDefinitionData getTodosAcceptJson200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos (200) JSON", - "Issue a GET request on the `/todos` end point with an `Accept` header of `application/json` to receive results in JSON format"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/accept-header/get-todos-200-json"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "79JTHiby2Qw"); - return aChallenge; - } - - public static ChallengeDefinitionData getTodosAcceptAny200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos (200) ANY", - "Issue a GET request on the `/todos` end point with an `Accept` header of `*/*` to receive results in default JSON format"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/accept-header/get-todos-200-any"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "O4DhJ8Ohkk8"); - return aChallenge; - } - - public static ChallengeDefinitionData getTodosPreferAcceptXML200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos (200) XML pref", - "Issue a GET request on the `/todos` end point with an `Accept` header of `application/xml, application/json` to receive results in the preferred XML format"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/accept-header/get-todos-200-xml-pref"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "sLChuy9pc9U"); - return aChallenge; - } - - public static ChallengeDefinitionData getTodosNoAccept200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos (200) no accept", - "Issue a GET request on the `/todos` end point with no `Accept` header present in the message to receive results in default JSON format"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/accept-header/get-todos-200-no-accept"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "CSVP2PcvOdg"); - return aChallenge; - } - - public static ChallengeDefinitionData getTodosUnavailableAccept406(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /todos (406)", - "Issue a GET request on the `/todos` end point with an `Accept` header `application/gzip` to receive 406 'NOT ACCEPTABLE' status code"); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/accept-header/get-todos-406"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "QzfbegkY1ok"); - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/HeadChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/HeadChallenges.java deleted file mode 100644 index 13f645fa..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/HeadChallenges.java +++ /dev/null @@ -1,19 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class HeadChallenges { - - public static ChallengeDefinitionData headTodos200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "HEAD /todos (200)", - "Issue a HEAD request on the `/todos` end point"); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/head/head-todos-200"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "zKbytTelP84"); - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/MiscChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/MiscChallenges.java deleted file mode 100644 index 86db3ae7..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/MiscChallenges.java +++ /dev/null @@ -1,42 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class MiscChallenges { - - // TODO: create solution and video for Post all - public static ChallengeDefinitionData postAllTodos201(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos (201) all", - "Issue as many POST requests as it takes to add the maximum number of TODOS allowed for a user. The maximum number should be listed in the documentation."); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/miscellaneous/create-maximum-number-todos"); - - return aChallenge; - } - - public static ChallengeDefinitionData deleteAllTodos200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "DELETE /todos/{id} (200) all", - "Issue a DELETE request to successfully delete the last todo in system so that there are no more todos in the system"); - - aChallenge.addHint( - "After deleting the last todo, there will be no todos left in the application"); - aChallenge.addHint( - "Make sure you don't use {id} in the url, replace that with the id of a todo e.g. /todos/1"); - aChallenge.addHint( - "You have to delete all the todo items in the system to complete this challenge"); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/miscellaneous/delete-all-todos"); - - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/OptionsChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/OptionsChallenges.java deleted file mode 100644 index 7ec5e87f..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/OptionsChallenges.java +++ /dev/null @@ -1,20 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class OptionsChallenges { - - public static ChallengeDefinitionData optionsTodos200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "OPTIONS /todos (200)", - "Issue an OPTIONS request on the `/todos` end point. You might want to manually check the 'Allow' header in the response is as expected."); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/options/options-todos-200"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "Ld5h1TSnXWA"); - - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/PostChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/PostChallenges.java deleted file mode 100644 index 834e6638..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/PostChallenges.java +++ /dev/null @@ -1,303 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class PostChallenges { - - /* - CREATE TODOS - */ - public static ChallengeDefinitionData postTodos201(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos (201)", - "Issue a POST request to successfully create a todo"); - - aChallenge.addHint("Add a JSON payload in the request", ""); - aChallenge.addHint( - "If you don't know the format of the payload, use the response from a GET /todos/{id} request and amend it", - ""); - aChallenge.addHint("You must add an X-CHALLENGER header for a valid session", ""); - - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/post-create/post-todos-201"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA"); - return aChallenge; - } - - public static ChallengeDefinitionData postTodosTitleTooLong400(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos (400) title too long", - "Issue a POST request to create a todo but fail length validation on the `title` field because your title exceeds maximum allowable characters."); - - aChallenge.addHint( - "The API Documentation shows the maximum allowed length of the title field", ""); - - aChallenge.addSolutionLink( - "Send a POST request to /todos with a title longer than 50 characters", "", ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/post-create/post-todos-400-title-too-long"); - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tlye5bQ72g0"); - // TODO: create solution for failing title too long - - return aChallenge; - } - - public static ChallengeDefinitionData postTodosBadDoneStatus400(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos (400) doneStatus", - "Issue a POST request to create a todo but fail validation on the `doneStatus` field"); - - aChallenge.addHint( - "doneStatus should be boolean, an invalid status would be a String or a number e.g. \"invalid\""); - - aChallenge.addSolutionLink( - "Send a POST request to /todos with a non-boolean `doneStatus` e.g. {\"title\":\"a title\",\"doneStatus\":\"invalid\"}", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/post-create/post-todos-400"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tlye5bQ72g0"); - return aChallenge; - } - - public static ChallengeDefinitionData postTodosDescriptionTooLong400(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos (400) description too long", - "Issue a POST request to create a todo but fail length validation on the `description` because your description exceeds maximum allowable characters."); - - aChallenge.addHint( - "The API Documentation shows the maximum allowed length of the description field", - ""); - - aChallenge.addSolutionLink( - "Send a POST request to /todos with a description longer than 200 characters", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/post-create/post-todos-400-description-too-long"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tlye5bQ72g0"); - // TODO: create video solution for failing description too long - return aChallenge; - } - - public static ChallengeDefinitionData postTodosMaxTitleDescriptionTooLong400( - int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos (201) max out content", - "Issue a POST request to create a todo with maximum length title and description fields."); - - aChallenge.addHint("Max lengths are listed in the API Documentation"); - aChallenge.addHint( - "CounterStrings are very useful for testing with maximum field lengths", - "https://eviltester.github.io/TestingApp/apps/counterstrings/counterstrings.html"); - aChallenge.addHint("Both title and description should be the correct maximum lengths"); - - aChallenge.addSolutionLink( - "Send a POST request to /todos with a description of 200 characters and a title with 50 characters", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/post-create/post-todos-201-max-content"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tlye5bQ72g0"); - // TODO: create video solution for max out title and description - return aChallenge; - } - - public static ChallengeDefinitionData postTodosPayloadTooLong400(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos (413) content too long", - "Issue a POST request to create a todo but fail payload length validation on the `description` because your whole payload exceeds maximum allowable 5000 characters."); - - aChallenge.addHint("Try using a long 5000 char string as the description or title text"); - aChallenge.addHint( - "CounterStrings are very useful for testing with maximum field lengths", - "https://eviltester.github.io/TestingApp/apps/counterstrings/counterstrings.html"); - - aChallenge.addSolutionLink( - "Send a POST request to /todos with a description of 5000 characters in length", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/post-create/post-todos-413-content-too-long"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tlye5bQ72g0"); - // TODO: create video solution for failing content too long - return aChallenge; - } - - public static ChallengeDefinitionData postTodosInvalidExtraField400(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos (400) extra", - "Issue a POST request to create a todo but fail validation because your payload contains an unrecognised field."); - - aChallenge.addHint("Try to create a todo with a title, description and a priority"); - - aChallenge.addSolutionLink( - "Send a POST request to /todos with a priority field e.g. {\"title\":\"a title\",\"priority\":\"extra\"}", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/post-create/post-todos-400-extra-field"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tlye5bQ72g0"); - // TODO: create video solution for unrecognised field names - return aChallenge; - } - - /* - UPDATE TODOs - */ - - public static ChallengeDefinitionData postTodosId200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos/{id} (200)", - "Issue a POST request to successfully update a todo"); - - aChallenge.addHint( - "Make sure you don't use {id} in the url, replace that with the id of a todo e.g. /todos/1"); - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/post-update/post-todos-id-200"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "feXdRpZ_tgs"); - return aChallenge; - } - - public static ChallengeDefinitionData postTodosId404(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos/{id} (404)", - "Issue a POST request for a todo which does not exist. Expect to receive a 404 response."); - - aChallenge.addHint( - "Make sure you don't use {id} in the url, replace that with the id of a todo that does not exist e.g. /todos/100"); - - aChallenge.addSolutionLink( - "Send a POST request to /todos/{id} with a valid update payload where {id} does not exist", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/post-update/post-todos-id-404"); - - // todo add video solution and hints for POST 404 - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "feXdRpZ_tgs"); - // return aChallenge; - return aChallenge; - } - - /* - CONTENT TYPE - */ - - // POST control content type, and Accepting only XML ie. Accept header of `application/xml` - // control content type to create with - XML - public static ChallengeDefinitionData postCreateTodoWithXMLAcceptXML(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos XML", - "Issue a POST request on the `/todos` end point to create a todo using Content-Type `application/xml`, and Accepting only XML ie. Accept header of `application/xml`"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/content-type-header/post-todos-xml"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "2-KBYHwb7MM"); - return aChallenge; - } - - // control content type to create with - JSON - public static ChallengeDefinitionData postCreateTodoWithJsonAcceptJson(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos JSON", - "Issue a POST request on the `/todos` end point to create a todo using Content-Type `application/json`, and Accepting only JSON ie. Accept header of `application/json`"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/content-type-header/post-todos-json"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "VS9qIhgp51Q"); - return aChallenge; - } - - // content type not supported 415 e.g. form encoded - public static ChallengeDefinitionData postCreateUnsupportedContentType415(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos (415)", - "Issue a POST request on the `/todos` end point with an unsupported content type to generate a 415 status code"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/content-type-header/post-todos-415"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "L8H-vkbXyr0"); - return aChallenge; - } - - /* - MIXED CONTENT AND ACCEPT TYPES - */ - - // content type XML - accept type JSON - public static ChallengeDefinitionData postTodosXmlToJson201(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos XML to JSON", - "Issue a POST request on the `/todos` end point to create a todo using Content-Type `application/xml` but Accept `application/json`"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/mix-accept-content/post-xml-accept-json"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "kfe7VtaV7u0"); - return aChallenge; - } - - // content type JSON - accept type XML - public static ChallengeDefinitionData postTodosJsonToXml201(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /todos JSON to XML", - "Issue a POST request on the `/todos` end point to create a todo using Content-Type `application/json` but Accept `application/xml`"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/mix-accept-content/post-json-accept-xml"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "uw1Jq8t1em4"); - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/PutChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/PutChallenges.java deleted file mode 100644 index ed38339a..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/PutChallenges.java +++ /dev/null @@ -1,139 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class PutChallenges { - - public static ChallengeDefinitionData putTodosId400(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "PUT /todos/{id} (400)", - "Issue a PUT request to unsuccessfully create a todo"); - - aChallenge.addHint("Add a JSON payload in the request", ""); - aChallenge.addHint( - "If you don't know the format of the payload, use the response from a GET /todos/{id} request and amend it", - ""); - aChallenge.addHint("Do not include an 'id' in the payload", ""); - aChallenge.addHint("The id in the URL should not exist", ""); - aChallenge.addHint("You must add an X-CHALLENGER header for a valid session", ""); - - aChallenge.addSolutionLink( - "Send a PUT request to /todos/{id} with a valid creation payload", "", ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/put-create/put-todos-400-create"); - - // todo: create video solution for PUT todos 400 challenge - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA"); - return aChallenge; - } - - /* - UPDATE TODO CHALLENGES - */ - - public static ChallengeDefinitionData putTodosIdFull200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "PUT /todos/{id} full (200)", - "Issue a PUT request to update an existing todo with a complete payload i.e. title, description and donestatus."); - - aChallenge.addHint("Add a JSON payload in the request", ""); - aChallenge.addHint( - "If you don't know the format of the payload, use the response from a GET /todos/{id} request and amend it", - ""); - aChallenge.addHint("Do not include an 'id' in the payload", ""); - - aChallenge.addSolutionLink( - "Send a PUT request to /todos/{id} with a full payload. Do not attempt to change the id.", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/put-update/put-todos-id-200-update-full"); - - // todo: create solution video for PUT todos full 200 challenge - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA"); - return aChallenge; - } - - public static ChallengeDefinitionData putTodosIdPartial200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "PUT /todos/{id} partial (200)", - "Issue a PUT request to update an existing todo with just mandatory items in payload i.e. title."); - - aChallenge.addHint("Add a JSON payload in the request", ""); - aChallenge.addHint( - "If you don't know the format of the payload, use the response from a GET /todos/{id} request and amend it", - ""); - aChallenge.addHint("Do not include an 'id' in the payload", ""); - - aChallenge.addSolutionLink( - "Send a PUT request to /todos/{id} with a partial payload. Mandatory field title must be included.", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/put-update/put-todos-id-200-update-partial"); - - // todo: create solution video for PUT todos partial 200 challenge - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA"); - return aChallenge; - } - - public static ChallengeDefinitionData putTodosIdNoTitle400(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "PUT /todos/{id} no title (400)", - "Issue a PUT request to fail to update an existing todo because title is missing in payload."); - - aChallenge.addHint( - "Title is required for Put requests because they are idempotent. You can amend using POST without a title, but not using a PUT."); - - aChallenge.addSolutionLink( - "Send a PUT request to /todos/{id} without a title field in the payload.", "", ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/put-update/put-todos-id-400-no-title"); - - // todo: create solution video for PUT todos partial 200 challenge - - return aChallenge; - } - - public static ChallengeDefinitionData putTodosIdNonMatchedIdsAmend400(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "PUT /todos/{id} no amend id (400)", - "Issue a PUT request to fail to update an existing todo because id different in payload."); - - // todo: create solution for PUT todos partial 200 challenge - aChallenge.addHint("ID is auto generated you can not amend it in the payload."); - aChallenge.addHint( - "If you have a different id in the payload from the url then this is viewed as an amendment and you can not amend an auto generated field."); - - aChallenge.addSolutionLink( - "Send a PUT request to /todos/{id} with a different id in the url than in the payload.", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/put-update/put-todos-id-400-no-amend-id"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA"); - // TODO: add solution video for put totos id no amend id - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/SecretTokenChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/SecretTokenChallenges.java deleted file mode 100644 index ab01d6e0..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/SecretTokenChallenges.java +++ /dev/null @@ -1,172 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class SecretTokenChallenges { - - // POST /secret/token with incorrect username and password credentials get 401 - public static ChallengeDefinitionData createSecretTokenNotAuthenticated401(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /secret/token (401)", - "Issue a POST request on the `/secret/token` end point and receive 401 when Basic auth username/password is not admin/password"); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/authentication/post-secret-401"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "RSQGADU3SLA"); - return aChallenge; - } - - // POST /secret/token with correct username and password credentials get secret token 201 - public static ChallengeDefinitionData createSecretTokenAuthenticated201(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /secret/token (201)", - "Issue a POST request on the `/secret/token` end point and receive 201 when Basic auth username/password is admin/password"); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", "HREF", "/apichallenges/solutions/authentication/post-secret-201"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "J2GQiuEfHkI"); - return aChallenge; - } - - // GET /secret/note with no token and 403 - public static ChallengeDefinitionData forbiddenNotAuthorized403(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /secret/note (403)", - "Issue a GET request on the `/secret/note` end point and receive 403 when X-AUTH-TOKEN does not match a valid token"); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/authorization/get-secret-note-403"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "77mnUQezdas"); - return aChallenge; - } - - // GET /secret/note with invalid token and 401 - public static ChallengeDefinitionData invalidRequestNoAuthHeader401(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /secret/note (401)", - "Issue a GET request on the `/secret/note` end point and receive 401 when no X-AUTH-TOKEN header present"); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/authorization/get-secret-note-401"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "__uZlQZ48io"); - return aChallenge; - } - - // GET /secret/note with token and see token - public static ChallengeDefinitionData authorizedGet200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /secret/note (200)", - "Issue a GET request on the `/secret/note` end point receive 200 when valid X-AUTH-TOKEN used - response body should contain the note"); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/authorization/get-secret-note-200"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "2uRpzr2OmEY"); - return aChallenge; - } - - // POST /secret/note with token and update secret note - public static ChallengeDefinitionData authorizedUpdate200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /secret/note (200)", - "Issue a POST request on the `/secret/note` end point with a note payload e.g. {\"note\":\"my note\"} and receive 200 when valid X-AUTH-TOKEN used. Note is maximum length 100 chars and will be truncated when stored."); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/authorization/post-secret-note-200"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "A9T9yjzEOEE"); - return aChallenge; - } - - // POST /secret/note with invalid token and 401 - public static ChallengeDefinitionData postMissingTokenAuth401(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /secret/note (401)", - "Issue a POST request on the `/secret/note` end point with a note payload {\"note\":\"my note\"} and receive 401 when no X-AUTH-TOKEN present"); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/authorization/post-secret-note-401-403"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "76U5TEvLzLI"); - return aChallenge; - } - - // POST /secret/note with no token and 403 - public static ChallengeDefinitionData postInvalidTokenAuth401(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /secret/note (403)", - "Issue a POST request on the `/secret/note` end point with a note payload {\"note\":\"my note\"} and receive 403 when X-AUTH-TOKEN does not match a valid token"); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/authorization/post-secret-note-401-403"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "76U5TEvLzLI"); - return aChallenge; - } - - // GET /secret/note with bearer token authorization - public static ChallengeDefinitionData getWithValidBearerToken200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /secret/note (Bearer)", - "Issue a GET request on the `/secret/note` end point receive 200 when using the X-AUTH-TOKEN value as an Authorization Bearer token - response body should contain the note"); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/authorization/get-post-secret-note-bearer"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "8GsMTZxEItw"); - return aChallenge; - } - - // POST /secret/note with token and update secret note - public static ChallengeDefinitionData postUpdateWithValidBearerToken200(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /secret/note (Bearer)", - "Issue a POST request on the `/secret/note` end point with a note payload e.g. {\"note\":\"my note\"} and receive 200 when valid X-AUTH-TOKEN value used as an Authorization Bearer token. Status code 200 received. Note is maximum length 100 chars and will be truncated when stored."); - - aChallenge.addHint("Remember to add your X-CHALLENGER guid header"); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/authorization/get-post-secret-note-bearer"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "8GsMTZxEItw"); - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/StatusCodeChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/StatusCodeChallenges.java deleted file mode 100644 index 2cbfe445..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/StatusCodeChallenges.java +++ /dev/null @@ -1,138 +0,0 @@ -package uk.co.compendiumdev.challenge.challenges.definitions; - -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitionData; - -public class StatusCodeChallenges { - - // extra status code challenges - // method not allowed - 405 - public static ChallengeDefinitionData methodNotAllowed405UsingDelete(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "DELETE /heartbeat (405)", - "Issue a DELETE request on the `/heartbeat` end point and receive 405 (Method Not Allowed)"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/status-codes/status-codes-405-500-501-204"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); - return aChallenge; - } - - // cannot process request server error 500 - public static ChallengeDefinitionData serverError500UsingPatch(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "PATCH /heartbeat (500)", - "Issue a PATCH request on the `/heartbeat` end point and receive 500 (internal server error)"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/status-codes/status-codes-405-500-501-204"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); - return aChallenge; - } - - // 501 - public static ChallengeDefinitionData notImplemented501UsingTrace(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "TRACE /heartbeat (501)", - "Issue a TRACE request on the `/heartbeat` end point and receive 501 (Not Implemented)"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/status-codes/status-codes-405-500-501-204"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); - return aChallenge; - } - - // No Content 204 - ping - public static ChallengeDefinitionData noContent204UsingGet(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "GET /heartbeat (204)", - "Issue a GET request on the `/heartbeat` end point and receive 204 when server is running"); - - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/status-codes/status-codes-405-500-501-204"); - aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); - return aChallenge; - } - - /* - Status codes using method overrides - */ - public static ChallengeDefinitionData overridePostToPatchFor500(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /heartbeat as PATCH (500)", - "Issue a POST request on the `/heartbeat` end point and receive 500 when you override the Method Verb to a PATCH"); - - aChallenge.addHint("Use a normal POST request, but add an X-HTTP-Method-Override header"); - - aChallenge.addSolutionLink( - "Add a header 'X-HTTP-Method-Override: PATCH' to a POST /heartbeat request", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/method-override/all-method-overrides"); - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); - return aChallenge; - } - - public static ChallengeDefinitionData overridePostToDeleteFor405(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /heartbeat as DELETE (405)", - "Issue a POST request on the `/heartbeat` end point and receive 405 when you override the Method Verb to a DELETE"); - - aChallenge.addHint("Use a normal POST request, but add an X-HTTP-Method-Override header"); - - aChallenge.addSolutionLink( - "Add a header 'X-HTTP-Method-Override: DELETE' to a POST /heartbeat request", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/method-override/all-method-overrides"); - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); - return aChallenge; - } - - // 501 - public static ChallengeDefinitionData overridePostToTraceFor501(int challengeOrder) { - ChallengeDefinitionData aChallenge = - new ChallengeDefinitionData( - ChallengeRenderer.renderChallengeNumber(challengeOrder), - "POST /heartbeat as Trace (501)", - "Issue a POST request on the `/heartbeat` end point and receive 501 (Not Implemented) when you override the Method Verb to a TRACE"); - aChallenge.addHint("Use a normal POST request, but add an X-HTTP-Method-Override header"); - - aChallenge.addSolutionLink( - "Add a header 'X-HTTP-Method-Override: TRACE' to a POST /heartbeat request", - "", - ""); - aChallenge.addSolutionLink( - "Read Solution", - "HREF", - "/apichallenges/solutions/method-override/all-method-overrides"); - - // aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); - return aChallenge; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutes.java deleted file mode 100644 index f42fd933..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutes.java +++ /dev/null @@ -1,375 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import static uk.co.compendiumdev.thingifier.adapter.httpserver.ServerRoutes.*; - -import java.util.List; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpRouteHandler; -import uk.co.compendiumdev.thingifier.adapter.httpserver.SimpleHttpRouteCreator; -import uk.co.compendiumdev.thingifier.adapter.httpserver.conversion.HttpServerRequestToInternalHttpRequest; -import uk.co.compendiumdev.thingifier.adapter.httpserver.conversion.InternalHttpResponseToHttpServer; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.HttpApiResponseToInternalHttpResponse; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.InternalHttpRequestToHttpApiRequest; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingDefinition; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingStatus; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; -import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; -import uk.co.compendiumdev.thingifier.api.ermodelconversion.JsonThing; -import uk.co.compendiumdev.thingifier.api.http.*; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; -import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.AcceptHeaderParser; -import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.BasicAuthHeaderParser; -import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.BearerAuthHeaderParser; -import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.ContentTypeHeaderParser; -import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.definitions.validation.MaximumLengthValidationRule; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.domain.instances.validation.EntityInstanceStateValidator; -import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport; - -// TODO: This should be using a Thingifier to do the work of XML JSON etc... like the simulation -public class AuthRoutes { - private Thingifier secretNoteStore; - private EntityDefinition secretNote; - private ThingifierHttpApi httpApi; - private JsonThing jsonThing; - private final EntityInstanceStateValidator stateValidator = new EntityInstanceStateValidator(); - - public void configure( - final Challengers challengers, final ThingifierApiDocumentationDefn apiDefn) { - // authentication and authorisation - // - create a 'secret' note which can be stored against session using an auth token - - this.secretNoteStore = new Thingifier(); - - this.secretNote = this.secretNoteStore.defineThing("secretnote", "secretnotes"); - - this.secretNote.addFields( - Field.is("note", FieldType.STRING) - .makeMandatory() - .withValidation(new MaximumLengthValidationRule(100)) - .withDefaultValue("")); - - this.httpApi = new ThingifierHttpApi(this.secretNoteStore); - this.jsonThing = new JsonThing(this.secretNoteStore.apiConfig().jsonOutput()); - - SimpleHttpRouteCreator.addHandler( - "/secret/token", - "options", - (request, result) -> { - result.status(204); - // disallow POST, DELETE, PATCH, TRACE - result.header("Allow", "POST, OPTIONS"); - return ""; - }); - - // TODO: this still feels tightly coupled to HTTP routing; route handling should delegate - // to an internal auth use case. - - // POST /secret/token with basic auth to get a secret/token to use as X-AUTH-TOKEN header - // todo: or {username, password} payload - post( - "/secret/token", - (request, result) -> { - BasicAuthHeaderParser basicAuth = - new BasicAuthHeaderParser(request.header("Authorization")); - - // admin/password as default username:password - if (!basicAuth.matches("admin", "password")) { - result.header("WWW-Authenticate", "Basic realm=\"User Visible Realm\""); - result.status(401); - return ""; - } - - ChallengerAuthData challenger = - challengers.getChallenger(request.header("X-CHALLENGER")); - - if (challenger == null) { - result.status(401); - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - return ""; - } - - // if no header X-AUTH-TOKEN then grant one - result.header("X-AUTH-TOKEN", challenger.getXAuthToken()); - result.status(201); - return ""; - }); - - SimpleHttpRouteCreator.routeStatusWhenNot(405, "/secret/token", List.of("post", "options")); - - apiDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.POST, - "/secret/token", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation( - "POST /secret/token with basic auth to get a secret/token to use as X-AUTH-TOKEN header, to allow access to the /secret/note end points.") - .addPossibleStatuses(201, 401) - .secureWithBasicAuth()); - - // todo: GET /secret/token returns the secret token or 401 if not authenticated - - // POST /secret/note GET /secret/note - limit note to 100 chars - // no auth token will receive a 403 - // auth token which does not match the session will receive a 401 - // header X-AUTH-TOKEN: token given - if token not found (then) 401 - - SimpleHttpRouteCreator.addHandler( - "/secret/note", - "options", - (request, result) -> { - result.status(204); - // disallow POST, DELETE, PATCH, TRACE - result.header("Allow", "GET, HEAD, POST, OPTIONS"); - return ""; - }); - - HttpRouteHandler getSecretNote = - (request, result) -> { - String authToken = request.header("X-AUTH-TOKEN"); - final String authorization = request.header("Authorization"); - - result.header("Content-Type", "application/json"); - - ChallengerAuthData challenger = - challengers.getChallenger(request.header("X-CHALLENGER")); - - if (challenger == null) { - result.status(401); - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - return ""; - } - - // authorization bearer token will take precedence over X-AUTH-HEADER - if (authorization != null && !authorization.isEmpty()) { - final BearerAuthHeaderParser bearerToken = - new BearerAuthHeaderParser(authorization); - if (bearerToken.isBearerToken() && bearerToken.isValid()) { - authToken = bearerToken.getToken(); - } - } - - if (authToken == null || authToken.isEmpty()) { - result.status(401); - return ""; - } - - if (!authToken.contentEquals(challenger.getXAuthToken())) { - result.status(403); // given token is not allowed to access anything - return ""; - } - - AcceptHeaderParser acceptHeaderParser = - new AcceptHeaderParser(request.header("ACCEPT")); - if (!acceptHeaderParser.missingAcceptHeader() - && !acceptHeaderParser.isSupportedHeader()) { - result.status(406); - return ""; - } - - final InternalHttpRequest internalRequest = - HttpServerRequestToInternalHttpRequest.convert(request); - final HttpApiRequest myRequest = - InternalHttpRequestToHttpApiRequest.convert(internalRequest); - - final ApiResponse response = - ApiResponse.success() - .returnSingleDraft( - EntityInstanceDraft.forEntity(secretNote) - .withField("note", challenger.getNote())); - - final HttpApiResponse httpApiResponse = - new HttpApiResponse( - myRequest.getHeaders(), - response, - jsonThing, - this.secretNoteStore.apiConfig()); - - return InternalHttpResponseToHttpServer.convert( - HttpApiResponseToInternalHttpResponse.convert(httpApiResponse), result); - - // return resultBasedOnAcceptHeader(result, request.header("ACCEPT"), - // challenger.getNote()); - }; - - get( - "/secret/note", - (request, result) -> { - return getSecretNote.handle(request, result); - }); - - head( - "/secret/note", - (request, result) -> { - getSecretNote.handle(request, result); - return ""; - }); - - apiDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.GET, - "/secret/note", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation( - "GET /secret/note with X-AUTH-TOKEN to return the secret note for the user.") - .addPossibleStatuses(200, 401, 403) - .addCustomHeader("X-AUTH-TOKEN", "string")); - - post( - "/secret/note", - (request, result) -> { - final String authorization = request.header("Authorization"); - String authToken = request.header("X-AUTH-TOKEN"); - - AcceptHeaderParser acceptHeaderParser = - new AcceptHeaderParser(request.header("ACCEPT")); - if (!acceptHeaderParser.missingAcceptHeader() - && !acceptHeaderParser.isSupportedHeader()) { - result.status(406); - return ""; - } - - ContentTypeHeaderParser contentTypeParser = - new ContentTypeHeaderParser(request.header("CONTENT-TYPE")); - if (!contentTypeParser.isJSON() && !contentTypeParser.isXML()) { - result.status(415); - return ""; - } - - // todo: if no X-CHALLENGER provided then, search memory for authToken and use - // associated - // challenger - ChallengerAuthData challenger = - challengers.getChallenger(request.header("X-CHALLENGER")); - - if (challenger == null) { - result.status(401); - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - return ""; - } - - result.header("X-CHALLENGER", challenger.getXChallenger()); - // set content-type header for error responses - if (acceptHeaderParser.hasAPreferenceForXml()) { - result.header("Content-Type", "application/xml"); - } else { - result.header("Content-Type", "application/json"); - } - - // authorization bearer token will take precedence over X-AUTH-HEADER - if (authorization != null && !authorization.isEmpty()) { - final BearerAuthHeaderParser bearerToken = - new BearerAuthHeaderParser(authorization); - if (bearerToken.isBearerToken() && bearerToken.isValid()) { - authToken = bearerToken.getToken(); - } - } - - if (authToken == null || authToken.isEmpty()) { - result.status(401); - return ""; - } - - if (!authToken.contentEquals(challenger.getXAuthToken())) { - result.status(403); // given token is not allowed to access anything - return ""; - } - - if (!acceptHeaderParser.missingAcceptHeader() - && !acceptHeaderParser.isSupportedHeader()) { - result.status(406); - return ""; - } - - final InternalHttpRequest internalRequest = - HttpServerRequestToInternalHttpRequest.convert(request); - final HttpApiRequest myRequest = - InternalHttpRequestToHttpApiRequest.convert(internalRequest); - HttpApiResponse httpApiResponse = - this.httpApi.validateRequestSyntax( - myRequest, ThingifierHttpApi.HttpVerb.POST); - - // TODO: this should be simpler to use by apps building on thingifier - if (httpApiResponse == null) { - - ApiResponse response = - this.secretNoteStore - .api() - .post( - "secretnote", - new BodyParser( - myRequest, - this.secretNoteStore.getThingNames()), - myRequest.getHeaders()); - if (!response.isErrorResponse()) { - - EntityInstance returnedInstance = response.getReturnedInstance(); - final List<String> protectedFieldNames = - returnedInstance - .getEntity() - .getFieldNamesOfType( - FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); - ValidationReport validity = - stateValidator.validateFields( - returnedInstance, protectedFieldNames, false); - validity.combine( - secretNoteStore - .getStore(EntityRelModel.DEFAULT_DATABASE_NAME) - .relationships() - .validate(returnedInstance)); - - this.secretNoteStore.deleteThing( - response.getReturnedInstance(), - EntityRelModel.DEFAULT_DATABASE_NAME); - - if (!validity.isValid()) { - response = - ApiResponse.error(400, validity.getCombinedErrorMessages()); - } else { - final EntityInstance postedThing = response.getReturnedInstance(); - response = ApiResponse.success().returnSingleInstance(postedThing); - challenger.setNote( - response.getReturnedInstance() - .getFieldValue("note") - .asString()); - } - } - - httpApiResponse = - new HttpApiResponse( - myRequest.getHeaders(), - response, - jsonThing, - this.secretNoteStore.apiConfig()); - } - - return InternalHttpResponseToHttpServer.convert( - HttpApiResponseToInternalHttpResponse.convert(httpApiResponse), result); - }); - - SimpleHttpRouteCreator.routeStatusWhenNot( - 405, "/secret/note", List.of("get", "post", "head", "options")); - - apiDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.POST, - "/secret/note", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation( - "POST /secret/note with X-AUTH-TOKEN, and a payload of `{'note':'contents of note'}` to amend the contents of the secret note.") - .addPossibleStatuses(200, 400, 401, 403) - .addCustomHeader("X-AUTH-TOKEN", "string")); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerIpAddressTracker.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerIpAddressTracker.java deleted file mode 100644 index 8b13cb7f..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerIpAddressTracker.java +++ /dev/null @@ -1,90 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListSet; - -public class ChallengerIpAddressTracker { - private final int maxChallengers; - private final Map<String, ConcurrentSkipListSet<String>> ipaddresses; - private final boolean addressLimitingOn; - - // todo: add a banned ip address list - // todo: add a no-limit ip address list - public ChallengerIpAddressTracker(int maxChallengersPerIp, boolean addressLimitingOn) { - maxChallengers = maxChallengersPerIp; - ipaddresses = new ConcurrentHashMap<>(); - this.addressLimitingOn = addressLimitingOn; - } - - public void purgeEmptyIpAddresses(Set<String> existingChallengerGuids) { - - for (String anIp : ipaddresses.keySet()) { - ConcurrentSkipListSet<String> challengerGuids = ipaddresses.get(anIp); - List<String> challengersToRemove = new ArrayList<>(); - - // find any challengers that have been purged - for (String aChallengerGuid : challengerGuids) { - if (!existingChallengerGuids.contains(aChallengerGuid)) { - challengersToRemove.add(aChallengerGuid); - } - } - - challengerGuids.removeAll(challengersToRemove); - } - - // purge any empty ip addresses - for (String anIp : ipaddresses.keySet()) { - if (ipaddresses.get(anIp).isEmpty()) { - ipaddresses.remove(anIp); - } - } - } - - public boolean hasLimitBeenReachedFor(String ip) { - - if (!addressLimitingOn) { - return false; - } - - if (countFor(ip) < maxChallengers) { - return false; - } - - return true; - } - - public boolean trackAgainstThisIp(String ip, String xChallengerGuid) { - - if (!addressLimitingOn) { - return false; - } - - if (hasLimitBeenReachedFor(ip)) { - return false; - } - - if (isTrackingIp(ip)) { - ipaddresses.get(ip).add(xChallengerGuid); - } else { - ConcurrentSkipListSet<String> guids = new ConcurrentSkipListSet<>(); - guids.add(xChallengerGuid); - ipaddresses.put(ip, guids); - } - return true; - } - - public boolean isTrackingIp(String ip) { - return ipaddresses.containsKey(ip); - } - - public int countFor(String ip) { - if (isTrackingIp(ip)) { - return ipaddresses.get(ip).size(); - } - return 0; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutes.java deleted file mode 100644 index 93557722..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutes.java +++ /dev/null @@ -1,435 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import static uk.co.compendiumdev.thingifier.adapter.httpserver.ServerRoutes.*; - -import com.google.gson.Gson; -import java.util.*; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitions; -import uk.co.compendiumdev.challenge.persistence.PersistenceLayer; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpRouteHandler; -import uk.co.compendiumdev.thingifier.adapter.httpserver.SimpleHttpRouteCreator; -import uk.co.compendiumdev.thingifier.api.docgen.*; -import uk.co.compendiumdev.thingifier.api.response.ApiResponseAsJson; -import uk.co.compendiumdev.thingifier.api.response.ApiResponseError; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; - -public class ChallengerTrackingRoutes { - - final int MAX_CHALLENGERS_PER_IP = 100; - - public void configure( - final Challengers challengers, - final boolean single_player_mode, - final ThingifierApiDocumentationDefn apiDefn, - final PersistenceLayer persistenceLayer, - final Thingifier thingifier, - ChallengeDefinitions challengeDefinitions) { - - // todo: control max challengers and ip address limiting dynamically through admin interface - // and via environment variables - // until then, leave this off by passing in false - ChallengerIpAddressTracker ipAddressTracker = - new ChallengerIpAddressTracker(MAX_CHALLENGERS_PER_IP, false); - - HttpRouteHandler getChallengerId = - (request, result) -> { - ChallengerAuthData challenger = null; - String xChallengerGuid = request.params("id"); - - if (xChallengerGuid != null && !xChallengerGuid.trim().isEmpty()) { - challenger = challengers.getChallenger(xChallengerGuid); - if (challenger != null) { - challenger.touch(); - thingifier.ensureCreatedAndPopulatedInstanceDatabaseNamed( - challenger.getXChallenger()); - result.status(200); - result.header("content-type", "application/json"); - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - return challenger.asJson(); - } else { - result.status(404); - } - } else { - result.status(404); - } - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - return ""; - }; - - SimpleHttpRouteCreator.addHandler( - "/challenger/:id", - "options", - (request, result) -> { - result.status(204); - // disallow POST, DELETE, PATCH, TRACE - result.header("Allow", "GET, PUT, HEAD, OPTIONS"); - return ""; - }); - - SimpleHttpRouteCreator.routeStatusWhenNot( - 405, "/challenger/:id", List.of("get", "put", "head", "options")); - - // refresh challenger to avoid purging - get( - "/challenger/:id", - (request, result) -> { - return getChallengerId.handle(request, result); - }); - - head( - "/challenger/:id", - (request, result) -> { - getChallengerId.handle(request, result); - return ""; - }); - - if (!single_player_mode) { - // make sure the X-CHALLENGER header shows up in the swagger when in multi-user mode - apiDefn.addCustomHeaderWhenRouteNotMatches( - new RoutingDefinition(RoutingVerb.POST, "/challenger", null, null), - new HeaderDefinition("X-CHALLENGER", "guid")); - } - - Field guidField = new Field("guid", FieldType.AUTO_GUID); - - // Document the endpoint - apiDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.GET, - "/challenger/:guid", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation( - "Get a challenger in Json format to allow continued tracking of challenges.") - .addPossibleStatuses(200, 404) - .addRequestUrlParam(guidField)); - - // endpoint to restore a saved challenger status from UI - put( - "/challenger/:id", - (request, result) -> { - ChallengerAuthData challenger; - - // for which challenger? - String xChallengerGuid = request.params("id"); - - if (xChallengerGuid == null) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson("Invalid Challenger GUID"); - } - - if (!single_player_mode) { - try { - UUID.fromString(xChallengerGuid); - } catch (Exception e) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson( - "Invalid Challenger GUID " + e.getMessage()); - } - } - - // try and parse payload, fail if nonsense - try { - challenger = new Gson().fromJson(request.body(), ChallengerAuthData.class); - } catch (Exception e) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson(e.getMessage()); - } - - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - - if (challenger == null) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson("Invalid Payload"); - } - - // check payload against id - if (!challenger.getXChallenger().equals(xChallengerGuid)) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson( - "URL GUID does not match payload X-CHALLENGER"); - } - - // does id exist in memory, if so just replace state data - if (challengers.inMemory(xChallengerGuid)) { - ChallengerAuthData existingChallenger = - challengers.getChallenger(xChallengerGuid); - existingChallenger.fromData( - challenger, challengeDefinitions.getDefinedChallenges()); - result.status(200); - result.header("content-type", "application/json"); - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - return existingChallenger.asJson(); - } - - // need to create a new challenger in memory with this state - // but first... have a limit on the number of challengers an IP address can - // create - challengers.purgeOldAuthData(); - ipAddressTracker.purgeEmptyIpAddresses(challengers.getChallengerGuids()); - if (ipAddressTracker.hasLimitBeenReachedFor(request.ip())) { - result.status(429); - result.header("content-type", "application/json"); - result.header("X-CHALLENGER", xChallengerGuid); - return ApiResponseError.asAppropriate( - request.header("accept"), - "Attempted to create too many challengers, wait and try again later."); - } - - // create a challenger from the JSON and instantiate the database - ChallengerAuthData newChallenger = - new ChallengerAuthData(challengeDefinitions.getDefinedChallenges()) - .fromData( - challenger, - challengeDefinitions.getDefinedChallenges()); - challengers.put(newChallenger); - thingifier.ensureCreatedAndPopulatedInstanceDatabaseNamed( - challenger.getXChallenger()); - - // track challenger against IP - ipAddressTracker.trackAgainstThisIp(request.ip(), xChallengerGuid); - - result.status(201); - result.header("content-type", "application/json"); - result.header("X-CHALLENGER", xChallengerGuid); - return ""; - }); - - // Document the endpoint - apiDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.PUT, - "/challenger/:guid", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation( - "Restore a saved challenger matching the supplied X-CHALLENGER guid to allow continued tracking of challenges.") - .addPossibleStatuses(200, 201, 400) - .addRequestUrlParam(guidField)); - - /* - / challenger - - */ - - SimpleHttpRouteCreator.addHandler( - "/challenger", - "options", - (request, result) -> { - result.status(204); - // disallow POST, DELETE, PATCH, TRACE - result.header("Allow", "POST, OPTIONS"); - return ""; - }); - - // create a challenger - post( - "/challenger", - (request, result) -> { - if (single_player_mode) { - XChallengerHeader.setResultHeaderBasedOnChallenger( - result, challengers.SINGLE_PLAYER.getXChallenger()); - result.header("Location", "/gui/challenges"); - result.status(201); - return ""; - } - - String xChallengerGuid = request.header("X-CHALLENGER"); - if (xChallengerGuid == null || xChallengerGuid.trim().isEmpty()) { - // create a new challenger with a database - final ChallengerAuthData challenger = challengers.createNewChallenger(); - // create the database for the user - thingifier.ensureCreatedAndPopulatedInstanceDatabaseNamed( - challenger.getXChallenger()); - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - result.header("Location", "/gui/challenges/" + challenger.getXChallenger()); - result.status(201); - } else { - ChallengerAuthData challenger = challengers.getChallenger(xChallengerGuid); - if (challenger == null) { - // if X-CHALLENGER header exists, and is not a known UUID, - // return 404, challenger ID not valid - result.status(404); - } else { - // challenger already exists, ensure the database does - // create the database for the user - thingifier.ensureCreatedAndPopulatedInstanceDatabaseNamed( - challenger.getXChallenger()); - // if X-CHALLENGER header exists, and has a valid UUID, and UUID exists, - // then return 200 - result.header( - "Location", "/gui/challenges/" + challenger.getXChallenger()); - result.status(200); - } - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - } - return ""; - }); - - apiDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.POST, - "/challenger", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation("Create a challenger using the X-CHALLENGER guid header.") - .addPossibleStatuses(200, 400, 405)); - - SimpleHttpRouteCreator.routeStatusWhenNot(405, "/challenger", List.of("post", "options")); - - /* - The todos restore endpoint - */ - - // Document the endpoint - apiDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.GET, - "/challenger/database/:guid", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation( - "Get the todo data for the supplied X-CHALLENGER guid to allow later restoration of the todos.") - .addPossibleStatuses(200, 400, 404) - .addRequestUrlParam(guidField)); - - HttpRouteHandler getChallengerDatabaseId = - (request, result) -> { - ChallengerAuthData challenger; - - // for which challenger? - String xChallengerGuid = request.params("id"); - - if (xChallengerGuid == null) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson("Invalid Challenger GUID"); - } - - if (!single_player_mode) { - try { - UUID.fromString(xChallengerGuid); - } catch (Exception e) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson( - "Invalid Challenger GUID " + e.getMessage()); - } - } - - challenger = challengers.getChallenger(xChallengerGuid); - XChallengerHeader.setResultHeaderBasedOnChallenger(result, challenger); - - if (challenger != null) { - challenger.touch(); - result.header("content-type", "application/json"); - - if (challengers.getErModel().getStore(xChallengerGuid) == null) { - result.status(404); - return ApiResponseAsJson.getErrorMessageJson( - "Challenger database not instantiated " + xChallengerGuid); - } - - result.status(200); - return challengers.getErModel().exportInstanceDataAsJson(xChallengerGuid); - } else { - result.status(404); - return ApiResponseAsJson.getErrorMessageJson( - "Challenger not found " + xChallengerGuid); - } - }; - - SimpleHttpRouteCreator.addHandler( - "/challenger/database/:id", - "options", - (request, result) -> { - result.status(204); - // disallow POST, DELETE, PATCH, TRACE - result.header("Allow", "GET, PUT, HEAD, OPTIONS"); - return ""; - }); - - SimpleHttpRouteCreator.routeStatusWhenNot( - 405, "/challenger/database/:id", List.of("get", "put", "head", "options")); - - // add a GET challenger/database/:id in the proper database format - - get( - "/challenger/database/:id", - (request, result) -> { - return getChallengerDatabaseId.handle(request, result); - }); - - head( - "/challenger/database/:id", - (request, result) -> { - getChallengerDatabaseId.handle(request, result); - return ""; - }); - - // endpoint to restore a saved challenger database from UI - put( - "/challenger/database/:id", - (request, result) -> { - String xChallengerGuid = request.params("id"); - - // challenger uuid must exist and be valid - if (xChallengerGuid == null) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson("Invalid Challenger GUID"); - } - - if (!single_player_mode) { - try { - UUID.fromString(xChallengerGuid); - } catch (Exception e) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson( - "Invalid Challenger GUID " + e.getMessage()); - } - } - - if (!challengers.inMemory(xChallengerGuid)) { - result.status(404); - return ApiResponseAsJson.getErrorMessageJson( - "Unknown Challenger GUID - have you created or loaded the challenger state"); - } - - // thingifier allows loading database from json extract - try { - thingifier.ensureCreatedAndPopulatedInstanceDatabaseFromJson( - xChallengerGuid.trim(), request.body()); - } catch (Exception e) { - result.status(400); - return ApiResponseAsJson.getErrorMessageJson(e.getMessage()); - } - - if (challengers.getErModel().getDatabaseNames().contains(xChallengerGuid)) { - result.status(204); - result.header("content-type", "application/json"); - result.header("X-CHALLENGER", xChallengerGuid); - return ""; - } else { - result.status(500); - return ApiResponseAsJson.getErrorMessageJson( - "Unknown error, database not found"); - } - }); - - // Document the endpoint - apiDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.PUT, - "/challenger/database/:guid", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation( - "Restore a saved set of todos for a challenger matching the supplied X-CHALLENGER guid.") - .addPossibleStatuses(204, 400) - .addRequestUrlParam(guidField)); - - // TODO: add a protected admin page with an environment variable protection as password - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutes.java deleted file mode 100644 index 80ce65b8..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutes.java +++ /dev/null @@ -1,119 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import static uk.co.compendiumdev.thingifier.adapter.httpserver.ServerRoutes.*; - -import java.util.List; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.apimodel.ChallengeThingifier; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitions; -import uk.co.compendiumdev.thingifier.adapter.httpserver.AdhocDocumentedHttpRouteConfigurer; -import uk.co.compendiumdev.thingifier.adapter.httpserver.SimpleHttpRouteCreator; -import uk.co.compendiumdev.thingifier.adapter.httpserver.routehandlers.HttpApiRequestResponseHandler; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingDefinition; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingStatus; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; -import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; -import uk.co.compendiumdev.thingifier.api.restapihandlers.RestApiGetHandler; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.query.FilterBy; -import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; - -public class ChallengesRoutes { - - public void configure( - final Challengers challengers, - final boolean single_player_mode, - final ThingifierApiDocumentationDefn apiDefn, - final ChallengeDefinitions challengeDefinitions) { - - get( - "/challenges", - (request, result) -> { - ChallengerAuthData challenger = - challengers.getChallenger(request.header("X-CHALLENGER")); - - if (!single_player_mode) { - if (challenger != null) { - result.header( - "Location", "/gui/challenges/" + challenger.getXChallenger()); - } - } else { - result.header("Location", "/gui/challenges"); - } - - // Todo: use the cloneThingifierWithNewData here and simplify the - // ChallengeThingifier - ChallengeThingifier challengeThingifier = new ChallengeThingifier(); - final EntityDefinition challengeDefn = challengeThingifier.challengeDefn; - challengeThingifier.populateThingifierFrom(challengeDefinitions); - - return new HttpApiRequestResponseHandler( - request, result, challengeThingifier.challengeThingifier) - .usingHandler( - (anHttpApiRequest) -> { - challengeThingifier.populateThingifierFromStatus( - challenger); - QueryFilterParams queryParams = - anHttpApiRequest.getFilterableQueryParams(); - // final Map<String, String> queryParams = - // anHttpApiRequest.getQueryParams(); - if (!queryParams.hasSortBy()) { - queryParams.add(new FilterBy("sort_by", "+ID")); - } - // - // if(!queryParams.containsKey("sortBy") && - // - // !queryParams.containsKey("sort_by")){ - // // force a sort - // - // queryParams.put("sort_by","+ID"); - // } - return new RestApiGetHandler( - challengeThingifier.challengeThingifier) - .handle( - challengeDefn.getPlural(), - queryParams, - anHttpApiRequest.getHeaders()); - }) - .handle(); - }); - - apiDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.GET, - "/challenges", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation("Get list of challenges and their completion status") - .addPossibleStatuses(200)); - - // TODO: because these hardcode contentType and ignore Accept there should be a light weight - // wrapper available - new AdhocDocumentedHttpRouteConfigurer(apiDefn) - .add( - "/challenges", - RoutingVerb.HEAD, - 200, - "Headers for list of challenges endpoint", - (request, result) -> { - result.status(200); - result.type("application/json"); - return ""; - }) - .add( - "/challenges", - RoutingVerb.OPTIONS, - 200, - "Options for list of challenges endpoint", - ((request, result) -> { - result.status(200); - result.type("application/json"); - result.header("Allow", "GET, HEAD, OPTIONS"); - return ""; - })); - - SimpleHttpRouteCreator.routeStatusWhenNot( - 405, "/challenges", List.of("get", "head", "options")); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutes.java deleted file mode 100644 index 78ff63e9..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutes.java +++ /dev/null @@ -1,39 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import java.util.List; -import uk.co.compendiumdev.thingifier.adapter.httpserver.AdhocDocumentedHttpRouteConfigurer; -import uk.co.compendiumdev.thingifier.adapter.httpserver.SimpleHttpRouteCreator; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; -import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; - -public class HeartBeatRoutes { - - public void configure(final ThingifierApiDocumentationDefn apiDefn) { - - String endpoint = "/heartbeat"; - - final AdhocDocumentedHttpRouteConfigurer routeConfig = - new AdhocDocumentedHttpRouteConfigurer(apiDefn); - - routeConfig.add(endpoint, RoutingVerb.GET, 204, "Is the server running? YES 204"); - - routeConfig.add(endpoint, RoutingVerb.HEAD, 204, "Headers for heartbeat endpoint"); - - routeConfig.add( - endpoint, - RoutingVerb.OPTIONS, - 204, - "Options for heartbeat endpoint", - (request, result) -> { - result.status(204); - result.header("Allow", "GET, HEAD, OPTIONS"); - return ""; - }); - - // undocumented handlers - SimpleHttpRouteCreator SimpleHttpRouteCreator = new SimpleHttpRouteCreator(endpoint); - SimpleHttpRouteCreator.status(405, true, List.of("post", "delete", "put")); - SimpleHttpRouteCreator.status(500, true, List.of("patch")); - SimpleHttpRouteCreator.status(501, true, List.of("trace")); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/XChallengerHeader.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/XChallengerHeader.java deleted file mode 100644 index 025cd40f..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/XChallengerHeader.java +++ /dev/null @@ -1,59 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpServerResponse; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpResponse; - -public class XChallengerHeader { - - public static final String NOT_FOUND_ERROR_MESSAGE = - "UNKNOWN CHALLENGER - Challenger not found"; - - public static void setResultHeaderBasedOnChallenger( - final HttpServerResponse result, final ChallengerAuthData challenger) { - - if (result == null) { - return; - } - - if (challenger == null) { - result.header("X-CHALLENGER", NOT_FOUND_ERROR_MESSAGE); - // result.header("X-CHALLENGER", "Challenger not recognised"); - } else { - result.header("X-CHALLENGER", challenger.getXChallenger()); - } - } - - public static void setResultHeaderBasedOnChallenger( - final HttpServerResponse result, final String challengerGUID) { - - if (result == null) { - return; - } - - if (challengerGUID == null || challengerGUID.trim().length() == 0) { - result.header("X-CHALLENGER", NOT_FOUND_ERROR_MESSAGE); - // result.header("X-CHALLENGER", "Challenger not recognised"); - } else { - result.header("X-CHALLENGER", challengerGUID); - } - } - - public static void setResultHeaderBasedOnChallenger( - final InternalHttpResponse response, final ChallengerAuthData challenger) { - - if (response == null) { - return; - } - - if (response.getHeaders().headerExists("X-CHALLENGER")) { - return; - } - - if (challenger == null) { - response.setHeader("X-CHALLENGER", NOT_FOUND_ERROR_MESSAGE); - } else { - response.setHeader("X-CHALLENGER", challenger.getXChallenger()); - } - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/ChallengerWebGUI.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/ChallengerWebGUI.java deleted file mode 100644 index ee9e7bea..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/ChallengerWebGUI.java +++ /dev/null @@ -1,811 +0,0 @@ -package uk.co.compendiumdev.challenge.gui; - -import static uk.co.compendiumdev.thingifier.adapter.httpserver.ServerRoutes.*; - -import java.time.LocalDate; -import java.util.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.challenge.challenges.*; -import uk.co.compendiumdev.challenge.persistence.PersistenceLayer; -import uk.co.compendiumdev.challenge.persistence.PersistenceResponse; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpServerRequest; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; - -public class ChallengerWebGUI { - private static final LocalDate SEO_FIXED_LASTMOD = LocalDate.parse("2026-02-18"); - - private final PageNotFoundResponse pageNotFoundHtmlResponse; - Logger logger = LoggerFactory.getLogger(ChallengerWebGUI.class); - private final DefaultGUIHTML guiManagement; - private final boolean guiStayAlive; - - public ChallengerWebGUI(final DefaultGUIHTML defaultGui, final boolean guiStayAlive) { - this.guiManagement = defaultGui; - this.guiStayAlive = guiStayAlive; - this.pageNotFoundHtmlResponse = new PageNotFoundResponse(guiManagement); - } - - String getChallengesPageHtmlHeader() { - return guiManagement.getPageStart( - "API Challenges - Improve your API Skills", - "<script src='/js/challengerui.js'></script>" - + "<meta name='description' content='A free online set of gamified REST API Challenges to practice and improve your API Testing Skills'>", - "/gui/challenges"); - } - - String getSponsorMessage() { - String sponsorMessage = - """ - <div class='sponsor-top'> - <p> - Support this site by joining our Patreon. - For as little as $1 a month you receive exclusive ad-free content, - ebooks and online training courses. - - <a href='https://patreon.com/eviltester' target='_blank'>Learn more</a> - </p> - </div> - """; - - return sponsorMessage; - } - - public void setup( - final Challengers challengers, - final ChallengeDefinitions challengeDefinitions, - final PersistenceLayer persistenceLayer, - final boolean single_player_mode) { - - guiManagement.appendMenuItem("Home", "/"); - guiManagement.appendMenuItem("Entities Explorer", "/gui/entities"); - guiManagement.appendMenuItem("Challenges", "/gui/challenges"); - guiManagement.appendMenuItem("API documentation", "/docs"); - guiManagement.appendMenuItem("Learning", "/learning"); - - String actualMenu = - """ - <script> - function setMenuNavBasedOnUrl(){ - const pathToCheck = window.location.pathname; - urlMapping = [ - ['/simpleapi/', 'simple-api-root-menu'], - ['/practice-modes/simpleapi', 'simple-api-root-menu'], - ['/gui/', 'api-challenges-root-menu'], - ['/docs', 'api-challenges-root-menu'], - ['/apichallenges', 'api-challenges-root-menu'], - ['/sim/docs', 'sim-api-root-menu'], - ['/practice-modes/simulation', 'sim-api-root-menu'], - ['/mirror/', 'mirror-api-root-menu'], - ['/practice-modes/mirror', 'mirror-api-root-menu'] - ]; - foundMapping = false; - for(const mapping of urlMapping){ - if(pathToCheck.startsWith(mapping[0])){ - document.getElementById(mapping[1]).classList.add('dropped'); - foundMapping=true; - break; - } - } - if(!foundMapping && pathToCheck=='/'){ - document.getElementById('home-root-menu').classList.add('dropped'); - foundMapping=true; - } - if(!foundMapping){ - // assume it is a learning page - document.getElementById('learning-root-menu').classList.add('dropped'); - } - } - document.addEventListener("DOMContentLoaded", setMenuNavBasedOnUrl); - </script> - <div class="container cssmenu"> - <nav aria-label="Site menu"> - <div class="css-menu"> - <ul class="sub-menu"> - <li id='home-root-menu'><a class="brand-link" href="/">Home</a></li> - - <li id='learning-root-menu'><a href="/learning">Learning Zone</a> - <!-- TODO include a sample of learning information --> - </li> - - <li id='simple-api-root-menu'><a href="/practice-modes/simpleapi">Simple API</a> - <ul> - <li><a href="/practice-modes/simpleapi">About Simple API</a> - <li><a href="/simpleapi/docs">API Docs</a> - <li><a href="/simpleapi/docs/swagger-ui">Swagger UI</a> - <li><a href="/simpleapi/gui/entities">Data Explorer</a></li> - <li><a href="/practice-modes/simpleapi-openapi">OpenAPI File</a> - </ul> - </li> - - <li id='api-challenges-root-menu'><a href="/gui/challenges">API Challenges</a> - <ul> - <li><a href="/apichallenges">About API Challenges</a></li> - <li><a href="/docs">API Docs</a></li> - <li><a href="/docs/swagger-ui">Swagger UI</a></li> - <li><a href="/gui/challenges">Progress</a></li> - <li><a href="/gui/entities">Data Explorer</a></li> - <li><a href="/apichallenges/solutions">Solutions</a></li> - <li><a href="/apichallenges/openapi">OpenAPI File</a> - </ul> - </li> - - <li id='sim-api-root-menu'><a href="/practice-modes/simulation">API Simulator</a> - <ul> - <li><a href="/practice-modes/simulation">About API Simulator</a></li> - <li><a href="/sim/docs">API Docs</a></li> - <li><a href="/sim/docs/swagger-ui">Swagger UI</a></li> - <li><a href="/sim/docs/swagger">[Download Open API File]</a></li> - - </ul> - </li> - - <li id='mirror-api-root-menu'><a href="/practice-modes/mirror">HTTP Mirror</a> - <ul> - <li><a href="/practice-modes/mirror">About HTTP Mirror</a></li> - <li><a href="/mirror/docs">Mirror API Docs</a></li> - <li><a href="/mirror/docs/swagger-ui">Swagger UI</a></li> - <li><a href="/mirror/docs/swagger">[Download Open API File]</a></li> - - </ul> - </li> - </ul> - </div> - </nav> - </div> - """ - .stripIndent(); - - guiManagement.setActualMenuHtml(getSponsorMessage() + actualMenu); - - // Add the Default GUI Endpoiints for entity exploration - - guiManagement.setHomePageContent( - """ - <h2 id="challenges">Challenges</h2> - <p>The challenges can be completed by issuing API requests to the API.</p> - <p>e.g. <code>GET http://localhost:4567/todos</code> would complete the challenge to "GET the list of todos"</p> - <p>You can also <code>GET http://localhost:4567/challenges</code> to get the list of challenges and their status as an API call. </p> - """); - - guiManagement.setFooter(getSponsorMessage() + getChallengesFooter()); - - // could redirect to eviltester.com if the canonical doesn't change the search indexing from - // heroku to eviltester - // before((request, response) -> { - // - // if (request.requestMethod().equals("GET")) { - // if (request.url().startsWith("http://apichallenges.herokuapp.com") - // || - // - // request.url().startsWith("https://apichallenges.herokuapp.com") - // ) { - // // and it is a browser request - // if (request.header("accept").contains("text/html")) { - // // then redirect - // response.header("location", - // "https://apichallenges.eviltester.com" + request.uri()); - // halt(301); - // } - // } else { - // System.out.println("nothing to see here"); - // } - // } - // } - // ); - - ResourceContentScanner contentScanner = new ResourceContentScanner(); - List<String> pathsToFileContent = - contentScanner.scanForFullPathsOfExtensionsIn("content/", "md"); - contentScanner.addPathsToAvailableContent(pathsToFileContent); - - // add an endpoint for each markdown content file - MarkdownContentManager contentManager = - new MarkdownContentManager(pathsToFileContent, guiManagement); - for (String pathToMarkdownFile : pathsToFileContent) { - String endPointForMarkdownFile = - pathToMarkdownFile.replaceFirst("content/", "/").replace(".md", ""); - get( - endPointForMarkdownFile, - ((request, response) -> { - try { - String responseBody = - contentManager.getResourceMarkdownFileAsHtml( - "content", - request.pathInfo(), - getMarkdownParamsFromRequest(request)); - response.body(responseBody); - response.type("text/html"); - if (response.containsHeader("x-robots-tag")) { - // we want it indexed because it is content - response.header("x-robots-tag", "all"); - } - response.status(200); - } catch (IllegalArgumentException e) { - // in theory this will never happen because we are only creating - // endpoints for existing resources - pageNotFoundHtmlResponse.amendResponse(response, ""); - } - return ""; - })); - head( - endPointForMarkdownFile, - ((request, response) -> { - try { - contentManager.getResourceMarkdownFileAsHtml( - "content", - request.pathInfo(), - getMarkdownParamsFromRequest(request)); - response.type("text/html"); - if (response.containsHeader("x-robots-tag")) { - // we want it indexed because it is content - response.header("x-robots-tag", "all"); - } - response.status(200); - } catch (IllegalArgumentException e) { - // in theory this will never happen because we are only creating - // endpoints for existing resources - pageNotFoundHtmlResponse.amendResponse(response, ""); - } - return ""; - })); - } - - // using the ResourceContentScanner, we can build the sitemap.xml automatically - SiteMapXml siteMap = new SiteMapXml(); - Map<String, LocalDate> contentUrls = contentScanner.scanForUrlsWithDates("content/", "md"); - for (String pathToMarkdownFile : contentUrls.keySet()) { - siteMap.addUrl( - "https://apichallenges.eviltester.com/" + pathToMarkdownFile, - contentUrls.get(pathToMarkdownFile).toString()); - } - siteMap.addUrl("https://apichallenges.eviltester.com", SEO_FIXED_LASTMOD.toString()); - siteMap.addUrl("https://apichallenges.eviltester.com/docs", SEO_FIXED_LASTMOD.toString()); - siteMap.addUrl( - "https://apichallenges.eviltester.com/docs/swagger-ui", - SEO_FIXED_LASTMOD.toString()); - siteMap.addUrl( - "https://apichallenges.eviltester.com/gui/challenges", - SEO_FIXED_LASTMOD.toString()); - - get( - "/sitemap.xml", - (request, response) -> { - response.type("application/xml"); - response.status(200); - return siteMap.asSitemapXml(); - }); - - // Redirect legacy URLs to avoid SEO penalties from old inbound links. - get( - "/apichallenges/solutions/method-overrides/all-method-overrides", - (request, response) -> { - response.redirect( - "/apichallenges/solutions/method-override/all-method-overrides", 301); - return ""; - }); - - get( - "/tools/clients/soapyi", - (request, response) -> { - response.redirect("/tools/clients/soapui", 301); - return ""; - }); - - // use the site/index.md to allow easier creation of landing page, rather than - // public/index.html - get( - "/", - (request, response) -> { - String responseBody = - contentManager.getHtmlVersionOfMarkdownContent( - "site", "/index", getMarkdownParamsFromRequest(request)); - response.body(responseBody); - response.type("text/html"); - if (response.containsHeader("x-robots-tag")) { - // we want it indexed because it is content - response.header("x-robots-tag", "all"); - } - response.status(200); - return ""; - }); - head( - "/", - (request, response) -> { - contentManager.getHtmlVersionOfMarkdownContent( - "site", "/index", getMarkdownParamsFromRequest(request)); - response.type("text/html"); - if (response.containsHeader("x-robots-tag")) { - // we want it indexed because it is content - response.header("x-robots-tag", "all"); - } - response.status(200); - return ""; - }); - - // single user / default session - get( - "/gui/challenges", - (request, result) -> { - if (request.cookie("X-THINGIFIER-DATABASE-NAME") != null) { - // we didn't add a challenger in the URL but we do have one in the cookie - result.header( - "location", - "/gui/challenges/" + request.cookie("X-THINGIFIER-DATABASE-NAME")); - result.status(302); - return ""; - } - - result.type("text/html"); - result.status(200); - - StringBuilder html = new StringBuilder(); - html.append(getChallengesPageHtmlHeader()); - html.append(guiManagement.getMenuAsHTML()); - html.append("<h1>API Challenges Progress</h1>"); - html.append(guiManagement.getStartOfMainContentMarker()); - - // todo explain challenges - single user mode - - // List<ChallengeData> reportOn = new ArrayList<>(); - - if (single_player_mode) { - html.append(playerChallengesIntro()); - // reportOn = new ChallengesPayload(challengeDefinitions, - // challengers.SINGLE_PLAYER).getAsChallenges(); - String json = "{}"; - if (challengers - .getErModel() - .getDatabaseNames() - .contains(EntityRelModel.DEFAULT_DATABASE_NAME)) { - json = - challengers - .getErModel() - .exportInstanceDataAsJson( - EntityRelModel.DEFAULT_DATABASE_NAME); - } - html.append(outputChallengeDataAsJS(challengers.SINGLE_PLAYER, json)); - html.append( - renderChallengeData( - challengeDefinitions, challengers.SINGLE_PLAYER)); - html.append(injectCookieFunctions()); - html.append( - storeThingifierDatabaseNameCookie( - challengers.SINGLE_PLAYER.getXChallenger())); - } else { - html.append( - "<div style='clear:both'><p><strong>Unknown Challenger ID</strong></p></div>"); - html.append(outputChallengeDataAsJS(challengers.SINGLE_PLAYER, "{}")); - html.append( - multiUserShortHelp( - persistenceLayer - .willAutoSaveChallengerStatusToPersistenceLayer(), - persistenceLayer - .willAutoLoadChallengerStatusFromPersistenceLayer())); - html.append(injectCookieFunctions()); - html.append(showPreviousGuids()); - html.append(inputAChallengeGuidScript()); - - // reportOn = new ChallengesPayload(challengeDefinitions, - // challengers.DEFAULT_PLAYER_DATA).getAsChallenges(); - html.append( - renderChallengeData( - challengeDefinitions, challengers.DEFAULT_PLAYER_DATA)); - } - - // html.append(renderChallengeData(reportOn)); - - html.append(guiManagement.getEndOfMainContentMarker()); - html.append(guiManagement.getPageFooter()); - html.append(guiManagement.getPageEnd()); - return html.toString(); - }); - - // multi user - get( - "/gui/challenges/*", - (request, result) -> { - result.type("text/html"); - result.status(200); - - StringBuilder html = new StringBuilder(); - html.append(getChallengesPageHtmlHeader()); - html.append(guiManagement.getMenuAsHTML()); - html.append("<h1>API Challenges Progress</h1>"); - html.append(guiManagement.getStartOfMainContentMarker()); - html.append(playerChallengesIntro()); - - // List<ChallengeData> reportOn = null; - - String xChallenger = null; - - try { - xChallenger = request.splat(); - } catch (Exception e) { - logger.warn("No challenger id to render"); - } - - // is there an in memory challenger with this id? - ChallengerAuthData challenger = null; - PersistenceResponse persistence = null; - - // only check if an xchallenger was passed in - if (xChallenger != null && !xChallenger.trim().isEmpty()) { - xChallenger = santitizeChallengerGuid(xChallenger); - challenger = challengers.getChallenger(xChallenger); - - persistence = new PersistenceResponse(); - - // if no inmemory challenger then ask the persistence layer - if (challenger == null) { - persistence = - persistenceLayer.tryToLoadChallenger(challengers, xChallenger); - } - } - - if (challenger == null) { - String persistenceReason = ""; - if (persistence != null) { - persistenceReason = persistence.getErrorMessage(); - } - html.append("<div class='standoutblock'>"); - html.append( - String.format( - "<p><strong>Unknown Challenger ID %s</strong></p>", - persistenceReason)); - html.append(showCurrentStatus()); - html.append("</div>"); - html.append( - multiUserShortHelp( - persistenceLayer - .willAutoSaveChallengerStatusToPersistenceLayer(), - persistenceLayer - .willAutoLoadChallengerStatusFromPersistenceLayer())); - html.append(injectCookieFunctions()); - html.append(showPreviousGuids()); - html.append(inputAChallengeGuidScript()); - html.append(outputChallengeDataAsJS(challengers.DEFAULT_PLAYER_DATA, "{}")); - html.append( - renderChallengeData( - challengeDefinitions, challengers.DEFAULT_PLAYER_DATA)); - } else { - html.append(injectCookieFunctions()); - - String json = "{}"; - if (challengers.getErModel().getDatabaseNames().contains(xChallenger)) { - json = challengers.getErModel().exportInstanceDataAsJson(xChallenger); - } - html.append(outputChallengeDataAsJS(challenger, json)); - - if (!single_player_mode) { - html.append(storeThingifierDatabaseNameCookie(xChallenger)); - html.append(storeCurrentGuidInLocalStorage(xChallenger)); - - html.append("<div class='standoutblock'>"); - html.append( - String.format( - "<p><strong>Progress For Challenger ID %s</strong></p>", - xChallenger)); - // keep challenge session alive when refresh - challenger.touch(); - html.append(showCurrentStatus()); - html.append(showPreviousGuids()); - html.append(inputAChallengeGuidScript()); - html.append("</div>"); - } else { - html.append(storeThingifierDatabaseNameCookie(xChallenger)); - html.append(storeCurrentGuidInLocalStorage(xChallenger)); - html.append("<div class='standoutblock'>"); - html.append(showCurrentStatus()); - html.append("</div>"); - } - - html.append(renderChallengeData(challengeDefinitions, challenger)); - - html.append(refreshScriptFor(challenger.getXChallenger())); - } - - // html.append(renderChallengeData(reportOn)); - - html.append(guiManagement.getEndOfMainContentMarker()); - html.append(guiManagement.getPageFooter()); - html.append(guiManagement.getPageEnd()); - return html.toString(); - }); - - get( - "/gui/404", - (request, result) -> { - pageNotFoundHtmlResponse.amendResponse(result, ""); - return ""; - }); - - get( - "/gui/404/*", - (request, result) -> { - result.status(404); - result.type("text/html"); - - String urltoshow = ""; - - try { - urltoshow = request.splat(); - } catch (Exception e) { - logger.error("No url to pretend to be on 404", e); - } - - pageNotFoundHtmlResponse.amendResponse( - result, - "<script>window.history.pushState({id:\"404sim\"},\"\",\"/" - + urltoshow - + "\");</script>"); - return ""; - }); - - after( - (request, response) -> { - - // Since we already scanned for static content we can just htmlise a 404 if - // necessary - if (response.status() == 404 - && request.header("accept") != null - && request.header("accept").contains("html")) { - - logger.info("An HTML 404"); - pageNotFoundHtmlResponse.amendResponse(response, ""); - } - }); - } - - private Map<String, String> getMarkdownParamsFromRequest(HttpServerRequest request) { - String originUrl = request.scheme() + "://" + request.host(); - Map<String, String> params = new HashMap<>(); - params.put("ORIGIN_URL", originUrl); - params.put("HOST_URL", request.host()); - return params; - } - - private String showCurrentStatus() { - return "<script>showCurrentStatus()</script>"; - } - - private String santitizeChallengerGuid(String xChallenger) { - return xChallenger.replaceAll("[^\\-a-zA-Z0-9]", ""); - } - - private String injectCookieFunctions() { - return ""; - } - - private String storeThingifierDatabaseNameCookie(String xChallenger) { - return "<script>" - + "setCookie('X-THINGIFIER-DATABASE-NAME','" - + xChallenger - + "',365);" - + "</script>"; - } - - private String storeCurrentGuidInLocalStorage(final String xChallenger) { - return "<script>" - + "var guids = localStorage.getItem('challenges-guids') || '';" - + String.format("if(guids==null || !guids.includes('|%s|')){", xChallenger) - + String.format( - "localStorage.setItem('challenges-guids',guids + '|%s|');", xChallenger) - + "}" - + "</script>"; - } - - private String inputAChallengeGuidScript() { - return "<p><button onclick=inputChallengeGuid()>Input a Challenger GUID to use</button></p>"; - } - - private String showPreviousGuids() { - return "<script>displayLocalGuids()</script>"; // + - } - - private String getChallengesFooter() { - return """ - <p> </p><hr/><div class='footer'><p>Copyright Compendium Developments Ltd 2020 - 2025</p> - <ul class='footerlinks'><li><a href='https://eviltester.com/apichallenges'>API Challenges Info</a></li> - <li><a href='https://eviltester.com'>EvilTester.com</a></li> - <li><a href='https://linkedin.com/in/eviltester'>Contact</a></li> - </ul></div> - """; - } - - private String playerChallengesIntro() { - final StringBuilder html = new StringBuilder(); - html.append("<div style='clear:both'>"); - html.append( - "<p>Use the Descriptions of the challenges below to explore the API and solve the challenges." - + " Remember to use the API documentation to see the format of POST requests.</p>" - + "<p>Progress, and the TODOs database content can be saved to, and restored from, LocalStorage in the browser - or managed via the API." - + "</p>"); - html.append("</div>"); - return html.toString(); - } - - private String multiUserShortHelp( - Boolean canSaveToPersistence, boolean canRestoreFromPersistence) { - final StringBuilder html = new StringBuilder(); - html.append("<div style='clear:both' class='headertextblock'>"); - html.append( - "<p>To view your challenges status in multi-user mode, make sure you have registered as a challenger using a `POST` request to `/challenger` and are including an `X-CHALLENGER` header in all your requests.</p>"); - html.append( - "<p>Then view the challenges in the GUI by visiting `/gui/challenges/{GUID}`, where `{GUID}` is the value in the `X-CHALLENGER` header.<p>"); - html.append( - "<p>Challenger sessions are purged from the server memory after 10 minutes of inactivity.</p>"); - if (canSaveToPersistence) { - html.append("Challenger progress is configured to saved on the server.<p>"); - } else { - html.append( - "Challenger progress is not configured to automatically save on the server. Use the GUI or UI to save progress locally.<p>"); - } - if (canRestoreFromPersistence) { - html.append( - "To restore a previously saved session progress from the server, issue an API request with the X-CHALLENGER header (note this will restore the completion state of challenges, but not the data you were using).<p>"); - } - html.append( - "<p>Session state and current todo list can be stored to local storage, and later restored using the GUI buttons or via API.</p>"); - html.append( - "<p>You can find more information about this on the <a href='/gui/multiuser'>Multi User Help Page</a><p>"); - html.append("</div>"); - return html.toString(); - } - - private String refreshScriptFor(final String xChallenger) { - - if (!guiStayAlive) { - return ""; - } - - StringBuilder html = new StringBuilder(); - - html.append("<script>"); - html.append("/* keep session alive */"); - html.append("setInterval(function(){"); - html.append( - "var oReq = new XMLHttpRequest();\n" - + "oReq.open('GET', '/challenger/" - + xChallenger - + "');\n" - + "oReq.send();"); - html.append("},300000);"); - html.append("</script>"); - return html.toString(); - } - - private String renderChallengeData(final List<ChallengeDefinitionData> reportOn) { - StringBuilder html = new StringBuilder(); - - html.append("<table>"); - html.append("<thead>"); - html.append("<tr>"); - html.append("<th>ID</th>"); - html.append("<th>Challenge</th>"); - html.append("<th>Done</th>"); - html.append("<th>Description</th>"); - html.append("</tr>"); - html.append("</thead>"); - html.append("<tbody>"); - - for (ChallengeDefinitionData challenge : reportOn) { - html.append(String.format("<tr class='status%b'>", challenge.status)); - html.append(String.format("<td>%s</td>", challenge.id)); - html.append(String.format("<td>%s</td>", challenge.name)); - html.append(String.format("<td>%b</td>", challenge.status)); - - String descriptionHTML = String.format("<p>%s</p>", challenge.description); - if (challenge.hasHints() || challenge.hasSolutionLinks()) { - descriptionHTML = descriptionHTML + "<br/>"; - } - if (challenge.hasHints()) { - descriptionHTML = descriptionHTML + "<details><summary>Hints</summary>"; - descriptionHTML = descriptionHTML + "<ul>"; - String hintHtml = ""; - for (ChallengeHint hint : challenge.hints) { - hintHtml = hintHtml + "<li>" + hint.hintText; - if (hint.hintLink != null && !hint.hintLink.isEmpty()) { - String target = "target='_blank'"; - if (!hint.hintLink.startsWith("http")) { - target = ""; - } - hintHtml = - hintHtml - + String.format( - " <a href='%s' %s>Learn More</a>", - hint.hintLink, target); - } - hintHtml = hintHtml + "</li>"; - } - descriptionHTML = descriptionHTML + hintHtml + "</ul>"; - descriptionHTML = descriptionHTML + "</details>"; - } - if (challenge.hasSolutionLinks()) { - - descriptionHTML = descriptionHTML + "<details><summary>Solution</summary>"; - descriptionHTML = descriptionHTML + "<ul>"; - String solutionsHtml = ""; - for (ChallengeSolutionLink solution : challenge.solutions) { - solutionsHtml = solutionsHtml + "<li>" + solution.asHtmlAHref() + "</li>"; - } - descriptionHTML = descriptionHTML + solutionsHtml + "</ul>"; - descriptionHTML = descriptionHTML + "</details>"; - } - - html.append(String.format("<td>%s</td>", descriptionHTML)); - html.append("</tr>"); - } - - html.append("</tbody>"); - html.append("</table>"); - - return html.toString(); - } - - private String outputChallengeDataAsJS(final ChallengerAuthData challenger, String json) { - - StringBuilder html = new StringBuilder(); - - // add the challenge data as JSON - final String dataString = challenger.asJson(); - html.append("<script>document.challengerData=" + dataString + ";</script>"); - // add the current todos as JSON - html.append("<script>document.databaseData=" + json + ";</script>"); - - return html.toString(); - } - - private String renderChallengeData( - final ChallengeDefinitions challengeDefinitions, final ChallengerAuthData challenger) { - StringBuilder html = new StringBuilder(); - - final Collection<ChallengeSection> sections = challengeDefinitions.getChallengeSections(); - - // add a toc - html.append("<h2 id='toc'>Challenge Sections</h2>"); - html.append("<ul>"); - for (ChallengeSection section : sections) { - html.append( - String.format( - "<li><a href='#%s'>%s</a></li>", - section.getTitle().replaceAll(" ", "").toLowerCase(), - section.getTitle())); - } - html.append("</ul>"); - - for (ChallengeSection section : sections) { - - html.append( - String.format( - "<h2 id='%s'>", - section.getTitle().replaceAll(" ", "").toLowerCase()) - + section.getTitle() - + "</h2>"); - html.append( - "<p class='challengesectiondescription'>" + section.getDescription() + "</p>"); - - List<ChallengeDefinitionData> sectionData = new ArrayList<>(); - for (ChallengeDefinitionData challenge : section.getChallenges()) { - final ChallengeDefinitionData data = - new ChallengeDefinitionData( - challenge.id, challenge.name, challenge.description); - CHALLENGE challengeKey = challengeDefinitions.getChallenge(challenge.name); - if (challenge != null) { - data.status = challenger.statusOfChallenge(challengeKey); - data.addHints(challenge.hints); - data.addSolutions(challenge.solutions); - } - sectionData.add(data); - } - - html.append(renderChallengeData(sectionData)); - html.append("<p><a href='#toc'>Back to Section List</a></p>"); - } - - return html.toString(); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/MarkdownContentManager.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/MarkdownContentManager.java deleted file mode 100644 index 656846d4..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/MarkdownContentManager.java +++ /dev/null @@ -1,1295 +0,0 @@ -package uk.co.compendiumdev.challenge.gui; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import org.commonmark.Extension; -import org.commonmark.ext.gfm.tables.TablesExtension; -import org.commonmark.node.Node; -import org.commonmark.parser.Parser; -import org.commonmark.renderer.html.HtmlRenderer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; - -// TODO: consider adding caching for generated markdown pages - -public class MarkdownContentManager { - - private static final String DEFAULT_CANONICAL_HOST = "https://apichallenges.eviltester.com"; - private static final String DEFAULT_SITE_NAME = "API Challenges"; - private static final String DEFAULT_OG_IMAGE_PATH = - "/images/social/apichallenges-og-1200x630.png"; - private static final String DEFAULT_OG_TYPE_CONTENT = "article"; - private static final String DEFAULT_OG_TYPE_WEBSITE = "website"; - private static final String DEFAULT_TWITTER_CARD = "summary_large_image"; - private static final String DEFAULT_META_ROBOTS = "index,follow"; - private static final String DEFAULT_SCHEMA_TYPE_CONTENT = "Article"; - private static final String DEFAULT_SCHEMA_TYPE_INDEX = "WebPage"; - private static final String DEFAULT_SCHEMA_AUTHOR_NAME = "alan-richardson"; - private static final String DEFAULT_SCHEMA_PUBLISHER_NAME = "eviltester.com"; - private static final String DEFAULT_SCHEMA_AUTHOR_RESOURCE = "seo/schema-author.properties"; - private static final String DEFAULT_SCHEMA_PUBLISHER_RESOURCE = - "seo/schema-publisher.properties"; - private static final String DEFAULT_AUTHOR_BIO_PATH = "/author/alan-richardson"; - private static final String DEFAULT_AUTHOR_BIO_SNIPPET_RESOURCE = - "partials/author-bio-snippet.html"; - private static final String DEFAULT_NEXT_CHALLENGE_CTA_RESOURCE = - "partials/next-challenge-cta.html"; - - private final DefaultGUIHTML guiManagement; - Logger logger = LoggerFactory.getLogger(MarkdownContentManager.class); - private final Set<String> markdownContentPaths; - private final Properties schemaAuthorDefaults; - private final Properties schemaPublisherDefaults; - private String sideMenuText; - - public MarkdownContentManager( - final List<String> pathsToFileContent, final DefaultGUIHTML defaultGui) { - markdownContentPaths = new HashSet<>(); - markdownContentPaths.addAll(pathsToFileContent); - this.guiManagement = defaultGui; - this.schemaAuthorDefaults = loadPropertiesFromResource(DEFAULT_SCHEMA_AUTHOR_RESOURCE); - this.schemaPublisherDefaults = - loadPropertiesFromResource(DEFAULT_SCHEMA_PUBLISHER_RESOURCE); - sideMenuText = ""; - } - - // TODO: this is currently a hacked in solution for experimenting, pull it out into classes and - // create state enum - public String getResourceMarkdownFileAsHtml( - String contentFolder, String contentPath, Map<String, String> params) { - - if (contentPath.endsWith(".html")) { - contentPath = contentPath.replace(".html", ""); - } - - if (contentPath.endsWith(".md")) { - contentPath = contentPath.replace(".md", ""); - } - - String contentToFind = contentFolder + contentPath + ".md"; - - // if content does not exist in the list then exit - if (!markdownContentPaths.contains(contentToFind)) { - throw new IllegalArgumentException("Resource not found %s.md".formatted(contentPath)); - } - - return getHtmlVersionOfMarkdownContent(contentFolder, contentPath, params); - } - - public String getHtmlVersionOfMarkdownContent( - String contentFolder, String contentPath, Map<String, String> params) { - - InputStream inputStream = getResourceAsStream(contentFolder + contentPath + ".md"); - - String[] breadcrumbs = - Arrays.stream(contentPath.split("/")) - .filter(item -> item != null && !item.isEmpty()) - .toArray(String[]::new); - - StringBuilder bcHtmlHeader = new StringBuilder(); - String bcPath = ""; - int linksInBreadcrumb = 0; - if (breadcrumbs.length > 0) { - // https://spec.commonmark.org/0.29/#html-blocks - bcHtmlHeader.append("<div class=\"breadcrumb\">\n\n"); - bcHtmlHeader.append("<blockquote>"); - - for (String bc : breadcrumbs) { - bcPath = bcPath + bc; - - if (!bc.isEmpty()) { - - if (contentPath.endsWith(bc)) { - bcHtmlHeader.append(String.format(" %s", bc)); - } else { - // if there is an index file then show the breadcrumb - if (markdownContentPaths.contains(contentFolder + "/" + bcPath + ".md")) { - linksInBreadcrumb++; - bcHtmlHeader.append( - String.format("<a href=\"%s\">%s</a> >", "/" + bcPath, bc)); - } - } - } - bcPath = bcPath + "/"; - } - bcHtmlHeader.append("</blockquote>"); - bcHtmlHeader.append("</div >\n\n"); - } - - if (linksInBreadcrumb == 0) { - // do not output the breadcrumb - bcHtmlHeader = new StringBuilder(); - } - - String headerInject = ""; - String youtubeHeaderInject = ""; - - List<Extension> extensions = List.of(TablesExtension.create()); - // parse this html and output - Parser parser = Parser.builder().extensions(extensions).build(); - - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - String line; - - List<String> mdheaders = new ArrayList<>(); - - StringBuilder mdcontent = new StringBuilder(); - - // mdcontent.append(bcHeader); - - String state = "EXPECTING_HEADER"; - boolean addedToc = false; - String firstYouTubeVideoId = ""; - - try { - while ((line = reader.readLine()) != null) { - - if (line.equals("---") && state.equals("EXPECTING_HEADER")) { - state = "READING_HEADER"; // start of headers - continue; - } - - if (line.equals("---") && state.equals("READING_HEADER")) { - state = "READING_CONTENT"; // end of headers - continue; - } - - if (line.contains(": ") && state.equals("READING_HEADER")) { - mdheaders.add(line); - continue; - } - - if (state.equals("READING_HEADER") && line.trim().isEmpty()) { - // ignore empty lines in the header - continue; - } - - if (state.equals("READING_HEADER") && !line.trim().isEmpty()) { - // probably shouldn't be reading headers we found a non-empty line - state = "READING_CONTENT"; - } - - // process any macros - line = processMacrosInContentLine(line, params); - - if (firstYouTubeVideoId.isEmpty()) { - firstYouTubeVideoId = extractYouTubeVideoId(line); - } - - if (line.contains("youtube.com/watch")) { - if (youtubeHeaderInject.isEmpty()) { - // only import the facade if we are rendering youtube - youtubeHeaderInject = - "<script type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@1.5.0/lite-youtube.js\"></script>"; - } - } - - mdcontent.append(line + "\n"); - - // todo: better header parsing, and parse headers separate from the main body - if (!mdheaders.contains("template: index")) { - // inject table of contents - if (line.startsWith("# ") && !addedToc) { - addedToc = true; - mdcontent.append("\n<div id='toc'></div>\n"); - } - } - } - } catch (Exception e) { - logger.error("Markdown parsing error", e); - } - - if (mdheaders.contains("showads: true")) { - // this did render google ads - // headerInject = headerInject + - // "<script async - // src=\"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7132305589272099\"" + - // " crossorigin=\"anonymous\"></script>"; - } - - headerInject = headerInject + youtubeHeaderInject; - - String markdownFromResource = mdcontent.toString(); - Node document = parser.parse(markdownFromResource); - - HtmlRenderer renderer = HtmlRenderer.builder().extensions(extensions).build(); - - String pageTitle = "Content Page"; - String seoTitle = ""; - String pageDescription = ""; - String seoDescription = ""; - String metaRobots = ""; - String ogImage = ""; - String ogImageAlt = ""; - String ogType = ""; - String twitterCard = ""; - String twitterSite = ""; - String schemaType = ""; - String schemaAuthor = ""; - String schemaPublisher = ""; - String schemaImage = ""; - String schemaBreadcrumbEnabledRaw = ""; - String schemaHowToEnabledRaw = ""; - String schemaHowToStepsRaw = ""; - String schemaVideoEnabledRaw = ""; - String schemaVideoId = ""; - String nextChallengePath = ""; - String pageDatePublished = ""; - String pageLastModified = ""; - String canonicalUrl = DEFAULT_CANONICAL_HOST + contentPath; - - for (String aHeader : mdheaders) { - if (aHeader.startsWith("title: ")) { - pageTitle = aHeader.replace("title: ", ""); - } - if (aHeader.startsWith("seo_title: ")) { - seoTitle = aHeader.replace("seo_title: ", ""); - } - if (aHeader.startsWith("description: ")) { - pageDescription = aHeader.replace("description: ", ""); - } - if (aHeader.startsWith("seo_description: ")) { - seoDescription = aHeader.replace("seo_description: ", ""); - } - if (aHeader.startsWith("meta_robots: ")) { - metaRobots = aHeader.replace("meta_robots: ", ""); - } - if (aHeader.startsWith("canonical: ")) { - canonicalUrl = aHeader.replace("canonical: ", ""); - } - if (aHeader.startsWith("og_image: ")) { - ogImage = aHeader.replace("og_image: ", ""); - } - if (aHeader.startsWith("og_image_alt: ")) { - ogImageAlt = aHeader.replace("og_image_alt: ", ""); - } - if (aHeader.startsWith("og_type: ")) { - ogType = aHeader.replace("og_type: ", ""); - } - if (aHeader.startsWith("twitter_card: ")) { - twitterCard = aHeader.replace("twitter_card: ", ""); - } - if (aHeader.startsWith("twitter_site: ")) { - twitterSite = aHeader.replace("twitter_site: ", ""); - } - if (aHeader.startsWith("schema_type: ")) { - schemaType = aHeader.replace("schema_type: ", ""); - } - if (aHeader.startsWith("schema_author: ")) { - schemaAuthor = aHeader.replace("schema_author: ", ""); - } - if (aHeader.startsWith("schema_publisher: ")) { - schemaPublisher = aHeader.replace("schema_publisher: ", ""); - } - if (aHeader.startsWith("schema_image: ")) { - schemaImage = aHeader.replace("schema_image: ", ""); - } - if (aHeader.startsWith("schema_breadcrumb_enabled: ")) { - schemaBreadcrumbEnabledRaw = aHeader.replace("schema_breadcrumb_enabled: ", ""); - } - if (aHeader.startsWith("schema_howto_enabled: ")) { - schemaHowToEnabledRaw = aHeader.replace("schema_howto_enabled: ", ""); - } - if (aHeader.startsWith("schema_howto_steps: ")) { - schemaHowToStepsRaw = aHeader.replace("schema_howto_steps: ", ""); - } - if (aHeader.startsWith("schema_video_enabled: ")) { - schemaVideoEnabledRaw = aHeader.replace("schema_video_enabled: ", ""); - } - if (aHeader.startsWith("schema_video_id: ")) { - schemaVideoId = aHeader.replace("schema_video_id: ", ""); - } - if (aHeader.startsWith("next_challenge: ")) { - nextChallengePath = aHeader.replace("next_challenge: ", ""); - } - if (aHeader.startsWith("date:")) { - pageDatePublished = aHeader.replaceFirst("^date:\\s*", ""); - } - if (aHeader.startsWith("lastmod:")) { - pageLastModified = aHeader.replaceFirst("^lastmod:\\s*", ""); - } - } - - final String htmlTitle = seoTitle.isEmpty() ? pageTitle : seoTitle; - final String htmlDescription = seoDescription.isEmpty() ? pageDescription : seoDescription; - final String robotsValue = metaRobots.isEmpty() ? DEFAULT_META_ROBOTS : metaRobots; - - final String canonicalHost = - getEnvironmentOrDefault("SEO_CANONICAL_HOST", DEFAULT_CANONICAL_HOST); - final String canonicalAbsoluteUrl = absolutizeUrl(canonicalUrl, canonicalHost); - final String defaultOgImagePath = - getEnvironmentOrDefault("SEO_DEFAULT_OG_IMAGE", DEFAULT_OG_IMAGE_PATH); - final String ogImageAbsoluteUrl = - absolutizeUrl(ogImage.isEmpty() ? defaultOgImagePath : ogImage, canonicalHost); - final String ogImageAltValue = ogImageAlt.isEmpty() ? htmlTitle : ogImageAlt; - final boolean indexTemplate = mdheaders.contains("template: index"); - final String ogTypeValue = - ogType.isEmpty() - ? (indexTemplate ? DEFAULT_OG_TYPE_WEBSITE : DEFAULT_OG_TYPE_CONTENT) - : ogType; - final String twitterCardValue = twitterCard.isEmpty() ? DEFAULT_TWITTER_CARD : twitterCard; - final String twitterSiteValue = - twitterSite.isEmpty() - ? getEnvironmentOrDefault("SEO_TWITTER_SITE", "") - : twitterSite; - - if (!htmlDescription.isEmpty()) { - headerInject = - headerInject - + "<meta name='description' content='" - + escapeHtmlAttribute(htmlDescription) - + "'>"; - } - headerInject = - headerInject - + "<meta name='robots' content='" - + escapeHtmlAttribute(robotsValue) - + "'>"; - headerInject = - headerInject - + "<meta property='og:title' content='" - + escapeHtmlAttribute(htmlTitle) - + "'>"; - headerInject = - headerInject - + "<meta property='og:description' content='" - + escapeHtmlAttribute(htmlDescription) - + "'>"; - headerInject = - headerInject - + "<meta property='og:type' content='" - + escapeHtmlAttribute(ogTypeValue) - + "'>"; - headerInject = - headerInject - + "<meta property='og:url' content='" - + escapeHtmlAttribute(canonicalAbsoluteUrl) - + "'>"; - headerInject = - headerInject - + "<meta property='og:site_name' content='" - + escapeHtmlAttribute(DEFAULT_SITE_NAME) - + "'>"; - headerInject = - headerInject - + "<meta property='og:image' content='" - + escapeHtmlAttribute(ogImageAbsoluteUrl) - + "'>"; - headerInject = - headerInject - + "<meta property='og:image:alt' content='" - + escapeHtmlAttribute(ogImageAltValue) - + "'>"; - headerInject = - headerInject - + "<meta name='twitter:card' content='" - + escapeHtmlAttribute(twitterCardValue) - + "'>"; - headerInject = - headerInject - + "<meta name='twitter:title' content='" - + escapeHtmlAttribute(htmlTitle) - + "'>"; - headerInject = - headerInject - + "<meta name='twitter:description' content='" - + escapeHtmlAttribute(htmlDescription) - + "'>"; - headerInject = - headerInject - + "<meta name='twitter:image' content='" - + escapeHtmlAttribute(ogImageAbsoluteUrl) - + "'>"; - if (!twitterSiteValue.isEmpty()) { - headerInject = - headerInject - + "<meta name='twitter:site' content='" - + escapeHtmlAttribute(twitterSiteValue) - + "'>"; - } - - final String schemaTypeValue = - schemaType.isEmpty() - ? (indexTemplate ? DEFAULT_SCHEMA_TYPE_INDEX : DEFAULT_SCHEMA_TYPE_CONTENT) - : schemaType; - final String schemaImageAbsoluteUrl = - absolutizeUrl( - schemaImage.isEmpty() ? ogImageAbsoluteUrl : schemaImage, canonicalHost); - final String defaultSchemaAuthor = getSchemaAuthorDefaultName(); - final String schemaAuthorValue = - schemaAuthor.isEmpty() ? defaultSchemaAuthor : schemaAuthor; - final String defaultSchemaPublisher = getSchemaPublisherDefaultName(); - final String schemaPublisherValue = - schemaPublisher.isEmpty() ? defaultSchemaPublisher : schemaPublisher; - final String authorJobTitle = schemaAuthorDefaults.getProperty("jobTitle", "").trim(); - final String authorBioSnippet = - buildAuthorBioSnippet( - contentFolder, - contentPath, - schemaAuthorValue, - authorJobTitle, - DEFAULT_AUTHOR_BIO_PATH); - final String nextChallengeCtaSnippet = - buildNextChallengeCtaSnippet(contentFolder, contentPath, nextChallengePath); - final Boolean schemaBreadcrumbEnabled = parseOptionalBoolean(schemaBreadcrumbEnabledRaw); - final Boolean schemaHowToEnabled = parseOptionalBoolean(schemaHowToEnabledRaw); - final List<String> schemaHowToSteps = parseHowToSteps(schemaHowToStepsRaw); - final Boolean schemaVideoEnabled = parseOptionalBoolean(schemaVideoEnabledRaw); - final String pageDateModified = resolveDateModified(pageLastModified, pageDatePublished); - final String schemaJsonLd = - buildSchemaJsonLd( - canonicalHost, - canonicalAbsoluteUrl, - contentPath, - breadcrumbs, - htmlTitle, - htmlDescription, - schemaTypeValue, - schemaImageAbsoluteUrl, - schemaAuthorValue, - schemaPublisherValue, - pageDatePublished, - pageDateModified, - firstYouTubeVideoId, - schemaBreadcrumbEnabled, - schemaHowToEnabled, - schemaHowToSteps, - schemaVideoEnabled, - schemaVideoId); - if (!schemaJsonLd.isEmpty()) { - headerInject = headerInject + schemaJsonLd; - } - - StringBuilder html = new StringBuilder(); - html.append( - guiManagement.getPageStart( - htmlTitle, - """ - <script src='/js/toc.js'></script> - <script src='/js/externalize-links.js'></script> - """ - + headerInject, - canonicalAbsoluteUrl)); - - html.append(guiManagement.getMenuAsHTML()); - // todo: create proper templates - if (!mdheaders.contains("template: index")) { - html.append("<section class='doc-columns'>"); - html.append("<div class='left-column'>"); - html.append("<div class='side-toc'>"); - html.append(renderer.render(parser.parse(dropDownMenuAsMarkdown()))); - html.append("</div>"); - html.append("</div>"); - html.append("<div class='right-column'>"); - } - html.append(guiManagement.getStartOfMainContentMarker()); - html.append(bcHtmlHeader.toString()); - html.append("<div class=\"main-text-content\">\n"); - html.append(renderer.render(document)); - html.append("</div>\n"); - html.append(nextChallengeCtaSnippet); - html.append(authorBioSnippet); - html.append(guiManagement.getEndOfMainContentMarker()); - if (!mdheaders.contains("template: index")) { - html.append("</div>"); - html.append("</section>"); - } - html.append(guiManagement.getPageFooter()); - html.append(guiManagement.getPageEnd()); - - return html.toString(); - } - - // TODO: move this into a markdown file so it can be cached and amended easily - private String dropDownMenuAsMarkdown() { - if (sideMenuText.isEmpty()) { - sideMenuText = getResourceAsString("partials/content-index.md"); - } - - return sideMenuText; - } - - // TODO: improve the macro parsing - // TODO: add a variables macro so we can set variables like schemeHost (http://localhost:4567) - // and replace variables in the docs - should add a 'default.varname': parsing in the markdown, - // would allow showing the 'proper url' regardless of environment hosting - /* - Macros are added to the markdown with the following syntax {{<macro_name>}} - e.g. {{<HOST_URL>}} - Some of these macros are direct string replacement injection from the params map - Others like youtube-embed have been hard coded here - - */ - private String processMacrosInContentLine(String line, Map<String, String> params) { - - if (!line.contains("{{<")) { - return line; - } - - // String youTubeHtmlBlock = """ - // <div class="video-container"> - // <iframe class='youtube-video' title='Watch Video - $2' loading='lazy' - // src="https://www.youtube.com/embed/$1" allow="autoplay; encrypted-media" - // allowfullscreen></iframe> - // </div> - // <div><p class="center-text"><a href="https://www.youtube.com/watch?v=$1" - // target="_blank">Watch on YouTube - $2</a></p></div> - // """; - - // use YoutubeFacade https://github.com/justinribeiro/lite-youtube - String youTubeHtmlBlock = -""" -<lite-youtube videoid="$1"> - <a class="lite-youtube-fallback" href="https://www.youtube.com/watch?v=$1">Watch on YouTube: "$2"</a> -</lite-youtube> - """; - - String youtubeMacroRegex = - "\\{\\{<youtube-embed key=\"([a-zA-Z0-9_-]+)\" title=\"(.+)\">}}"; - line = line.replaceAll(youtubeMacroRegex, youTubeHtmlBlock); - - if (line.contains("{{<PARTIAL_SNIPPET")) { - String partialMacroRegex = "\\{\\{<PARTIAL_SNIPPET filename=\"(.+)\">}}"; - Pattern r = Pattern.compile(partialMacroRegex); - Matcher m = r.matcher(line); - if (m.find()) { - String filename = m.group(1); - String partialContent = getResourceAsString(filename); - line = line.replaceAll(partialMacroRegex, partialContent); - } - } - - for (String paramReplace : params.keySet()) { - String macroRegex = "\\{\\{<%s>}}".formatted(paramReplace); - line = line.replaceAll(macroRegex, params.get(paramReplace)); - } - return line; - } - - private String getResourceAsString(String fileName) { - return new BufferedReader(new InputStreamReader(getResourceAsStream(fileName))) - .lines() - .collect(Collectors.joining("\n")); - } - - private String getEnvironmentOrDefault(final String envName, final String defaultValue) { - final String envValue = System.getenv(envName); - if (envValue == null || envValue.trim().isEmpty()) { - return defaultValue; - } - return envValue.trim(); - } - - private String absolutizeUrl(final String url, final String host) { - if (url == null || url.isEmpty()) { - return ""; - } - final String trimmed = url.trim(); - if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { - return trimmed; - } - if (trimmed.startsWith("/")) { - return host + trimmed; - } - return host + "/" + trimmed; - } - - private String escapeHtmlAttribute(final String value) { - if (value == null) { - return ""; - } - return value.replace("&", "&") - .replace("\"", """) - .replace("'", "'") - .replace("<", "<") - .replace(">", ">"); - } - - private String buildSchemaJsonLd( - final String canonicalHost, - final String canonicalAbsoluteUrl, - final String contentPath, - final String[] breadcrumbs, - final String htmlTitle, - final String htmlDescription, - final String schemaTypeValue, - final String schemaImageAbsoluteUrl, - final String schemaAuthor, - final String schemaPublisherValue, - final String pageDatePublished, - final String pageDateModified, - final String firstYouTubeVideoId, - final Boolean schemaBreadcrumbEnabled, - final Boolean schemaHowToEnabled, - final List<String> schemaHowToSteps, - final Boolean schemaVideoEnabled, - final String schemaVideoId) { - - final String orgName = - schemaPublisherValue.isEmpty() - ? getEnvironmentOrDefault("SEO_SCHEMA_ORG_NAME", DEFAULT_SITE_NAME) - : schemaPublisherValue; - final String websiteName = - getEnvironmentOrDefault("SEO_SCHEMA_WEBSITE_NAME", DEFAULT_SITE_NAME); - final String logoUrl = - absolutizeUrl( - getEnvironmentOrDefault("SEO_SCHEMA_LOGO_URL", DEFAULT_OG_IMAGE_PATH), - canonicalHost); - - final List<String> sameAsLinks = - parseCommaSeparatedUrls(System.getenv("SEO_SCHEMA_SAME_AS")); - final String searchActionTemplate = System.getenv("SEO_SCHEMA_SEARCH_URL_TEMPLATE"); - final String authorUrl = getSchemaAuthorDefaultUrl(); - final List<String> authorSameAs = - parseCommaSeparatedUrls(schemaAuthorDefaults.getProperty("sameAs", "")); - final String authorJobTitle = schemaAuthorDefaults.getProperty("jobTitle", "").trim(); - final String publisherLegalName = - schemaPublisherDefaults.getProperty("legalName", "").trim(); - final List<String> publisherSameAs = - parseCommaSeparatedUrls(schemaPublisherDefaults.getProperty("sameAs", "")); - final String publisherContactType = - schemaPublisherDefaults.getProperty("contactType", "").trim(); - final String publisherEmail = schemaPublisherDefaults.getProperty("email", "").trim(); - final String publisherPhone = schemaPublisherDefaults.getProperty("telephone", "").trim(); - final String orgId = canonicalHost + "#organization"; - final String websiteId = canonicalHost + "#website"; - final String personId = canonicalHost + "#author"; - final String pageId = canonicalAbsoluteUrl + "#webpage"; - - final StringBuilder scripts = new StringBuilder(); - scripts.append( - toJsonLdScript( - buildOrganizationJson( - orgId, - orgName, - canonicalHost, - logoUrl, - sameAsLinks, - publisherLegalName, - publisherSameAs, - publisherContactType, - publisherEmail, - publisherPhone))); - scripts.append( - toJsonLdScript( - buildPersonJson( - personId, - schemaAuthor, - authorUrl, - authorSameAs, - authorJobTitle, - orgId))); - scripts.append( - toJsonLdScript( - buildWebsiteJson( - websiteId, - websiteName, - canonicalHost, - orgId, - searchActionTemplate))); - scripts.append( - toJsonLdScript( - buildPageJson( - schemaTypeValue, - pageId, - htmlTitle, - htmlDescription, - canonicalAbsoluteUrl, - schemaImageAbsoluteUrl, - personId, - orgId, - pageDatePublished, - pageDateModified))); - - final boolean includeBreadcrumb = - schemaBreadcrumbEnabled == null || schemaBreadcrumbEnabled; - if (includeBreadcrumb) { - final String breadcrumbJson = buildBreadcrumbListJson(canonicalHost, breadcrumbs); - scripts.append(toJsonLdScript(breadcrumbJson)); - } - - final String howToJson = - buildHowToJson( - contentPath, - canonicalAbsoluteUrl, - htmlTitle, - htmlDescription, - schemaImageAbsoluteUrl, - schemaHowToEnabled, - schemaHowToSteps); - scripts.append(toJsonLdScript(howToJson)); - - final String videoJson = - buildVideoObjectJson( - canonicalAbsoluteUrl, - htmlTitle, - htmlDescription, - schemaImageAbsoluteUrl, - firstYouTubeVideoId, - orgId, - schemaVideoEnabled, - schemaVideoId); - scripts.append(toJsonLdScript(videoJson)); - - return scripts.toString(); - } - - private String toJsonLdScript(final String json) { - if (json == null || json.isEmpty()) { - return ""; - } - // Prevent accidental script close within inline JSON. - final String safeJson = json.replace("</", "<\\/"); - return "<script type='application/ld+json'>" + safeJson + "</script>"; - } - - private List<String> parseCommaSeparatedUrls(final String csv) { - if (csv == null || csv.trim().isEmpty()) { - return Collections.emptyList(); - } - return Arrays.stream(csv.split(",")) - .map(String::trim) - .filter(item -> !item.isEmpty()) - .collect(Collectors.toList()); - } - - private String buildOrganizationJson( - final String organizationId, - final String organizationName, - final String canonicalHost, - final String logoUrl, - final List<String> sameAsLinks, - final String legalName, - final List<String> publisherSameAs, - final String contactType, - final String email, - final String telephone) { - StringBuilder json = new StringBuilder(); - json.append("{"); - json.append("\"@context\":\"https://schema.org\","); - json.append("\"@type\":\"Organization\","); - json.append("\"@id\":\"").append(escapeJsonValue(organizationId)).append("\","); - json.append("\"name\":\"").append(escapeJsonValue(organizationName)).append("\","); - json.append("\"url\":\"").append(escapeJsonValue(canonicalHost)).append("\""); - if (!legalName.isEmpty()) { - json.append(",\"legalName\":\"").append(escapeJsonValue(legalName)).append("\""); - } - - if (!logoUrl.isEmpty()) { - json.append(",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"") - .append(escapeJsonValue(logoUrl)) - .append("\"}"); - } - - final List<String> mergedSameAs = new ArrayList<>(); - mergedSameAs.addAll(sameAsLinks); - for (String aLink : publisherSameAs) { - if (!mergedSameAs.contains(aLink)) { - mergedSameAs.add(aLink); - } - } - - if (!mergedSameAs.isEmpty()) { - json.append(",\"sameAs\":["); - for (int i = 0; i < mergedSameAs.size(); i++) { - if (i > 0) { - json.append(","); - } - json.append("\"").append(escapeJsonValue(mergedSameAs.get(i))).append("\""); - } - json.append("]"); - } - - if (!contactType.isEmpty() || !email.isEmpty() || !telephone.isEmpty()) { - json.append(",\"contactPoint\":{\"@type\":\"ContactPoint\""); - if (!contactType.isEmpty()) { - json.append(",\"contactType\":\"") - .append(escapeJsonValue(contactType)) - .append("\""); - } - if (!email.isEmpty()) { - json.append(",\"email\":\"").append(escapeJsonValue(email)).append("\""); - } - if (!telephone.isEmpty()) { - json.append(",\"telephone\":\"").append(escapeJsonValue(telephone)).append("\""); - } - json.append("}"); - } - - json.append("}"); - return json.toString(); - } - - private String buildPersonJson( - final String personId, - final String authorName, - final String authorUrl, - final List<String> sameAsLinks, - final String jobTitle, - final String worksForOrgId) { - if (authorName == null || authorName.trim().isEmpty()) { - return ""; - } - StringBuilder json = new StringBuilder(); - json.append("{"); - json.append("\"@context\":\"https://schema.org\","); - json.append("\"@type\":\"Person\","); - json.append("\"@id\":\"").append(escapeJsonValue(personId)).append("\","); - json.append("\"name\":\"").append(escapeJsonValue(authorName)).append("\""); - if (authorUrl != null && !authorUrl.isEmpty()) { - json.append(",\"url\":\"").append(escapeJsonValue(authorUrl)).append("\""); - } - if (jobTitle != null && !jobTitle.isEmpty()) { - json.append(",\"jobTitle\":\"").append(escapeJsonValue(jobTitle)).append("\""); - } - if (!sameAsLinks.isEmpty()) { - json.append(",\"sameAs\":["); - for (int i = 0; i < sameAsLinks.size(); i++) { - if (i > 0) { - json.append(","); - } - json.append("\"").append(escapeJsonValue(sameAsLinks.get(i))).append("\""); - } - json.append("]"); - } - if (worksForOrgId != null && !worksForOrgId.isEmpty()) { - json.append(",\"worksFor\":{\"@id\":\"") - .append(escapeJsonValue(worksForOrgId)) - .append("\"}"); - } - json.append("}"); - return json.toString(); - } - - private String buildWebsiteJson( - final String websiteId, - final String websiteName, - final String canonicalHost, - final String organizationId, - final String searchActionTemplate) { - StringBuilder json = new StringBuilder(); - json.append("{"); - json.append("\"@context\":\"https://schema.org\","); - json.append("\"@type\":\"WebSite\","); - json.append("\"@id\":\"").append(escapeJsonValue(websiteId)).append("\","); - json.append("\"name\":\"").append(escapeJsonValue(websiteName)).append("\","); - json.append("\"url\":\"").append(escapeJsonValue(canonicalHost)).append("\""); - json.append(",\"publisher\":{\"@id\":\"") - .append(escapeJsonValue(organizationId)) - .append("\"}"); - - if (searchActionTemplate != null && !searchActionTemplate.trim().isEmpty()) { - json.append(",\"potentialAction\":{"); - json.append("\"@type\":\"SearchAction\","); - json.append("\"target\":\"") - .append(escapeJsonValue(searchActionTemplate.trim())) - .append("\","); - json.append("\"query-input\":\"required name=search_term_string\""); - json.append("}"); - } - - json.append("}"); - return json.toString(); - } - - private String buildPageJson( - final String schemaTypeValue, - final String pageId, - final String htmlTitle, - final String htmlDescription, - final String canonicalAbsoluteUrl, - final String schemaImageAbsoluteUrl, - final String personId, - final String organizationId, - final String pageDatePublished, - final String pageDateModified) { - StringBuilder json = new StringBuilder(); - json.append("{"); - json.append("\"@context\":\"https://schema.org\","); - json.append("\"@type\":\"").append(escapeJsonValue(schemaTypeValue)).append("\","); - json.append("\"@id\":\"").append(escapeJsonValue(pageId)).append("\","); - json.append("\"name\":\"").append(escapeJsonValue(htmlTitle)).append("\","); - if ("Article".equalsIgnoreCase(schemaTypeValue)) { - json.append("\"headline\":\"").append(escapeJsonValue(htmlTitle)).append("\","); - } - if (!htmlDescription.isEmpty()) { - json.append("\"description\":\"") - .append(escapeJsonValue(htmlDescription)) - .append("\","); - } - json.append("\"url\":\"").append(escapeJsonValue(canonicalAbsoluteUrl)).append("\","); - json.append("\"mainEntityOfPage\":\"") - .append(escapeJsonValue(canonicalAbsoluteUrl)) - .append("\""); - - if (!schemaImageAbsoluteUrl.isEmpty()) { - json.append(",\"image\":\"") - .append(escapeJsonValue(schemaImageAbsoluteUrl)) - .append("\""); - } - if (personId != null && !personId.isEmpty()) { - json.append(",\"author\":{\"@id\":\"").append(escapeJsonValue(personId)).append("\"}"); - } - if (organizationId != null && !organizationId.isEmpty()) { - json.append(",\"publisher\":{\"@id\":\"") - .append(escapeJsonValue(organizationId)) - .append("\"}"); - } - if (!pageDatePublished.isEmpty()) { - json.append(",\"datePublished\":\"") - .append(escapeJsonValue(pageDatePublished)) - .append("\""); - } - if (!pageDateModified.isEmpty()) { - json.append(",\"dateModified\":\"") - .append(escapeJsonValue(pageDateModified)) - .append("\""); - } - - json.append("}"); - return json.toString(); - } - - private String buildBreadcrumbListJson(final String canonicalHost, final String[] breadcrumbs) { - if (breadcrumbs == null || breadcrumbs.length == 0) { - return ""; - } - - StringBuilder json = new StringBuilder(); - json.append("{"); - json.append("\"@context\":\"https://schema.org\","); - json.append("\"@type\":\"BreadcrumbList\","); - json.append("\"itemListElement\":["); - - int position = 1; - json.append("{\"@type\":\"ListItem\",\"position\":") - .append(position++) - .append(",\"name\":\"Home\",\"item\":\"") - .append(escapeJsonValue(canonicalHost)) - .append("\"}"); - - String path = ""; - for (String crumb : breadcrumbs) { - if (crumb == null || crumb.isEmpty()) { - continue; - } - path = path + "/" + crumb; - json.append(",{\"@type\":\"ListItem\",\"position\":") - .append(position++) - .append(",\"name\":\"") - .append(escapeJsonValue(humanizeSlug(crumb))) - .append("\",\"item\":\"") - .append(escapeJsonValue(canonicalHost + path)) - .append("\"}"); - } - json.append("]"); - json.append("}"); - return json.toString(); - } - - private String buildHowToJson( - final String contentPath, - final String canonicalAbsoluteUrl, - final String htmlTitle, - final String htmlDescription, - final String schemaImageAbsoluteUrl, - final Boolean schemaHowToEnabled, - final List<String> schemaHowToSteps) { - final boolean isHowToSection = - contentPath.startsWith("/apichallenges/solutions/") - || contentPath.startsWith("/tutorials/"); - final boolean includeHowTo = - schemaHowToEnabled == null ? isHowToSection : schemaHowToEnabled; - if (!includeHowTo) { - return ""; - } - - final List<String> steps = new ArrayList<>(); - if (schemaHowToSteps != null && !schemaHowToSteps.isEmpty()) { - steps.addAll(schemaHowToSteps); - } - if (steps.size() < 2) { - return ""; - } - - StringBuilder json = new StringBuilder(); - json.append("{"); - json.append("\"@context\":\"https://schema.org\","); - json.append("\"@type\":\"HowTo\","); - json.append("\"name\":\"").append(escapeJsonValue(htmlTitle)).append("\","); - json.append("\"description\":\"").append(escapeJsonValue(htmlDescription)).append("\","); - json.append("\"url\":\"").append(escapeJsonValue(canonicalAbsoluteUrl)).append("\""); - if (!schemaImageAbsoluteUrl.isEmpty()) { - json.append(",\"image\":\"") - .append(escapeJsonValue(schemaImageAbsoluteUrl)) - .append("\""); - } - json.append(",\"step\":["); - for (int i = 0; i < steps.size(); i++) { - if (i > 0) { - json.append(","); - } - json.append("{\"@type\":\"HowToStep\",\"name\":\"") - .append(escapeJsonValue(steps.get(i))) - .append("\"}"); - } - json.append("]"); - json.append("}"); - return json.toString(); - } - - private String buildVideoObjectJson( - final String canonicalAbsoluteUrl, - final String htmlTitle, - final String htmlDescription, - final String schemaImageAbsoluteUrl, - final String firstYouTubeVideoId, - final String organizationId, - final Boolean schemaVideoEnabled, - final String schemaVideoId) { - final String videoIdToUse = - (schemaVideoId != null && !schemaVideoId.trim().isEmpty()) - ? schemaVideoId.trim() - : firstYouTubeVideoId; - final boolean includeVideoObject = - schemaVideoEnabled == null - ? videoIdToUse != null && !videoIdToUse.isEmpty() - : schemaVideoEnabled; - if (!includeVideoObject || videoIdToUse == null || videoIdToUse.isEmpty()) { - return ""; - } - final String videoWatchUrl = "https://www.youtube.com/watch?v=" + videoIdToUse; - final String embedUrl = "https://www.youtube.com/embed/" + videoIdToUse; - - StringBuilder json = new StringBuilder(); - json.append("{"); - json.append("\"@context\":\"https://schema.org\","); - json.append("\"@type\":\"VideoObject\","); - json.append("\"name\":\"").append(escapeJsonValue(htmlTitle)).append("\","); - json.append("\"description\":\"").append(escapeJsonValue(htmlDescription)).append("\","); - json.append("\"url\":\"").append(escapeJsonValue(canonicalAbsoluteUrl)).append("\","); - json.append("\"contentUrl\":\"").append(escapeJsonValue(videoWatchUrl)).append("\","); - json.append("\"embedUrl\":\"").append(escapeJsonValue(embedUrl)).append("\""); - if (!schemaImageAbsoluteUrl.isEmpty()) { - json.append(",\"thumbnailUrl\":\"") - .append(escapeJsonValue(schemaImageAbsoluteUrl)) - .append("\""); - } - if (organizationId != null && !organizationId.isEmpty()) { - json.append(",\"publisher\":{\"@id\":\"") - .append(escapeJsonValue(organizationId)) - .append("\"}"); - } - json.append("}"); - return json.toString(); - } - - private String humanizeSlug(final String slug) { - return slug.replace("-", " ").trim(); - } - - private String extractYouTubeVideoId(final String line) { - if (line == null || line.isEmpty()) { - return ""; - } - Pattern macroPattern = Pattern.compile("youtube-embed key=\"([a-zA-Z0-9_-]+)\""); - Matcher macroMatcher = macroPattern.matcher(line); - if (macroMatcher.find()) { - return macroMatcher.group(1); - } - - Pattern watchPattern = Pattern.compile("youtube\\.com/watch\\?v=([a-zA-Z0-9_-]+)"); - Matcher watchMatcher = watchPattern.matcher(line); - if (watchMatcher.find()) { - return watchMatcher.group(1); - } - return ""; - } - - private String escapeJsonValue(final String value) { - if (value == null) { - return ""; - } - return value.replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); - } - - private Properties loadPropertiesFromResource(final String resourcePath) { - final Properties properties = new Properties(); - try (InputStream inputStream = getResourceAsStream(resourcePath)) { - if (inputStream != null) { - properties.load(inputStream); - } - } catch (IOException e) { - logger.warn("Could not load schema properties from {}", resourcePath, e); - } - return properties; - } - - private String getSchemaAuthorDefaultName() { - final String authorFromResource = schemaAuthorDefaults.getProperty("name", "").trim(); - if (!authorFromResource.isEmpty()) { - return authorFromResource; - } - return DEFAULT_SCHEMA_AUTHOR_NAME; - } - - private String getSchemaAuthorDefaultUrl() { - return schemaAuthorDefaults.getProperty("url", "").trim(); - } - - private String getSchemaPublisherDefaultName() { - final String publisherFromResource = schemaPublisherDefaults.getProperty("name", "").trim(); - if (!publisherFromResource.isEmpty()) { - return publisherFromResource; - } - return DEFAULT_SCHEMA_PUBLISHER_NAME; - } - - static String resolveDateModified( - final String pageLastModified, final String pageDatePublished) { - if (pageLastModified != null && !pageLastModified.trim().isEmpty()) { - return pageLastModified.trim(); - } - if (pageDatePublished != null && !pageDatePublished.trim().isEmpty()) { - return pageDatePublished.trim(); - } - return ""; - } - - private Boolean parseOptionalBoolean(final String rawValue) { - if (rawValue == null || rawValue.trim().isEmpty()) { - return null; - } - final String value = rawValue.trim().toLowerCase(); - if (value.equals("true") || value.equals("yes") || value.equals("on")) { - return true; - } - if (value.equals("false") || value.equals("no") || value.equals("off")) { - return false; - } - return null; - } - - private List<String> parseHowToSteps(final String rawValue) { - if (rawValue == null || rawValue.trim().isEmpty()) { - return Collections.emptyList(); - } - return Arrays.stream(rawValue.split("\\|\\|")) - .map(String::trim) - .filter(item -> !item.isEmpty()) - .collect(Collectors.toList()); - } - - private String buildAuthorBioSnippet( - final String contentFolder, - final String contentPath, - final String authorName, - final String authorJobTitle, - final String authorBioPath) { - final boolean isContentPage = "content".equalsIgnoreCase(contentFolder); - final boolean isAuthorPage = - contentPath != null && contentPath.equalsIgnoreCase(authorBioPath); - if (!isContentPage || isAuthorPage) { - return ""; - } - - final String safeAuthorName = escapeHtmlAttribute(authorName); - final String safeAuthorJobTitle = escapeHtmlAttribute(authorJobTitle); - final String safeAuthorJobTitleWithPrefix = - safeAuthorJobTitle.isEmpty() ? "" : ", " + safeAuthorJobTitle; - final String safeAuthorBioPath = escapeHtmlAttribute(authorBioPath); - - try { - return getResourceAsString(DEFAULT_AUTHOR_BIO_SNIPPET_RESOURCE) - .replace("{{AUTHOR_NAME}}", safeAuthorName) - .replace("{{AUTHOR_JOB_TITLE_WITH_PREFIX}}", safeAuthorJobTitleWithPrefix) - .replace("{{AUTHOR_BIO_PATH}}", safeAuthorBioPath); - } catch (Exception e) { - logger.warn( - "Could not load author snippet resource {}, using fallback html", - DEFAULT_AUTHOR_BIO_SNIPPET_RESOURCE, - e); - return buildInlineAuthorBioSnippetFallback( - safeAuthorName, safeAuthorJobTitle, safeAuthorBioPath); - } - } - - private String buildInlineAuthorBioSnippetFallback( - final String safeAuthorName, - final String safeAuthorJobTitle, - final String safeAuthorBioPath) { - StringBuilder snippet = new StringBuilder(); - snippet.append("<aside class='author-bio-snippet' aria-label='About the author'>"); - snippet.append("<p><strong>Written by ").append(safeAuthorName).append("</strong>"); - if (!safeAuthorJobTitle.isEmpty()) { - snippet.append(", ").append(safeAuthorJobTitle); - } - snippet.append(".</p>"); - snippet.append("<p><a href='") - .append(safeAuthorBioPath) - .append("'>Read the full author bio and credentials</a>") - .append(".</p>"); - snippet.append("</aside>"); - return snippet.toString(); - } - - private String buildNextChallengeCtaSnippet( - final String contentFolder, final String contentPath, final String nextChallengePath) { - if (!"content".equalsIgnoreCase(contentFolder) || contentPath == null) { - return ""; - } - if (!contentPath.startsWith("/apichallenges/solutions/")) { - return ""; - } - if (nextChallengePath == null || nextChallengePath.trim().isEmpty()) { - return ""; - } - - final String trimmedPath = nextChallengePath.trim(); - final String ctaUrl = trimmedPath.startsWith("/") ? trimmedPath : "/" + trimmedPath; - final String ctaLabel = "Try the next challenge walkthrough"; - - try { - return getResourceAsString(DEFAULT_NEXT_CHALLENGE_CTA_RESOURCE) - .replace("{{NEXT_URL}}", escapeHtmlAttribute(ctaUrl)) - .replace("{{NEXT_LABEL}}", escapeHtmlAttribute(ctaLabel)); - } catch (Exception e) { - logger.warn( - "Could not load next challenge cta resource {}, using fallback html", - DEFAULT_NEXT_CHALLENGE_CTA_RESOURCE, - e); - return "<aside class='next-challenge-cta'><a class='next-challenge-cta-link' href='" - + escapeHtmlAttribute(ctaUrl) - + "'>" - + escapeHtmlAttribute(ctaLabel) - + "</a></aside>"; - } - } - - private InputStream getResourceAsStream(String fileName) { - - ClassLoader classLoader = getClass().getClassLoader(); - InputStream inputStream = classLoader.getResourceAsStream(fileName); - - // the stream holding the file content - if (inputStream == null) { - logger.error("content file not found: " + fileName); - return null; - } else { - return inputStream; - } - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/PageNotFoundResponse.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/PageNotFoundResponse.java deleted file mode 100644 index 7a16105a..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/PageNotFoundResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package uk.co.compendiumdev.challenge.gui; - -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpServerResponse; -import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; - -public class PageNotFoundResponse { - private final DefaultGUIHTML guiManagement; - - public PageNotFoundResponse(DefaultGUIHTML guiManagement) { - this.guiManagement = guiManagement; - } - - public void amendResponse(HttpServerResponse response, String bodyStringAppend) { - response.status(404); - response.type("text/html"); - StringBuilder html = new StringBuilder(); - html.append(guiManagement.getPageStart("404 Not Found", "", "")); - html.append(guiManagement.getMenuAsHTML()); - html.append(guiManagement.getStartOfMainContentMarker()); - html.append("<h1>Page Not Found</h1>"); - html.append(bodyStringAppend); - html.append(guiManagement.getEndOfMainContentMarker()); - html.append(guiManagement.getPageFooter()); - html.append(guiManagement.getPageEnd()); - response.type("text/html"); - response.body(html.toString()); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/ResourceContentScanner.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/ResourceContentScanner.java deleted file mode 100644 index 91e078a2..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/ResourceContentScanner.java +++ /dev/null @@ -1,121 +0,0 @@ -package uk.co.compendiumdev.challenge.gui; - -import io.github.classgraph.ClassGraph; -import io.github.classgraph.Resource; -import io.github.classgraph.ScanResult; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.time.DateTimeException; -import java.time.Instant; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.util.*; - -public class ResourceContentScanner { - - List<String> availableContent = new ArrayList<>(); - - public List<String> scanForFullPathsOfExtensionsIn(String folder, String extension) { - List<String> pathsToFileContent = new ArrayList<>(); - try (ScanResult scanResult = new ClassGraph().acceptPaths(folder).scan()) { - scanResult - .getResourcesWithExtension(extension) - .forEach( - (Resource res) -> { - pathsToFileContent.add(res.getPath()); - }); - } - return pathsToFileContent; - } - - public Map<String, LocalDate> scanForUrlsWithDates(String folder, String extension) { - Map<String, LocalDate> urlsWithLastModDates = new HashMap<>(); - try (ScanResult scanResult = new ClassGraph().acceptPaths(folder).scan()) { - scanResult - .getResourcesWithExtension(extension) - .forEach( - (Resource res) -> { - final LocalDate dateFromFrontMatter = - extractDateFromFrontMatter(res); - urlsWithLastModDates.put( - res.getPath() - .replaceFirst(folder, "") - .replace("." + extension, ""), - dateFromFrontMatter == null - ? Instant.ofEpochMilli(res.getLastModified()) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - : dateFromFrontMatter); - }); - } - return urlsWithLastModDates; - } - - private LocalDate extractDateFromFrontMatter(final Resource res) { - - try (BufferedReader reader = - new BufferedReader(new InputStreamReader(res.open(), StandardCharsets.UTF_8))) { - - String line; - boolean inHeader = false; - String lastmod = ""; - String date = ""; - - while ((line = reader.readLine()) != null) { - if (line.equals("---") && !inHeader) { - inHeader = true; - continue; - } - if (line.equals("---") && inHeader) { - break; - } - if (!inHeader) { - // no front matter - break; - } - - if (line.startsWith("lastmod: ")) { - lastmod = line.replaceFirst("^lastmod:\\s*", "").trim(); - } - if (line.startsWith("date: ")) { - date = line.replaceFirst("^date:\\s*", "").trim(); - } - } - - final LocalDate parsedLastmod = parseDateValue(lastmod); - if (parsedLastmod != null) { - return parsedLastmod; - } - return parseDateValue(date); - - } catch (IOException ignored) { - return null; - } - } - - private LocalDate parseDateValue(final String rawDateValue) { - if (rawDateValue == null || rawDateValue.trim().isEmpty()) { - return null; - } - final String value = rawDateValue.trim(); - - try { - return LocalDate.parse(value); - } catch (DateTimeException ignored) { - // try date-time format - } - - try { - return OffsetDateTime.parse(value).toLocalDate(); - } catch (DateTimeException ignored) { - return null; - } - } - - public void addPathsToAvailableContent(List<String> pathsToFileContent) { - availableContent.addAll(pathsToFileContent); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/SiteMapXml.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/SiteMapXml.java deleted file mode 100644 index edc55b6d..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/SiteMapXml.java +++ /dev/null @@ -1,63 +0,0 @@ -package uk.co.compendiumdev.challenge.gui; - -import java.util.ArrayList; -import java.util.List; - -public class SiteMapXml { - - List<SiteMapUrl> urls = new ArrayList<>(); - String cache = ""; - - public void addUrl(String aUrl, String lastModified) { - urls.add(new SiteMapUrl(aUrl, lastModified)); - } - - public boolean contains(String aUrl) { - for (SiteMapUrl url : urls) { - if (aUrl.equals(url.aUrl())) { - return true; - } - } - return false; - } - - public String asSitemapXml() { - - if (cache.isEmpty()) { - StringBuilder sitemapxml = new StringBuilder(); - sitemapxml.append( - """ - <?xml version="1.0" encoding="utf-8"?> - <urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> - """ - .stripIndent() - .stripLeading()); - for (SiteMapUrl aUrl : urls) { - sitemapxml.append( - """ - <url> - <loc>%s</loc> - <lastmod>%s</lastmod> - <changefreq>weekly</changefreq> - <priority>0.5</priority> - </url> - """ - .formatted(aUrl.aUrl(), aUrl.lastModified()) - .stripIndent() - .stripLeading()); - } - sitemapxml.append( - """ - </urlset> - """ - .stripIndent() - .stripLeading()); - cache = sitemapxml.toString(); - } - - return cache; - } -} - -record SiteMapUrl(String aUrl, String lastModified) {} -; diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/httpserver/HttpMessageLengthValidator.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/httpserver/HttpMessageLengthValidator.java deleted file mode 100644 index a12bebb3..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/httpserver/HttpMessageLengthValidator.java +++ /dev/null @@ -1,70 +0,0 @@ -package uk.co.compendiumdev.challenge.httpserver; - -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpServerRequest; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpServerResponse; -import uk.co.compendiumdev.thingifier.adapter.httpserver.conversion.HttpServerRequestToInternalHttpRequest; -import uk.co.compendiumdev.thingifier.adapter.httpserver.conversion.InternalHttpResponseToHttpServer; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.HttpApiResponseToInternalHttpResponse; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.InternalHttpRequestToHttpApiRequest; -import uk.co.compendiumdev.thingifier.api.ermodelconversion.JsonThing; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; -import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; - -public class HttpMessageLengthValidator { - - public static final int DEFAULT_MAX_LENGTH = 24000; - - private final int maxLength; - - public HttpMessageLengthValidator() { - this(DEFAULT_MAX_LENGTH); - } - - HttpMessageLengthValidator(int maxLength) { - this.maxLength = maxLength; - } - - public boolean rejectRequestTooLong( - final HttpServerRequest request, final HttpServerResponse result) { - if (contentLength(request) > this.maxLength) { - // randomly picked 24K - result.status(413); - return true; - } - return false; - } - - public String messageTooLongErrorResponse( - final ThingifierApiConfig apiConfig, - final HttpServerRequest request, - final HttpServerResponse result) { - final ApiResponse response = - ApiResponse.error( - 413, - String.format( - "Error: request too large, max allowed is %d bytes", - this.maxLength)); - - final InternalHttpRequest internalRequest = - HttpServerRequestToInternalHttpRequest.convert(request); - final HttpApiRequest myRequest = - InternalHttpRequestToHttpApiRequest.convert(internalRequest); - JsonThing jsonThing = new JsonThing(apiConfig.jsonOutput()); - final HttpApiResponse httpApiResponse = - new HttpApiResponse(myRequest.getHeaders(), response, jsonThing, apiConfig); - - return InternalHttpResponseToHttpServer.convert( - HttpApiResponseToInternalHttpResponse.convert(httpApiResponse), result); - } - - private int contentLength(final HttpServerRequest request) { - try { - return Integer.parseInt(request.contentLength()); - } catch (NumberFormatException e) { - return 0; - } - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/AwsS3Storage.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/AwsS3Storage.java deleted file mode 100644 index 8051cb02..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/AwsS3Storage.java +++ /dev/null @@ -1,97 +0,0 @@ -package uk.co.compendiumdev.challenge.persistence; - -// import com.amazonaws.regions.Regions; -// import com.amazonaws.services.s3.AmazonS3; -// import com.amazonaws.services.s3.AmazonS3ClientBuilder; -// import com.amazonaws.services.s3.model.GetObjectRequest; -// import com.amazonaws.services.s3.model.S3Object; -// import com.google.gson.Gson; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.challenge.ChallengerAuthData; - -public class AwsS3Storage implements ChallengerPersistenceMechanism { - - Logger logger = LoggerFactory.getLogger(AwsS3Storage.class); - - // static AmazonS3 s3Client; - - // to work we need environment variables for - // AWS_ACCESS_KEY_ID - // AWS_SECRET_ACCESS_KEY - - // https://docs.aws.amazon.com/AmazonS3/latest/dev/RetrievingObjectUsingJava.html - // https://docs.aws.amazon.com/AmazonS3/latest/dev/UploadObjSingleOpJava.html - - public AwsS3Storage(boolean allowSave, boolean allowLoad, String awsBucket) { - logger.debug( - "AWS S3 storage unavailable in this build: allowSave={}, allowLoad={}, bucket={}", - allowSave, - allowLoad, - awsBucket); - } - - @Override - public PersistenceResponse saveChallengerStatus(final ChallengerAuthData data) { - - return new PersistenceResponse() - .withSuccess(false) - .withErrorMessage("AWS not supported by current build"); - - // // by default will not save to aws - need to add environment variable - // if(!allowSave){ - // return new PersistenceResponse().withSuccess(false).withErrorMessage("AWS - // Configuration does not allow saving challenger status"); - // } - // - // if(data==null){ - // return new PersistenceResponse().withSuccess(false).withErrorMessage("no data - // provided"); - // } - // - // - // try{ - // ensureClientExists(); - // - // final String dataString = new Gson().toJson(data); - // // Upload a text string as a new object. - // //s3Client.putObject(bucketName, data.getXChallenger(), dataString); - // return new PersistenceResponse().withSuccess(true); - // } catch (Exception e) { - // - // logger.error("Error storing data to bucket for guid: {}", - // data.getXChallenger(), e); - // return new PersistenceResponse().withSuccess(false).withErrorMessage("Error - // storing data to S3"); - // } - } - - @Override - public PersistenceResponse loadChallengerStatus(final String guid) { - - return new PersistenceResponse() - .withSuccess(false) - .withErrorMessage("AWS not supported by current build"); - - // // by default will not save from aws - need to add environment variable - // if(!allowLoad){ - // return new PersistenceResponse().withSuccess(false).withErrorMessage("AWS - // Configuration does not allow loading challenger status"); - // } - // - // try { - // ensureClientExists(); - // - // final S3Object fullObject = s3Client.getObject(new - // GetObjectRequest(bucketName, guid)); - // String dataString = getObjectContent(fullObject.getObjectContent()); - // return new PersistenceResponse().withSuccess(true).withChallengerAuthData( - // new Gson().fromJson(dataString, ChallengerAuthData.class)); - // } catch (Exception e) { - // logger.error("Error Reading Challenge Status From S3: {}", guid, e); - // return new PersistenceResponse(). - // withSuccess(false). - // withErrorMessage("Error Reading Challenges Status from S3"); - // } - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/ChallengerFileStorage.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/ChallengerFileStorage.java deleted file mode 100644 index 8011a2fe..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/ChallengerFileStorage.java +++ /dev/null @@ -1,118 +0,0 @@ -package uk.co.compendiumdev.challenge.persistence; - -import com.google.gson.Gson; -import java.io.*; -import java.nio.file.Files; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.challenge.ChallengerAuthData; - -public class ChallengerFileStorage - implements ChallengerPersistenceMechanism, DatabaseContentPersistenceMechanism { - - Logger logger = LoggerFactory.getLogger(ChallengerFileStorage.class); - - public PersistenceResponse saveChallengerStatus(final ChallengerAuthData data) { - - File folder = new File(System.getProperty("User.dir"), "challengersessions"); - folder.mkdirs(); - - File file = new File(folder, getFileNameFor(data.getXChallenger())); - - if (!file.exists()) { - logger.warn("Creating new challenger status file: {}", file.getAbsolutePath()); - } - - try (FileOutputStream out = new FileOutputStream(file)) { - final String dataString = new Gson().toJson(data); - out.write(dataString.getBytes()); - return new PersistenceResponse().withSuccess(true); - } catch (IOException e) { - logger.error("Error writing to file: {}", file.getAbsolutePath(), e); - return new PersistenceResponse().withSuccess(false).withErrorMessage(e.getMessage()); - } - } - - private String getFileNameFor(final String guid) { - return guid + ".data.txt"; - } - - public PersistenceResponse loadChallengerStatus(final String guid) { - File folder = new File(System.getProperty("User.dir"), "challengersessions"); - File file = new File(folder, getFileNameFor(guid)); - - if (!file.exists()) { - String message = "Could not find challenger status file: " + file.getAbsolutePath(); - if (guid.startsWith("rest-api-challenges-single-player")) { - message = - message - + "\nChallenger status file will be created when a challenge is completed."; - } - logger.warn(message); - return new PersistenceResponse().withSuccess(false).withErrorMessage(message); - } - - try { - final byte[] data = Files.readAllBytes(file.toPath()); - final String dataString = new String(data); - return new PersistenceResponse() - .withSuccess(true) - .withChallengerAuthData( - new Gson().fromJson(dataString, ChallengerAuthData.class)); - } catch (IOException e) { - logger.error("Error Reading Challenge Status From file: {}", file.getAbsolutePath(), e); - return new PersistenceResponse().withSuccess(false).withErrorMessage(e.getMessage()); - } - } - - public PersistenceResponse saveDatabaseContent(String guid, String databaseContents) { - - File folder = new File(System.getProperty("User.dir"), "challengersessions"); - folder.mkdirs(); - - File file = new File(folder, getDatabaseFileNameFor(guid)); - - if (!file.exists()) { - logger.warn("Creating new challenger database file: {}", file.getAbsolutePath()); - } - - try (FileOutputStream out = new FileOutputStream(file)) { - String dataString = databaseContents == null ? "" : databaseContents; - out.write(dataString.getBytes()); - return new PersistenceResponse().withSuccess(true); - } catch (IOException e) { - logger.error("Error writing to file: {}", file.getAbsolutePath(), e); - return new PersistenceResponse().withSuccess(false).withErrorMessage(e.getMessage()); - } - } - - private String getDatabaseFileNameFor(String guid) { - return guid + ".content.txt"; - } - - public PersistenceResponse loadDatabaseContent(String guid) { - File folder = new File(System.getProperty("User.dir"), "challengersessions"); - File file = new File(folder, getDatabaseFileNameFor(guid)); - - if (!file.exists()) { - String message = "Could not find database contents file: " + file.getAbsolutePath(); - if (guid.startsWith("rest-api-challenges-single-player")) { - message = - message - + "\nDatabase content file will be created when a challenge is completed."; - } - logger.warn(message); - return new PersistenceResponse().withSuccess(false).withErrorMessage(message); - } - - try { - final byte[] data = Files.readAllBytes(file.toPath()); - final String dataString = new String(data); - - return new PersistenceResponse().withSuccess(true).withDatabaseContents(dataString); - } catch (IOException e) { - logger.error("Error Reading Database content From file: {}", file.getAbsolutePath(), e); - return new PersistenceResponse().withSuccess(false).withErrorMessage(e.getMessage()); - } - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/ChallengerPersistenceMechanism.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/ChallengerPersistenceMechanism.java deleted file mode 100644 index 721fde0c..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/ChallengerPersistenceMechanism.java +++ /dev/null @@ -1,10 +0,0 @@ -package uk.co.compendiumdev.challenge.persistence; - -import uk.co.compendiumdev.challenge.ChallengerAuthData; - -public interface ChallengerPersistenceMechanism { - - PersistenceResponse saveChallengerStatus(ChallengerAuthData data); - - PersistenceResponse loadChallengerStatus(String guid); -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/DatabaseContentPersistenceMechanism.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/DatabaseContentPersistenceMechanism.java deleted file mode 100644 index 5df0b9e2..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/DatabaseContentPersistenceMechanism.java +++ /dev/null @@ -1,8 +0,0 @@ -package uk.co.compendiumdev.challenge.persistence; - -public interface DatabaseContentPersistenceMechanism { - - PersistenceResponse saveDatabaseContent(String guid, String databaseContents); - - PersistenceResponse loadDatabaseContent(String guid); -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/PersistenceLayer.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/PersistenceLayer.java deleted file mode 100644 index ffc76626..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/PersistenceLayer.java +++ /dev/null @@ -1,165 +0,0 @@ -package uk.co.compendiumdev.challenge.persistence; - -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.ChallengerState; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.thingifier.api.ermodelconversion.JsonPopulator; - -public class PersistenceLayer { - - private StorageType storeOn; - - // TODO: have a database persistence layer e.g. 'save to disk' option for the todos - // TODO: single player mode should have this switched on by default - // TODO: allow configuring what is on and what is off for any storage type using constructor - // rather than environment variables - // todo: add all active storage mechanisms in a list and store on all - switch it off by - // removing from list - - ChallengerPersistenceMechanism file = new ChallengerFileStorage(); - DatabaseContentPersistenceMechanism dbfile = (DatabaseContentPersistenceMechanism) file; - - static ChallengerPersistenceMechanism aws; - boolean allowSaveToS3 = false; - boolean allowLoadFromS3 = false; - - public PersistenceResponse tryToLoadChallenger( - final Challengers challengers, final String xChallengerGuid) { - - final PersistenceResponse response = loadChallengerStatus(xChallengerGuid); - - if (response.isSuccess()) { - ChallengerAuthData challenger = - new ChallengerAuthData(challengers.getDefinedChallenges()) - .fromData(response.getAuthData(), challengers.getDefinedChallenges()); - if (xChallengerGuid.equals(Challengers.SINGLE_PLAYER_GUID)) { - challenger.setXChallengerGUID(xChallengerGuid); - } - challenger.touch(); - challenger.setState( - ChallengerState.LOADED_FROM_PERSISTENCE); // refresh last accessed date - challengers.put(challenger); - - String databaseName = challenger.getXChallenger(); - challengers.getErModel().createInstanceDatabaseIfNotExisting(databaseName); - - // did we also load the data? if so, populate the database from it - if (!response.getDatabaseContents().isEmpty()) { - new JsonPopulator(response.getDatabaseContents()) - .populate( - challengers.getErModel().getSchema(), - challengers.getErModel().getStore(databaseName)); - } else { - // set the database to default values - challengers.getErModel().populateDatabase(databaseName); - } - } - - return response; - } - - public enum StorageType { - LOCAL, - CLOUD, - NONE - }; - - public PersistenceLayer(StorageType storeWhere) { - this.storeOn = storeWhere; - - if (this.storeOn == StorageType.CLOUD) { - - String allow_save = System.getenv("AWS_ALLOW_SAVE"); - if (allow_save != null && allow_save.toLowerCase().trim().equals("true")) { - allowSaveToS3 = false; - } - - String allow_load = System.getenv("AWS_ALLOW_LOAD"); - if (allow_load != null && allow_load.toLowerCase().trim().equals("true")) { - allowLoadFromS3 = false; - } - - String bucketName = System.getenv("AWSBUCKET"); - aws = new AwsS3Storage(allowSaveToS3, allowLoadFromS3, bucketName); - } - } - - public PersistenceResponse saveChallengerStatus( - ChallengerAuthData data, String databaseContents) { - - if (storeOn == StorageType.LOCAL) { - PersistenceResponse fileStoreChallenger = file.saveChallengerStatus(data); - PersistenceResponse fileStoreDatabase = - dbfile.saveDatabaseContent(data.getXChallenger(), databaseContents); - return new PersistenceResponse() - .withSuccess(fileStoreChallenger.isSuccess() && fileStoreDatabase.isSuccess()) - .withErrorMessage( - fileStoreChallenger.getErrorMessage() - + fileStoreDatabase.getErrorMessage()) - .withDatabaseContents(fileStoreDatabase.getDatabaseContents()) - .withChallengerAuthData(fileStoreChallenger.getAuthData()); - } - - if (storeOn == StorageType.CLOUD && aws != null) { - return aws.saveChallengerStatus(data); - } - - // if(storeOn==StorageType.NONE){ - return new PersistenceResponse() - .withSuccess(false) - .withErrorMessage("No Persistence Configured - store in memory only."); - // } - } - - public PersistenceResponse loadChallengerStatus(String guid) { - - if (storeOn == StorageType.LOCAL) { - PersistenceResponse fileStoreChallenger = file.loadChallengerStatus(guid); - PersistenceResponse fileStoreDatabase = dbfile.loadDatabaseContent(guid); - return new PersistenceResponse() - .withSuccess(fileStoreChallenger.isSuccess()) - . // only track challenger success && fileStoreDatabase.isSuccess()). - withErrorMessage( - fileStoreChallenger.getErrorMessage() - + fileStoreDatabase.getErrorMessage()) - .withDatabaseContents(fileStoreDatabase.getDatabaseContents()) - .withChallengerAuthData(fileStoreChallenger.getAuthData()); - } - - if (storeOn == StorageType.CLOUD && aws != null) { - return aws.loadChallengerStatus(guid); - } - - // if(storeOn==StorageType.NONE){ - return new PersistenceResponse() - .withSuccess(false) - .withErrorMessage("No Persistence Configured - store in memory only."); - // } - } - - public boolean willAutoSaveChallengerStatusToPersistenceLayer() { - - if (storeOn == StorageType.LOCAL) { - return true; - } - - if (storeOn == StorageType.CLOUD && allowSaveToS3) { - return true; - } - - return false; - } - - public boolean willAutoLoadChallengerStatusFromPersistenceLayer() { - - if (storeOn == StorageType.LOCAL) { - return true; - } - - if (storeOn == StorageType.CLOUD && allowLoadFromS3) { - return true; - } - - return false; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/PersistenceResponse.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/PersistenceResponse.java deleted file mode 100644 index f74c0781..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/persistence/PersistenceResponse.java +++ /dev/null @@ -1,54 +0,0 @@ -package uk.co.compendiumdev.challenge.persistence; - -import uk.co.compendiumdev.challenge.ChallengerAuthData; - -public class PersistenceResponse { - - private ChallengerAuthData authData; - private boolean success; - private String errorMessage; - private String databaseContents; - - public PersistenceResponse() { - this.errorMessage = ""; - this.success = true; - this.authData = null; - this.databaseContents = ""; - } - - public PersistenceResponse withSuccess(final boolean successStatus) { - this.success = successStatus; - return this; - } - - public PersistenceResponse withErrorMessage(final String errorMessage) { - this.errorMessage = errorMessage; - return this; - } - - public PersistenceResponse withChallengerAuthData(final ChallengerAuthData authData) { - this.authData = authData; - return this; - } - - public ChallengerAuthData getAuthData() { - return this.authData; - } - - public boolean isSuccess() { - return success; - } - - public String getErrorMessage() { - return this.errorMessage; - } - - public PersistenceResponse withDatabaseContents(String jsonString) { - this.databaseContents = jsonString; - return this; - } - - public String getDatabaseContents() { - return this.databaseContents; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorHttpApiRequestHandler.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorHttpApiRequestHandler.java deleted file mode 100644 index 6c9fd7d3..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorHttpApiRequestHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.mirror; - -import uk.co.compendiumdev.thingifier.adapter.http.routehandlers.HttpApiRequestHandler; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.AcceptHeaderParser; -import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; - -public class MirrorHttpApiRequestHandler implements HttpApiRequestHandler { - private final EntityDefinition entityDefn; - - public MirrorHttpApiRequestHandler(final EntityDefinition entityDefn) { - this.entityDefn = entityDefn; - } - - public ApiResponse handle(final HttpApiRequest myRequest) { - // convert request into a string for message body- getRequestDetails - String requestDetails = new MirrorHttpApiTextRequestHandler().getRequestDetails(myRequest); - - final AcceptHeaderParser parser = new AcceptHeaderParser(myRequest.getHeader("accept")); - - ApiResponse response = null; - - // handle text separately as the main api does not 'do' text - if (parser.hasAskedForTEXT()) { - response = ApiResponse.success().setHeader("Content-Type", "text/plain"); - response.setBody(requestDetails); - } - - if (response == null) { - // let main code handle formatting etc. - response = - ApiResponse.success() - .returnSingleDraft( - EntityInstanceDraft.forEntity(entityDefn) - .withField("details", requestDetails)); - } - - return response; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorHttpApiTextRequestHandler.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorHttpApiTextRequestHandler.java deleted file mode 100644 index 467a0d0c..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorHttpApiTextRequestHandler.java +++ /dev/null @@ -1,92 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.mirror; - -import uk.co.compendiumdev.thingifier.adapter.http.routehandlers.HttpApiRequestHandler; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeaderPair; -import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.core.query.FilterBy; - -public class MirrorHttpApiTextRequestHandler implements HttpApiRequestHandler { - - public ApiResponse handle(final HttpApiRequest myRequest) { - // convert request into a string for message body- getRequestDetails - String requestDetails = getRequestDetails(myRequest); - - ApiResponse response; - - response = ApiResponse.success().setHeader("Content-Type", "text/plain"); - response.setBody(requestDetails); - - return response; - } - - public String getRequestDetails(final HttpApiRequest myRequest) { - StringBuilder output = new StringBuilder(); - - output.append(String.format("%s %s", myRequest.getVerb(), myRequest.getUrl())); - output.append("\n"); - - output.append("\n"); - output.append("Parsed Query Params"); - output.append("\n"); - output.append("============"); - output.append("\n"); - for (String queryParam : myRequest.getQueryParamNames()) { - output.append( - String.format("%s: %s", queryParam, myRequest.rawQueryParamsValue(queryParam))); - output.append("\n"); - } - - output.append("\n"); - output.append("Raw Query Params"); - output.append("\n"); - output.append("============"); - output.append("\n"); - for (FilterBy queryParam : myRequest.getFilterableQueryParams().toList()) { - output.append( - String.format( - "%s %s %s", - queryParam.fieldName, - queryParam.filterOperation, - queryParam.fieldValue)); - output.append("\n"); - } - - output.append("\n"); - output.append("IP"); - output.append("\n"); - output.append("======="); - output.append("\n"); - output.append(myRequest.getIP()); - output.append("\n"); - - output.append("\n"); - output.append("Raw Headers"); - output.append("\n"); - output.append("======="); - output.append("\n"); - for (HttpHeaderPair header : myRequest.getHeadersList()) { - output.append(String.format("%s: %s", header.key, header.value)); - output.append("\n"); - } - - output.append("\n"); - output.append("Processed Headers"); - output.append("\n"); - output.append("======="); - output.append("\n"); - for (String header : myRequest.getHeaders().asMap().keySet()) { - output.append(String.format("%s: %s", header, myRequest.getHeader(header))); - output.append("\n"); - } - output.append("\n"); - - output.append("Body"); - output.append("\n"); - output.append("===="); - output.append("\n"); - output.append(myRequest.getBody()); - output.append("\n"); - return output.toString(); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorRoutes.java deleted file mode 100644 index 5282448d..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorRoutes.java +++ /dev/null @@ -1,172 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.mirror; - -import static uk.co.compendiumdev.thingifier.adapter.httpserver.ServerRoutes.*; - -import java.util.ArrayList; -import java.util.List; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.httpserver.AdhocDocumentedHttpRouteConfigurer; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpRouteHandler; -import uk.co.compendiumdev.thingifier.adapter.httpserver.SimpleHttpRouteCreator; -import uk.co.compendiumdev.thingifier.adapter.httpserver.ThingifierAutoDocGenRouting; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; -import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; -import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; - -/* - Create a set of custom end points that do not use a thingifier, but - take advantage of the documentation generation functionality provided - by the thingifier -*/ -public class MirrorRoutes { - - public void configure( - final ThingifierApiDocumentationDefn apiDefn, DefaultGUIHTML guiTemplates) { - - // /mirror should be the GUI with api below it - String endpoint = "/mirror/request"; - String rawEndPoint = "/mirror/raw"; - - RequestMirror requestMirror = new RequestMirror(); - - // redirect a GET to "/fromPath" to "/toPath" - redirect.get("/mirror", "/practice-modes/mirror"); - - List<String> verbEndpoints = new ArrayList<>(); - verbEndpoints.add(endpoint); - verbEndpoints.add(endpoint + "/*"); - verbEndpoints.add(rawEndPoint); - verbEndpoints.add(rawEndPoint + "/*"); - - AdhocDocumentedHttpRouteConfigurer routeCreatorAndDocumentor = - new AdhocDocumentedHttpRouteConfigurer(apiDefn); - - for (String anEndpoint : verbEndpoints) { - HttpRouteHandler routeHandler = - (request, result) -> { - result.status(204); - result.header( - "Allow", "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE"); - return ""; - }; - if (anEndpoint.endsWith("/*")) { - // add to routing but not to the api documentation - SimpleHttpRouteCreator.addHandler(anEndpoint, "options", routeHandler); - } else { - routeCreatorAndDocumentor.add( - anEndpoint, - RoutingVerb.OPTIONS, - 204, - "Options for mirror endpoint", - routeHandler); - } - } - - HttpRouteHandler mirroredRoute = - (request, result) -> { - return requestMirror.mirrorRequest(request, result); - }; - - HttpRouteHandler rawTextMirroredRoute = - (request, result) -> { - return requestMirror.mirrorRequestAsText(request, result); - }; - - RoutingVerb[] verbs200status = { - RoutingVerb.GET, RoutingVerb.POST, RoutingVerb.PUT, - RoutingVerb.DELETE, RoutingVerb.PATCH, RoutingVerb.TRACE - }; - - for (String anEndpoint : verbEndpoints) { - for (RoutingVerb routing : verbs200status) { - if (anEndpoint.endsWith("/*")) { - // add to routing but not to the api documentation - SimpleHttpRouteCreator.addHandler(anEndpoint, routing.name(), mirroredRoute); - } else { - if (anEndpoint.startsWith(endpoint)) { - routeCreatorAndDocumentor.add( - anEndpoint, - routing, - 200, - "Mirror a " + routing.name().toUpperCase() + " request", - mirroredRoute); - } - if (anEndpoint.startsWith(rawEndPoint)) { - routeCreatorAndDocumentor.add( - anEndpoint, - routing, - 200, - "Raw Text Mirror of a " + routing.name().toUpperCase() + " request", - rawTextMirroredRoute); - } - } - } - } - - /* - Handle HEAD verb - special handling to only return headers - */ - for (String anEndpoint : verbEndpoints) { - HttpRouteHandler routeHAndler = - (request, result) -> { - requestMirror.mirrorRequest(request, result); - return ""; - }; - if (anEndpoint.endsWith("/*")) { - // add to routing but not to the api documentation - SimpleHttpRouteCreator.addHandler(anEndpoint, "head", routeHAndler); - } else { - routeCreatorAndDocumentor.add( - anEndpoint, - RoutingVerb.HEAD, - 204, - "Headers for mirror endpoint", - routeHAndler); - } - } - - Thingifier dummy = new Thingifier(); - dummy.setDocumentation( - "HTTP Mirror Mode", - "The HTTP Mirror mode shows you the request that you sent in. The raw mode shows the exact request, the request mode shows it 'interpreted' by a basic API."); - // dummy.apiConfig().setValidatesContentType(false); - dummy.apiConfig().setDefaultContentTypeAsJson(false); - dummy.apidocsconfig() - .setHeaderSectionOverride( - """ - <p> - Raw mode will always return the response as plain text. - HEAD and OPTIONS will respond like normal HEAD and OPTIONS. - You can not use the accept header to control the response format. - </p> - <p> - Request mode will try to honour the accept headers and return the response as JSON or XML. - </p> - <p> - Nothing is stored on the server. The request is only used to generate a response which is passed back to you. - </p> - <p> - Validation is performed on the length of the request and if the request is too large then it will be rejectec. - </p> - """); - dummy.apidocsconfig() - .setApiIntroductionParaOverride( - """ - <p> - For any of the endpoints listed, you can also add any number of parameters to the URL e.g. /mirror/raw/this/and/that?key=value - </p> - """); - - ThingifierApiDocumentationDefn apiDocDefn = routeCreatorAndDocumentor.getApiDocDefn(); - apiDocDefn.setThingifier(dummy); - apiDocDefn.setPathPrefix("/mirror"); // where can the API endpoints be found - apiDocDefn.setSeoTitle("Mirror Mode API Documentation | API Challenges"); - apiDocDefn.setSeoDescription( - "Review Mirror Mode endpoint documentation to inspect reflected HTTP requests, headers, and payload behavior for debugging and learning."); - apiDocDefn.setMetaRobots("noindex,follow"); - apiDocDefn.setOgType("website"); - apiDocDefn.setTwitterCard("summary_large_image"); - - new ThingifierAutoDocGenRouting(apiDocDefn.getThingifier(), apiDocDefn, guiTemplates); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/RequestMirror.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/RequestMirror.java deleted file mode 100644 index 5ba81d57..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/RequestMirror.java +++ /dev/null @@ -1,75 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.mirror; - -import uk.co.compendiumdev.challenge.httpserver.HttpMessageLengthValidator; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpServerRequest; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpServerResponse; -import uk.co.compendiumdev.thingifier.adapter.httpserver.routehandlers.HttpApiRequestResponseHandler; -import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.AcceptHeaderParser; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; - -public class RequestMirror { - - EntityDefinition entityDefn; - - // new RequestMirror().mirrorRequest(request, result); - public String mirrorRequest(final HttpServerRequest request, final HttpServerResponse result) { - - final Thingifier mirrorThingifier = new Thingifier(); - - entityDefn = mirrorThingifier.defineThing("messageDetails", "messagesDetails"); - - entityDefn.addFields(Field.is("details", FieldType.STRING)); - - // reject large requests - HttpMessageLengthValidator lengthValidator = new HttpMessageLengthValidator(); - - if (lengthValidator.rejectRequestTooLong(request, result)) { - return lengthValidator.messageTooLongErrorResponse( - mirrorThingifier.apiConfig(), request, result); - } - - String returnValue = - new HttpApiRequestResponseHandler(request, result, mirrorThingifier) - .usingHandler(new MirrorHttpApiRequestHandler(this.entityDefn)) - .validateRequestSyntax(false) - .handle(); - - final AcceptHeaderParser parser = new AcceptHeaderParser(request.header("accept")); - - // handle text separately as the main api does not 'do' text - if (parser.hasAskedForTEXT()) { - result.header("Content-Type", "text/plain"); - } - - return returnValue; - } - - public String mirrorRequestAsText( - final HttpServerRequest request, final HttpServerResponse result) { - - // The raw unfiltered request as text - - final Thingifier mirrorThingifier = new Thingifier(); - - // reject large requests - HttpMessageLengthValidator lengthValidator = new HttpMessageLengthValidator(); - - if (lengthValidator.rejectRequestTooLong(request, result)) { - return lengthValidator.messageTooLongErrorResponse( - mirrorThingifier.apiConfig(), request, result); - } - - String returnValue = - new HttpApiRequestResponseHandler(request, result, mirrorThingifier) - .usingHandler(new MirrorHttpApiTextRequestHandler()) - .validateRequestSyntax(false) - .handle(); - - result.header("Content-Type", "text/plain"); - - return returnValue; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/AddMoreItemsIfNecessary.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/AddMoreItemsIfNecessary.java deleted file mode 100644 index d3cacd29..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/AddMoreItemsIfNecessary.java +++ /dev/null @@ -1,29 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.simpleapi; - -import uk.co.compendiumdev.thingifier.adapter.http.messagehooks.HttpApiRequestHook; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -public class AddMoreItemsIfNecessary implements HttpApiRequestHook { - - private final EntityRelModel erModel; - - public AddMoreItemsIfNecessary(EntityRelModel eRmodel) { - this.erModel = eRmodel; - } - - @Override - public HttpApiResponse run(HttpApiRequest request, ThingifierApiConfig config) { - - EntityDefinition item = erModel.getSchema().getEntityDefinitionNamed("item"); - ThingStore store = erModel.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - if (store != null && item != null && store.entityQueries().count(item) < 5) { - erModel.populateDatabase(EntityRelModel.DEFAULT_DATABASE_NAME); - } - return null; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/RandomIsbnGenerator.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/RandomIsbnGenerator.java deleted file mode 100644 index 706f1e8f..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/RandomIsbnGenerator.java +++ /dev/null @@ -1,21 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.simpleapi; - -import java.util.Random; - -public class RandomIsbnGenerator { - - public static String generate() { - return generate(new Random()); - } - - public static String generate(Random random) { - - String isbn13 = "xxx-x-xx-xxxxxx-x"; - - while (isbn13.contains("x")) { - isbn13 = isbn13.replaceFirst("x", String.valueOf(random.nextInt(9))); - } - - return isbn13; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/ResetAutoIncrementWhenTooHigh.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/ResetAutoIncrementWhenTooHigh.java deleted file mode 100644 index a7555d6c..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/ResetAutoIncrementWhenTooHigh.java +++ /dev/null @@ -1,34 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.simpleapi; - -import uk.co.compendiumdev.thingifier.adapter.http.messagehooks.HttpApiRequestHook; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -public class ResetAutoIncrementWhenTooHigh implements HttpApiRequestHook { - - private final EntityRelModel erModel; - - public ResetAutoIncrementWhenTooHigh(EntityRelModel eRmodel) { - this.erModel = eRmodel; - } - - @Override - public HttpApiResponse run(HttpApiRequest request, ThingifierApiConfig config) { - - EntityDefinition item = erModel.getSchema().getEntityDefinitionNamed("item"); - ThingStore store = erModel.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - if (store == null || item == null) { - return null; - } - - store.administration().resetAutoIncrementCounterWhenNextValueAbove(item, "id", 99999); - if (store.entityQueries().count(item) < 5) { - erModel.populateDatabase(EntityRelModel.DEFAULT_DATABASE_NAME); - } - return null; - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleAPITestDataPopulator.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleAPITestDataPopulator.java deleted file mode 100644 index 65c1892e..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleAPITestDataPopulator.java +++ /dev/null @@ -1,37 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.simpleapi; - -import java.util.Random; -import uk.co.compendiumdev.thingifier.core.domain.datapopulator.RepositoryDataPopulator; -import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -public class SimpleAPITestDataPopulator implements RepositoryDataPopulator { - - @Override - public void populate(final ERSchema schema, final ThingStore store) { - String[] types = {"book", "book", "dvd", "blu-ray", "cd", "cd", "dvd", "blu-ray"}; - - EntityDefinition item = schema.getEntityDefinitionNamed("item"); - - Random random = new Random(); - for (String type : types) { - store.entities() - .create( - EntityInstanceDraft.forEntity(item) - .withField("type", type) - .withField("numberinstock", String.valueOf(random.nextInt(20))) - .withField("isbn13", randomIsbn(random)) - .withField( - "price", - String.valueOf(random.nextInt(99)) - + "." - + random.nextInt(99))); - } - } - - private String randomIsbn(Random random) { - return RandomIsbnGenerator.generate(random); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiCorsHeadersResponseHook.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiCorsHeadersResponseHook.java deleted file mode 100644 index 96696bdc..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiCorsHeadersResponseHook.java +++ /dev/null @@ -1,38 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.simpleapi; - -import static uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpMethod.OPTIONS; - -import java.util.List; -import uk.co.compendiumdev.thingifier.adapter.httpserver.messagehooks.InternalHttpResponseHook; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpResponse; - -public class SimpleApiCorsHeadersResponseHook implements InternalHttpResponseHook { - - @Override - public void run(final InternalHttpRequest request, final InternalHttpResponse response) { - - // TODO: hooks should only apply to a specific routing set and this should not be necessary - List<String> validEndpointPrefixesToRunAgainst = List.of("simpleapi"); - String[] pathSegments = request.getPath().split("/"); - if (!validEndpointPrefixesToRunAgainst.contains(pathSegments[0])) { - return; - } - - // allow cross origin requests - // and swagger - // https://support.smartbear.com/swaggerhub/docs/en/edit-apis/cors-requirements-for--try-it-out-.html - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Headers", "*"); - response.setHeader("Access-Control-Allow-Credentials", "true"); - response.setHeader("Access-Control-Allow-Methods", "*"); - // this is necessary for swagger UI to show headers in the UI - response.setHeader("Access-Control-Expose-Headers", "*"); - if (request.getVerb() == OPTIONS - && request.getHeaders().headerExists("Access-Control-Allow-Methods")) { - response.setHeader( - "Access-Control-Allow-Methods", - request.getHeader("Access-Control-Allow-Methods")); - } - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java deleted file mode 100644 index e7e16269..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java +++ /dev/null @@ -1,157 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.simpleapi; - -import static uk.co.compendiumdev.thingifier.adapter.httpserver.ServerRoutes.*; - -import java.util.List; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.httpserver.SimpleHttpRouteCreator; -import uk.co.compendiumdev.thingifier.adapter.httpserver.ThingifierAutoDocGenRouting; -import uk.co.compendiumdev.thingifier.adapter.httpserver.ThingifierHttpApiRoutings; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingDefinition; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingStatus; -import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; -import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.definitions.validation.MatchesRegexValidationRule; -import uk.co.compendiumdev.thingifier.core.domain.definitions.validation.MaximumLengthValidationRule; -import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; -import uk.co.compendiumdev.thingifier.htmlgui.routing.DefaultGuiRoutings; - -/* - The simple API is a no-auth API where anyone can amend, create, delete items. - - To make this Safe all the fields will be primitives and no Strings. - -*/ -public class SimpleApiRoutes { - - private final DefaultGUIHTML guiTemplates; - public Thingifier simplethings; - public EntityDefinition entityDefn; - - public SimpleApiRoutes(DefaultGUIHTML guiTemplates) { - // fake the data storage - this.simplethings = new Thingifier(); - - simplethings.setDocumentation( - "Simple API Mode", - "A simple API, no auth protection so you can add and delete what you want in a multi-user mode."); - this.entityDefn = simplethings.defineThing("item", "items", 100); - - // TODO: add descriptions on a field level to explain what they are and show this in - // documentation - this.entityDefn.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - this.entityDefn.addFields( - Field.is("type", FieldType.ENUM) - .makeMandatory() - .withExample("book") - .withExample("blu-ray") - .withExample("cd") - .withExample("dvd"), - Field.is("isbn13", FieldType.STRING) - .makeMandatory() - .withValidation( - new MatchesRegexValidationRule( - "[0-9]{3}[-]?[0-9]{1}[-]?[0-9]{2}[-]?[0-9]{6}[-]?[0-9]{1}")) - .withValidation(new MaximumLengthValidationRule(17)) - .setMustBeUnique(true) - .setUniqueAfterTransform((s) -> s.replace("-", "")) - .withExample("123-4-56-789012-3"), - Field.is("price", FieldType.FLOAT) - .makeMandatory() - .withExample("97.99") - .withMinMaxValues(0f, 50000.0f), - Field.is("numberinstock", FieldType.INTEGER) - .withDefaultValue("0") - .withMinMaxValues(0, 100)); - - simplethings.setDataGenerator(new SimpleAPITestDataPopulator()); - - simplethings.apiConfig().setFrom(new ThingifierApiConfig("/simpleapi")); - // do not convert floats to int - simplethings.apiConfig().setApiToEnforceDeclaredTypesInInput(false); - // single items should be single items - simplethings.apiConfig().setReturnSingleGetItemsAsCollection(false); - - // TODO: should probably have a support multiple databases config somewhere - simplethings.getERmodel().populateDatabase(EntityRelModel.DEFAULT_DATABASE_NAME); - - this.guiTemplates = guiTemplates; - } - - public void configure() { - - DefaultGUIHTML gui = guiTemplates; - - new DefaultGuiRoutings(simplethings, gui).configureRoutes("/simpleapi/gui"); - - ThingifierApiDocumentationDefn apiDocDefn = new ThingifierApiDocumentationDefn(); - apiDocDefn.addServer("https://apichallenges.eviltester.com", "cloud hosted version"); - apiDocDefn.addServer("http://localhost:4567", "local execution"); - apiDocDefn.setVersion("1.0.0"); - apiDocDefn.setThingifier(simplethings); - apiDocDefn.setPathPrefix("/simpleapi"); // where can the API endpoints be found - apiDocDefn.setSeoTitle("Simple API Documentation | API Challenges"); - apiDocDefn.setSeoDescription( - "Read Simple API endpoint documentation with schemas, examples, and response expectations for hands-on HTTP and API testing practice."); - apiDocDefn.setMetaRobots("index,follow"); - apiDocDefn.setOgType("website"); - apiDocDefn.setTwitterCard("summary_large_image"); - - new SimpleHttpRouteCreator("/simpleapi/items").status(501, true, List.of("patch", "trace")); - - get( - "/simpleapi/randomisbn", - (request, result) -> { - result.status(200); - result.header("content-type", "text/plain"); - return RandomIsbnGenerator.generate(); - }); - - options( - "/simpleapi/randomisbn", - (request, result) -> { - result.status(200); - result.header("content-type", "text/plain"); - result.header("Allow", "GET, HEAD, OPTIONS"); - return ""; - }); - - head( - "/simpleapi/randomisbn", - (request, result) -> { - result.status(200); - result.header("content-type", "text/plain"); - return ""; - }); - - apiDocDefn.addRouteToDocumentation( - new RoutingDefinition( - RoutingVerb.GET, - "/simpleapi/randomisbn", - RoutingStatus.returnedFromCall(), - null) - .addDocumentation( - "return a random ISBN that can be used for testing purposes with the Simple API.") - .addPossibleStatuses(200)); - - new SimpleHttpRouteCreator("/simpleapi/randomisbn") - .status(405, true, List.of("put", "post", "delete", "patch", "trace")); - - new ThingifierAutoDocGenRouting(simplethings, apiDocDefn, gui); - - ThingifierHttpApiRoutings simpleApiHttpRouting = - new ThingifierHttpApiRoutings(simplethings, apiDocDefn); - - simpleApiHttpRouting.registerHttpApiRequestHook( - new AddMoreItemsIfNecessary(simplethings.getERmodel())); - simpleApiHttpRouting.registerHttpApiRequestHook( - new ResetAutoIncrementWhenTooHigh(simplethings.getERmodel())); - simpleApiHttpRouting.registerInternalHttpResponseHook( - new SimpleApiCorsHeadersResponseHook()); - } -} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutes.java deleted file mode 100644 index 1b58c3c8..00000000 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutes.java +++ /dev/null @@ -1,360 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.simulation; - -import static uk.co.compendiumdev.thingifier.adapter.httpserver.ServerRoutes.*; - -import java.util.List; -import uk.co.compendiumdev.challenge.ChallengerConfig; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.routehandlers.HttpApiRequestHandler; -import uk.co.compendiumdev.thingifier.adapter.httpserver.SimpleHttpRouteCreator; -import uk.co.compendiumdev.thingifier.adapter.httpserver.ThingifierAutoDocGenRouting; -import uk.co.compendiumdev.thingifier.adapter.httpserver.routehandlers.HttpApiRequestResponseHandler; -import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.definitions.validation.MaximumLengthValidationRule; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.query.FilterBy; -import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; -import uk.co.compendiumdev.thingifier.core.repository.EntityInstanceQuery; -import uk.co.compendiumdev.thingifier.core.repository.EntityInstanceRepository; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; -import uk.co.compendiumdev.thingifier.core.repository.ThingStoreProviderConfig; -import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; - -public class SimulationRoutes { - - public Thingifier simulation; - public EntityDefinition entityDefn; - private EntityInstanceRepository entityRepository; - private EntityInstanceQuery entityQuery; - - private ThingifierApiDocumentationDefn apiDocDefn; - private DefaultGUIHTML guiTemplates; - private final ThingStoreProviderConfig simulationRepositoryConfig; - - public SimulationRoutes(DefaultGUIHTML guiTemplates) { - this(guiTemplates, ChallengerConfig.defaultSimulationRepositoryConfig()); - } - - public SimulationRoutes( - final DefaultGUIHTML guiTemplates, - final ThingStoreProviderConfig simulationRepositoryConfig) { - this.guiTemplates = guiTemplates; - this.simulationRepositoryConfig = simulationRepositoryConfig; - } - - public void setUpData() { - setUpRepositoryBackedData(); - setUpDocumentation(); - } - - void setUpRepositoryBackedData() { - // fake the data storage - simulation = - new Thingifier(new EntityRelModel(simulationRepositoryConfig.createProvider())); - - simulation.setDocumentation( - "Simulation Mode", - "A simulated API, each request generates a new set of data but responses are processed by an API handler."); - entityDefn = simulation.defineThing("entity", "entities"); - - entityDefn.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - entityDefn.addFields( - Field.is("name", FieldType.STRING) - .makeMandatory() - .withValidation(new MaximumLengthValidationRule(50)) - .withDefaultValue("unnamed"), - Field.is("description", FieldType.STRING) - .withDefaultValue("") - .withValidation(new MaximumLengthValidationRule(200))); - - ThingStore store = simulation.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - entityRepository = store.entities(); - entityQuery = store.entityQueries(); - - for (int id = 1; id <= 10; id++) { - - createManagedEntityNamed("entity number " + id); - } - - createManagedEntityNamed("bob"); - - ThingifierApiConfig customApiconfig = new ThingifierApiConfig("/sim"); - simulation.apiConfig().setFrom(customApiconfig); - } - - private void setUpDocumentation() { - apiDocDefn = new ThingifierApiDocumentationDefn(); - apiDocDefn.addServer("https://apichallenges.eviltester.com", "cloud hosted version"); - apiDocDefn.addServer("http://localhost:4567", "local execution"); - apiDocDefn.setVersion("1.0.0"); - apiDocDefn.setThingifier(simulation); - apiDocDefn.setPathPrefix("/sim"); // where can the API endpoints be found - apiDocDefn.setSeoTitle("Simulation Mode API Documentation | API Challenges"); - apiDocDefn.setSeoDescription( - "Review Simulation Mode API documentation for deterministic request behavior, payload formats, and response handling used in guided practice."); - apiDocDefn.setMetaRobots("noindex,follow"); - apiDocDefn.setOgType("website"); - apiDocDefn.setTwitterCard("summary_large_image"); - - simulation - .apidocsconfig() - .setHeaderSectionOverride( - """ - <p>A simulated API, where each request is run against a new generated set of data but - responses are processed by an API handler. - </p> - <p> - No data is stored on the server. - </p> - <p> - The simulator is designed to be used by following along with the instructions - otherwise sequential requests will not make sense e.g. if you DELETE an item - and then GET it, then it will be returned because the simulator expects you to - DELETE a specific item. - </p> - """ - .stripIndent()); - - new ThingifierAutoDocGenRouting(simulation, apiDocDefn, guiTemplates); - } - - private EntityInstance createManagedEntityNamed(final String name) { - return entityRepository.create( - EntityInstanceDraft.forEntity(entityDefn).withField("name", name)); - } - - public void configure() { - - setUpData(); - - // /sim should be the GUI - String apiEndpoint = "/sim/entities"; - - // redirect a GET to "/fromPath" to "/toPath" for GUI - redirect.get("/sim", "/practice-modes/simulation"); - - options( - apiEndpoint, - (request, result) -> { - result.status(204); - result.header("Allow", "GET, POST, PUT, HEAD, OPTIONS"); - return ""; - }); - - new SimpleHttpRouteCreator(apiEndpoint).status(501, true, List.of("patch", "trace")); - new SimpleHttpRouteCreator(apiEndpoint).status(405, true, List.of("delete")); - - new SimpleHttpRouteCreator(apiEndpoint + "/*").status(501, true, List.of("patch", "trace")); - - options( - apiEndpoint + "/*", - (request, result) -> { - result.status(204); - result.header("x-robots-tag", "noindex"); - result.header("Allow", "GET, POST, PUT, DELETE, HEAD, OPTIONS"); - return ""; - }); - - HttpApiRequestHandler getEntitiesHandler = - (HttpApiRequest anHttpApiRequest) -> { - QueryFilterParams queryParams = anHttpApiRequest.getFilterableQueryParams(); - // id 11 is the special POST-created entity and is not visible in collection - // GET. - queryParams.add(new FilterBy("id", "!=11")); - - List<EntityInstance> instances = entityQuery.list(entityDefn, queryParams); - return ApiResponse.success() - .returnInstanceCollection(instances) - .resultContainsType(entityDefn); - }; - - get( - apiEndpoint, - (request, result) -> { - return new HttpApiRequestResponseHandler(request, result, simulation) - .usingHandler(getEntitiesHandler) - .handle(); - }); - - head( - apiEndpoint, - (request, result) -> { - new HttpApiRequestResponseHandler(request, result, simulation) - .usingHandler(getEntitiesHandler) - .handle(); - return ""; - }); - - HttpApiRequestHandler getEntityHandler = - (HttpApiRequest anHttpApiRequest) -> { - ApiResponse response; - - // process it because the request validated - String id = anHttpApiRequest.getUrlParam(":id"); - EntityInstance instance = entityQuery.findByPrimaryKey(entityDefn, id); - if (instance == null) { - response = ApiResponse.error404("Could not find Entity with ID " + id); - } else { - response = ApiResponse.success().returnSingleInstance(instance); - } - - if (id.equals("10")) { - // 10 is the entity we amend to name:eris - response = - ApiResponse.success() - .returnSingleDraft( - EntityInstanceDraft.forEntity(entityDefn) - .withProtectedField("id", "10") - .withField("name", "eris")); - } - - if (id.equals("9")) { - // 9 is the entity we delete - response = ApiResponse.error404("Could not find Entity with ID 9"); - } - - return response; - }; - - // get a specific entity - get( - apiEndpoint + "/:id", - (request, result) -> { - return new HttpApiRequestResponseHandler(request, result, simulation) - .usingHandler(getEntityHandler) - .handle(); - }); - - head( - apiEndpoint + "/:id", - (request, result) -> { - new HttpApiRequestResponseHandler(request, result, simulation) - .usingHandler(getEntityHandler) - .handle(); - - return ""; - }); - - // post create new - will create as 11 {"name":"bob"} - post( - apiEndpoint, - (request, result) -> { - return new HttpApiRequestResponseHandler(request, result, simulation) - .usingHandler( - (anHttpApiRequest) -> { - return ApiResponse.created( - entityQuery.findByPrimaryKey(entityDefn, "11"), - simulation.apiConfig()); - }) - .handle(); - }); - - HttpApiRequestHandler putAndPostEntityHandler = - (HttpApiRequest anHttpApiRequest) -> { - // process it because the request validated - ApiResponse response; - String id = anHttpApiRequest.getUrlParam(":id"); - if (id.equals("11")) { - // we can create id 11 - response = - ApiResponse.created( - entityQuery.findByPrimaryKey(entityDefn, "11"), - simulation.apiConfig()); - } else { - if (id.equals("10")) { - // 10 is the entity we amend to name:eris - response = - ApiResponse.success() - .returnSingleDraft( - EntityInstanceDraft.forEntity(entityDefn) - .withProtectedField("id", "10") - .withField("name", "eris")); - } else { - final EntityInstance instance = - entityQuery.findByPrimaryKey(entityDefn, id); - if (instance == null) { - if (anHttpApiRequest.getVerb() == HttpApiRequest.VERB.POST) { - response = - ApiResponse.error404( - "Could not find Entity with ID " + id); - } else { // must be a PUT - response = - ApiResponse.error( - 403, "Not authorised to create that entity"); - } - } else { - response = - ApiResponse.error( - 403, "Not authorised to amend that entity"); - } - } - } - return response; - }; - - // post amend 10 - // post create - 11 - post( - apiEndpoint + "/:id", - (request, result) -> { - return new HttpApiRequestResponseHandler(request, result, simulation) - .usingHandler(putAndPostEntityHandler) - .handle(); - }); - - // put specific id will create (11), - // and can amend with put (10) - put( - apiEndpoint + "/:id", - (request, result) -> { - return new HttpApiRequestResponseHandler(request, result, simulation) - .usingHandler(putAndPostEntityHandler) - .handle(); - }); - - delete( - apiEndpoint + "/:id", - (request, result) -> { - return new HttpApiRequestResponseHandler(request, result, simulation) - .usingHandler( - (anHttpApiRequest) -> { - ApiResponse response; - String id = anHttpApiRequest.getUrlParam(":id"); - if (id.equals("9")) { - // we can delete id 9 - response = new ApiResponse(204); - } else { - final EntityInstance instance = - entityQuery.findByPrimaryKey(entityDefn, id); - if (instance == null) { - response = - ApiResponse.error404( - "Could not find Entity with ID " - + id); - } else { - response = - ApiResponse.error( - 403, - "Not authorised to delete that entity"); - } - } - return response; - }) - .handle(); - }); - } - - public void close() { - if (simulation != null) { - simulation.close(); - } - } -} diff --git a/challenger/src/main/resources/content/apichallenges.md b/challenger/src/main/resources/content/apichallenges.md deleted file mode 100644 index 685db4ff..00000000 --- a/challenger/src/main/resources/content/apichallenges.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: About The API Challenges -seo_title: Learn API Testing with Hands-On Challenges and Tutorials -description: A brief overview of the API challenges and how to use them as a self-guided learning path for API Testing -lastmod: 2026-02-18 -seo_description: Learn API testing by doing with guided challenges, practical tutorials, and realistic exercises that build confidence with real requests and responses. -showads: true ---- - -# API Challenges - -The API Challenges API is used for managing a To-Do list. - -You need to authenticate to create a session in the system. This will create a unique API repository with basic test data. -As you interact with the API there are a variety of challenges to complete e.g. Get all TODOs, Delete a Todo, etc. - -Each challenge is designed to teach you some aspect of how APIs work, and how to test them. - -The API has [documentation available](/docs). - -A set of [Challenges](/gui/challenges) are available to guide you through the exploration and learning of the API. To complete each challenge you will have to explore a different aspect of API testing. - -You can also view the data in the application without using the API. Using the [Entities Explorer](/gui/entities) view. - -## Application - -This application has been deployed to a cloud instance. It is also available to download and run locally. - -When run in a cloud environment: - -- the data clears itself every 10 minutes -- each user has a unique set of data to work with so no interference from other users -- you need to create an `X-CHALLENGER` session to track your progress on the challenges ([more info](/gui/multiuser)) - -In single user mode, you can use the API without needing any extra headers or configuration. - - -## How to Play in Multi-user Mode - -More information on how to play the challenges in multi-user mode i.e. `apichallenges.eviltester.com` can be found [on the multi-user instructions page](/gui/multiuser): - -- [multi-user instructions page](/gui/multiuser) - -## Challenges - -The [Challenges](/gui/challenges) can be completed by issuing HTTP API requests. - -e.g. `GET {{<ORIGIN_URL>}}/todos` would complete the challenge to "GET the list of todos" - -You can also `GET {{<ORIGIN_URL>}}/challenges` to get the list of challenges and their status as an API call. - -## Challenge Solutions - -You can find solutions to all of the challenges [here](/apichallenges/solutions): - -- [Solutions for the API Challenges](/apichallenges/solutions) - -## How to Track Your Progress - -The actual challenges page is available from the site menu: - -- [challenges list](/gui/challenges) - -To track your challenges and use the gamification tracking to view your progress you'll need to: - -- create a challenger by issuing a `POST` request on the `/challenger` endpoint -- the response will include an `X-CHALLENGER` header with a unique GUID -- this GUID is the reference for an in-memory database in the API Challenges -- make requests to the API with the `X-CHALLENGER` header in the request headers -- you can then create and amend data in the database using the API -- and your progress will be tracked in the system memory - -Full details are included in the [multi-user](/gui/multiuser) instructions. And in the information on the [challenges list page](/gui/challenges). - - diff --git a/challenger/src/main/resources/content/apichallenges/openapi.md b/challenger/src/main/resources/content/apichallenges/openapi.md deleted file mode 100644 index dab9d501..00000000 --- a/challenger/src/main/resources/content/apichallenges/openapi.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: API Challenges Open API Swagger File -seo_title: Open API Swagger File for Practicing | API Challenges -description: Download the Open API files for the API Challenges. -lastmod: 2026-02-18 -seo_description: Explore API Challenges Open API with practical guidance and actionable next steps designed to improve API testing skills through hands-on practice. -showads: true ---- - -# API Challenge Open API Files - -Download an OpenAPI/Swagger JSON file to use in your REST Client. - -## File Download Links - -Two OpenAPI/Swagger formatted files are available. - -- [Download Normal OpenAPI File](/docs/swagger) - to use API like a User -- [Download Permissive OpenAPI File](/docs/swagger?permissive) - less validation and more suitable for supporting testing - -## About API Challenge's Normal OpenAPI File - -The Normal OpenAPI File is intended for use as though you were a user. - -It only lists endpoints that are valid to use, and has additional validation on the URL Parameters that you can enter. - -When this type of file is loaded into a Swagger UI Generation application it makes it easy to USE the API but makes it harder to TEST the API. - -- [Download Normal OpenAPI File](/docs/swagger) - -## About API Challenge's Permissive OpenAPI File - -The Permissive OpenAPI File is intended for testing. - -It lists all the end points with more Verbs i.e. even verbs that the API defines as not available. - -The parameters are also possible to send as empty and type validation is not performed on the parameter values. This makes it possible to use Swagger UI applications to test more extreme situations. - -- [Download Permissive OpenAPI File](/docs/swagger?permissive) diff --git a/challenger/src/main/resources/content/apichallenges/solutions.md b/challenger/src/main/resources/content/apichallenges/solutions.md deleted file mode 100644 index db8e9442..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: API Challenges Solution Index -seo_title: Solution Index and Walkthroughs | API Challenges -description: A list of all the solutions for the API Challenges. Try them yourself, but if you get stuck, we have instructions and solution videos. -lastmod: 2026-02-18 -seo_description: Explore API Challenges Solution with practical guidance and actionable next steps designed to improve API testing skills through hands-on practice. -showads: true ---- - -# API Challenge Solutions - -Use this API challenge walkthrough index to find step-by-step REST API challenge solutions for every major topic in the API Challenges app. Each page focuses on a single outcome and shows the exact request method, endpoint, headers, payload constraints, and expected status code. If you are learning API testing, this gives you a practical way to understand why requests pass, why they fail, and how to debug quickly. - -Treat this page as your API testing roadmap. Start with session setup and challenge tracking, then progress through GET, POST, PUT, DELETE, HEAD, and OPTIONS behavior. Continue into content negotiation (`Accept` and `Content-Type`), then move into authentication, authorization, method override scenarios, and status-code edge cases. Following this order builds durable test design skills and helps you avoid random trial-and-error testing. - -These API test walkthroughs are designed for real hands-on execution in tools like Postman, Insomnia, Bruno, or cURL. Send the request yourself first, compare the response to the expected result, then use the solution details to close gaps in your approach. - -If your goal is API automation, each solution also provides clear assertion targets you can translate into maintainable automated checks. - -## Getting Started - -- [POST /challenger (201)](/apichallenges/solutions/create-session/post-challenger-201) - -## First Real Challenge - -- [GET /challenges (200)](/apichallenges/solutions/first-challenge/get-challenges-200) - -## GET Challenges - -- [GET /todos (200)](/apichallenges/solutions/get/get-todos-200) -- [GET /todo (404)](/apichallenges/solutions/get/get-todo-404) -- [GET /todos id (200)](/apichallenges/solutions/get/get-todos-id-200) -- [GET /todos id (404)](/apichallenges/solutions/get/get-todos-id-404) -- [GET /todos ?filter (200)](/apichallenges/solutions/get/get-todos-200-filter) - -## HEAD Challenges - -- [HEAD /todos id (200)](/apichallenges/solutions/head/head-todos-200) - -## Creation Challenges with POST - -- [POST /todos id (201)](/apichallenges/solutions/post-create/post-todos-201) -- [POST /todos (400) doneStatus](/apichallenges/solutions/post-create/post-todos-400) -- [POST /todos (400) title too long](/apichallenges/solutions/post-create/post-todos-400-title-too-long) -- [POST /todos (400) description too long](/apichallenges/solutions/post-create/post-todos-400-description-too-long) -- [POST /todos (201) max out content](/apichallenges/solutions/post-create/post-todos-201-max-content) -- [POST /todos (413) content too long](/apichallenges/solutions/post-create/post-todos-413-content-too-long) -- [POST /todos (400) extra](/apichallenges/solutions/post-create/post-todos-400-extra-field) - -## Creation Challenges with PUT - -- [PUT /todos/{id} (400)](/apichallenges/solutions/put-create/put-todos-400-create) - -## Update Challenges with POST - -- [POST /todos id (200)](/apichallenges/solutions/post-update/post-todos-id-200) -- [POST /todos/{id} (404)](/apichallenges/solutions/post-update/post-todos-id-404) - -## Update Challenges with PUT - -- [PUT /todos/{id} full (200)](/apichallenges/solutions/put-update/put-todos-id-200-update-full) -- [PUT /todos/{id} partial (200)](/apichallenges/solutions/put-update/put-todos-id-200-update-partial) -- [PUT /todos/{id} no title (400)](/apichallenges/solutions/put-update/put-todos-id-400-no-title) -- [PUT /todos/{id} no amend id (400)](/apichallenges/solutions/put-update/put-todos-id-400-no-amend-id) - -## DELETE Challenges - -- [DELETE /todos id (200)](/apichallenges/solutions/delete/delete-todos-id-200) - -## OPTIONS Challenges - -- [OPTIONS /todos (200)](/apichallenges/solutions/options/options-todos-200) - -## Accept Header Challenges - -- [GET /todos (200) XML](/apichallenges/solutions/accept-header/get-todos-200-xml) -- [GET /todos (200) JSON](/apichallenges/solutions/accept-header/get-todos-200-json) -- [GET /todos (200) ANY](/apichallenges/solutions/accept-header/get-todos-200-any) -- [GET /todos (200) XML pref](/apichallenges/solutions/accept-header/get-todos-200-xml-pref) -- [GET /todos (200) no accept](/apichallenges/solutions/accept-header/get-todos-200-no-accept) -- [GET /todos (406)](/apichallenges/solutions/accept-header/get-todos-406) - -## Content-Type Header Challenges - -- [POST /todos XML](/apichallenges/solutions/content-type-header/post-todos-xml) -- [POST /todos JSON](/apichallenges/solutions/content-type-header/post-todos-json) -- [POST /todos (415)](/apichallenges/solutions/content-type-header/post-todos-415) - -## Fancy a Break? Restore your session - -- [Solve the Save and Restore Session Challenges](/apichallenges/solutions/manage-session/save-restore-session) - - GET /challenger/guid (existing X-CHALLENGER) - - PUT /challenger/guid RESTORE - - GET /challenger/database/guid (200) - - PUT /challenger/database/guid (Update) - -## Mix Accept and Content-Type Challenges - -- [POST /todos XML to JSON](/apichallenges/solutions/mix-accept-content/post-xml-accept-json) -- [POST /todos JSON to XML](/apichallenges/solutions/mix-accept-content/post-json-accept-xml) - -## Status Code Challenges - -- [Solve the 405, 500, 501 and 204 Status Code Challenges](/apichallenges/solutions/status-codes/status-codes-405-500-501-204) - - DELETE /heartbeat (405) - - PATCH /heartbeat (500) - - TRACE /heartbeat (501) - - GET /heartbeat (204) - -## HTTP Method Override Challenges - -- [Solve the /heartbeat Method Override Challenges](/apichallenges/solutions/method-override/all-method-overrides) - - POST /heartbeat as DELETE (405) - - POST /heartbeat as PATCH (500) - - POST /heartbeat as Trace (501) - -## Authentication Challenges - -- [POST /secret/token (401)](/apichallenges/solutions/authentication/post-secret-401) -- [POST /secret/token (201)](/apichallenges/solutions/authentication/post-secret-201) - -## Authorization Challenges - -- [GET /secret/note (403)](/apichallenges/solutions/authorization/get-secret-note-403) -- [GET /secret/note (401)](/apichallenges/solutions/authorization/get-secret-note-401) -- [GET /secret/note (200)](/apichallenges/solutions/authorization/get-secret-note-200) -- [POST /secret/note (200)](/apichallenges/solutions/authorization/post-secret-note-200) -- [POST /secret/note (401) && (403)](/apichallenges/solutions/authorization/post-secret-note-401-403) - - POST /secret/note (401) - - POST /secret/note (403) -- [GET && POST /secret/note (Bearer)](/apichallenges/solutions/authorization/get-post-secret-note-bearer) - - GET /secret/note (Bearer) - - POST /secret/note (Bearer) - -## Miscellaneous Challenges - -- [DELETE /todos/{id} (200) all](/apichallenges/solutions/miscellaneous/create-maximum-number-todos) -- [POST /todos (201) all](/apichallenges/solutions/miscellaneous/delete-all-todos) diff --git a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-any.md b/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-any.md deleted file mode 100644 index d1a1ad4d..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-any.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -date: 2021-05-29T09:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - GET todos any 200 -seo_title: Solution: GET todos any 200 Guide | API Challenges -description: How to solve API challenge GET todos any 200 to accept the todos in default format. -seo_description: Use this walkthrough to solve GET todos any 200 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/accept-header/get-todos-200-xml-pref -schema_howto_steps: Create a GET request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send the request and verify the response status is 200 -showads: true ---- - -# GET all the todos in default format - -How to complete the challenge `GET /todos ANY (200)` to successfully GET all the todos in default format. - -## GET /todos ANY (200) - -> Issue a GET request on the `/todos` end point with an `Accept` header of `*/*` to receive results in Default format - -- `GET` request will receive a response with all the todo items - - e.g. `GET /todos` to get all the todo items -- `200` is a success code, in this case it means the end point exists and the `todo items were returned -- `Accept` means that an `Accept` header was added to specify that the todos should be returned in **ANY** format i.e. the default from the server -- add the `X-CHALLENGER` header to track progress - - -## Basic Instructions - -- Issue a `GET` request to end point "/todos" - - if running locally that endpoint would be - - `{{<ORIGIN_URL>}}/todos` -- The request should have an `Accept` header specifying ANY format by using a value of `*/*`, our application defaults to JSON -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `200` when all the details are valid. -- Check the body of the message has JSON format data, which is the default from the server -- Check the `content-type` header in the response has `application/json` - - -## Example Request - -~~~~~~~~ -> GET /todos HTTP/1.1 -> Host: {{<HOST_URL>}} -> User-Agent: insomnia/2021.2.2 -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sat, 29 May 2021 09:06:15 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```json -{ - "todos": [ - { - "id": 235, - "title": "pay invoices", - "doneStatus": false, - "description": "" - }, - { - "id": 239, - "title": "tidy meeting room", - "doneStatus": false, - "description": "" - } - ] -} -``` - - -## Overview Video - -{{<youtube-embed key="O4DhJ8Ohkk8" title="Solution to Get all Todos in default format">}} - -[Patreon ad free version](https://www.patreon.com/posts/51830126) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-json.md b/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-json.md deleted file mode 100644 index 964ab102..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-json.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -date: 2021-05-09T09:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - GET todos json 200 -seo_title: Solution: GET todos json 200 | API Challenges -description: How to solve API challenge GET todos json 200 to accept the todos in json format. -seo_description: Use this walkthrough to solve GET todos json 200 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/accept-header/get-todos-200-any -schema_howto_steps: Create a GET request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send the request and verify the response status is 200 -showads: true ---- - -# GET all the todos in JSON format - -How to complete the challenge `GET /todos JSON (200)` to successfully GET all the todos in JSON format. - -## GET /todos JSON (200) - -> Issue a GET request on the `/todos` end point with an `Accept` header of `application/json` to receive results in JSON format - -- `GET` request will receive a response with all the todo items - - e.g. `GET /todos` to get all the todo items -- `200` is a success code, in this case it means the end point exists and the `todo items were returned -- `Accept` means that an `Accept` header was added to specify that the todos should be returned in **JSON** format -- add the `X-CHALLENGER` header to track progress - - -## Basic Instructions - -- Issue a `GET` request to end point "/todos" - - if running locally that endpoint would be - - `{{<ORIGIN_URL>}}/todos` -- The request should have an `Accept` header specifying JSON format by using a value of `application/json` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `200` when all the details are valid. -- Check the body of the message has JSON format data -- Check the `content-type` header in the response has `application/json` - - -## Example Request - -~~~~~~~~ -> GET /todos HTTP/1.1 -> Host: {{<HOST_URL>}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: application/json -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sun, 09 May 2021 11:07:48 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```json -{ - "todos": [ - { - "id": 16, - "title": "process payroll", - "doneStatus": false, - "description": "" - }, - { - "id": 15, - "title": "pay invoices", - "doneStatus": false, - "description": "" - } - ] -} -``` - -## Overview Video - -{{<youtube-embed key="79JTHiby2Qw" title="Solution to GET todos in JSON format">}} - -[Patreon ad free version](https://www.patreon.com/posts/51045284) - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-no-accept.md b/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-no-accept.md deleted file mode 100644 index 8fde8eef..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-no-accept.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -date: 2021-05-29T10:32:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - GET todos No Accept Header 200 -seo_title: Solution GET todos No Accept Header 200 | API Challenges -description: How to solve API challenge GET todos No Accept 200 to GET the todos with no accept header present. -seo_description: Use this walkthrough to solve GET todos No Accept Header with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/accept-header/get-todos-406 -schema_howto_steps: Create a GET request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `GET /todos No Accept (200)` - -Accept headers are optional. Most API clients will add one by default. But we do not need to pass in an `Acdept` header to successfully GET all the todos in JSON format. This challenge allows us to test this, to complete it we must ensure that we do not pass in an accept header. - -## GET /todos No Accept (200) - -When we issue a request with no accept header, we should receive the default from the server. But... sending a request without an Accept header might be harder depending on the tool we use. - -> Issue a GET request on the `/todos` end point with no `Accept` header to receive results in JSON format. - -- `GET` request will receive a response with all the todo items - - e.g. `GET /todos` to get all the todo items -- `200` is a success code, in this case it means the end point exists and the `todo items were returned -- `No Accept` means that the request should not include an `Accept` header -- add the `X-CHALLENGER` header to track progress - - -## Basic Instructions - -- Issue a `GET` request to end point "/todos" - - if running locally that endpoint would be - - `{{<ORIGIN_URL>}}/todos` -- The request should not have an `Accept` header at all -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `200` when all the details are valid. -- Check the body of the message has JSON format data, which is the default from the server -- Check the `content-type` header in the response has `application/json` - -Sometimes it is useful to send our requests through a proxy or look at the debug output from our tool to make sure that we are sending the requests we expect to send. - -A Proxy can also be used to amend the request and remove the headers. - -cURL may provide more flexibility for doing this than other tools. - -In Insomnia, right click on the [Send] button and Generate Client Code for cURL. Then amend the code to remove the header by adding `--header 'Accept:'` - - -## cURL Details - -~~~~~~~~ -curl --request GET \ - --url {{<ORIGIN_URL>}}/todos \ - --header 'X-CHALLENGER: x-challenger-guid' - --header 'Accept:' - -v -~~~~~~~~ - -Hints: - -- add `--header 'Accept:'` to the generated code to remove Accept header from the request. -- add `-v` to the generated code if you want to see the full response output. - - -## Example Request - -~~~~~~~~ -> GET /todos HTTP/1.1 -> Host: {{<HOST_URL>}} -> User-Agent: curl/7.64.1 -> X-CHALLENGER: x-challenger-guid -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sat, 29 May 2021 10:35:04 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```json -{"todos":[{"id":280,"title":"install webcam","doneStatus":false,"description":""}]} -``` - - -## Overview Video - -{{<youtube-embed key="CSVP2PcvOdg" title="Solution to Get all Todos in defaulted format">}} - -[Patreon ad free version](https://www.patreon.com/posts/51831718) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-xml-pref.md b/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-xml-pref.md deleted file mode 100644 index ed65bca4..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-xml-pref.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -date: 2021-05-29T10:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - GET todos XML as Preference 200 -seo_title: Solution: GET todos XML Preference 200 | API Challenges -description: How to solve API challenge GET todos XML 200 to accept the todos in XML format as preferred format. -seo_description: Use this walkthrough to solve GET todos XML as Preference with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/accept-header/get-todos-200-no-accept -schema_howto_steps: Create a GET request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send the request and verify the response status is 200 -showads: true ---- - -# Use Accept Headers to GET XML Content as a preference - -How to complete the challenge `GET /todos XML (200)` to successfully GET all the todos in XML format as first preference. - -## GET /todos XML Preference (200) - -It is possible to ask for multiple types in the Accept header, expressing a preference for the returned format. If none of the types are available then expect a 406 response. - -> Issue a GET request on the `/todos` end point with an `Accept` header of `application/xml` followed by `application/json` to receive results in XML format if supported, or JSON if not. - -- `GET` request will receive a response with all the todo items - - e.g. `GET /todos` to get all the todo items -- `200` is a success code, in this case it means the end point exists and the `todo items were returned -- `Accept` means that an `Accept` header was added to specify that the todos should be returned in **XML** format as first preference, followed by **JSON** as second preference -- add the `X-CHALLENGER` header to track progress - - -## Basic Instructions - -- Issue a `GET` request to end point "/todos" - - if running locally that endpoint would be - - `{{<ORIGIN_URL>}}/todos` -- The request should have an `Accept` header specifying XML format by using a value of `application/xml,application/json` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `200` when all the details are valid. -- Check the body of the message has JSON format data, which is the default from the server -- Check the `content-type` header in the response has `application/json` - -The chained Accept header `application/xml,application/json` asks for XML as first preference, but if not supported then supply JSON. - - -## Example Request - -~~~~~~~~ -> GET /todos HTTP/1.1 -> Host: {{<HOST_URL>}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: application/xml,application/json -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sat, 29 May 2021 10:05:24 GMT -< Content-Type: application/xml -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```xml -<todos> - <todo> - <doneStatus>false</doneStatus> - <description/> - <id>267</id> - <title>train staff - - - false - - 268 - schedule meeting - - -``` - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/51831256) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-xml.md b/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-xml.md deleted file mode 100644 index 13a72576..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-200-xml.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -date: 2021-04-23T09:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - GET todos xml 200 -seo_title: Solution: GET todos xml 200 Guide | API Challenges -description: How to solve API challenge GET todos xml 200 to accept the todos in xml format. -seo_description: Use this walkthrough to solve GET todos xml 200 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/accept-header/get-todos-200-json -schema_howto_steps: Create a GET request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `GET /todos XML (200)` - -When we issue a GET request we can use the `Accept` header to request a specific format of result from the API. In this case we will ask for XML to successfully GET all the todos in XML format. - -## GET /todos XML (200) - -> Issue a GET request on the `/todos` end point with an `Accept` header of `application/xml` to receive results in XML format - -- `GET` request will receive a response with all the todo items - - e.g. `GET /todos` to get all the todo items -- `200` is a success code, in this case it means the end point exists and the `todo items were returned -- `Accept` means that an `Accept` header was added to specify that the todos should be returned in XML format -- add the `X-CHALLENGER` header to track progress - - -## Basic Instructions - -- Issue a `GET` request to end point "/todos" - - if running locally that endpoint would be - - `{{}}/todos` -- The request should have an `Accept` header specifying XML format by using a value of `application/xml` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `200` when all the details are valid. -- Check the body of the message has XML format data -- Check the `content-type` header in the response has `application/xml` - - -## Example Request - -~~~~~~~~ -> GET /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: application/xml -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Thu, 22 Apr 2021 16:49:31 GMT -< Content-Type: application/xml -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```xml - - - false - - 273 - scan paperwork - - - false - - 277 - pay invoices - - -``` - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/50348257) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-406.md b/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-406.md deleted file mode 100644 index 7941dd60..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/accept-header/get-todos-406.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -date: 2021-07-17T10:32:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - GET todos Invalid Accept Header 406 -seo_title: Solution: GET todos Invalid Accept Head | API Challenges -description: How to solve API challenge GET todos Invalid Accept 406 to GET the todos with an unsupported accept header present which generates a 406 error response. -seo_description: Use this walkthrough to solve GET todos Invalid Accept with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/content-type-header/post-todos-xml -schema_howto_steps: Create a GET request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send the request and verify the response status is 406 -showads: true ---- - -# How to complete the challenge `GET /todos (406)` - -Some APIs will report an error when asked for a return format that they do not support. Other APIs will respond with a default. The API Challenges will respond with a `406` status code. We can test for this by sending a GET request for all todos but pass in an 'accept' format that the system does not support. - -## GET /todos (406) - -When we issue a request with an accept header, we are asking for a specific content format in the response. But... if we ask for a format that is not supported then the system may reespond with a 406 'NOT ACCEPTABLE' status code. - -> Issue a GET request on the `/todos` end point with an `Accept` header of `application/gzip` to receive a response with a `406` 'NOT ACCEPTABLE' status code. - -- `GET` request asks for a response with all the todo items - - e.g. `GET /todos` to get all the todo items -- `406` is a success code, in this case it means the accept header is not supported by the system -- add the `X-CHALLENGER` header to track progress - - -## Basic Instructions - -- Issue a `GET` request to end point "/todos" - - if running locally that endpoint would be - - `{{}}/todos` -- The request should have an `Accept` header with the value `application/gzip` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `406` when all the details are valid. -- Check the body of the message has JSON formatted error message response -- Check the `content-type` header in the response has `application/json` matching the response body - -Some systems may simply ignore the 'accept' header and return the response in the default format. - - -## Example Request - -~~~~~~~~ -> GET /todos HTTP/1.1 -> Host: {{}} -> User-Agent: insomnia/2021.2.2 -> Accept: application/gzip -> X-CHALLENGER: x-challenger-guid -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 406 Not Acceptable -< Connection: close -< Date: Sat, 17 Jul 2021 12:21:37 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```json -{ - "errorMessages": [ - "Unrecognised Accept Type" - ] -} -``` - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/53793842) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/authentication/post-secret-201.md b/challenger/src/main/resources/content/apichallenges/solutions/authentication/post-secret-201.md deleted file mode 100644 index 5ec9141c..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/authentication/post-secret-201.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -date: 2021-07-24T08:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - authentication passed 201 -seo_title: Solution: authentication passed 201 | API Challenges -description: How to solve API challenge 30 - authenticate with username and password for basic auth. -seo_description: Use this walkthrough to solve authentication passed 201 with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/authorization/get-secret-note-403 -schema_howto_steps: Create a POST request to /secret/token||Add Basic Authorization with the username and password required by the challenge||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request with no body and verify the response status is 201||Capture the returned X-AUTH-TOKEN for later authorization challenges -showads: true ---- - -# How to complete the basic auth authentication challenge - -One way of authenticating a user is through Basic Auth which requires a username and password in the Auth header. - -## Authentication Challenge - -Most of the challenges simply require the correct payload, and an X-Challenger header to track the session. The authentication challenges require an extra header, the value for which can only be obtained with a username and password. - -## Challenge 30 Authentication Passed - -> Issue a POST request on the `/secret/token` end point and receive 201 when Basic auth username/password is admin/password - -- `POST` request means use the HTTP Verb POST - - e.g. `POST /secret/token` sends to the secret token endpoint -- `Basic auth` means include the [Basic Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) -- `username/password is admin/password` the authorisation header value is base 64 encoded, and the details should match `admin` as the username, and `password` for the password -- add the `X-CHALLENGER` header to track progress and because the authentication code we need is asociated with the `X-challenger` session -- Receive a 201 response because the session token has been created to allow authorization to access the secret notes - - -## Basic Instructions - -- Create a new request for the `/secret/token` end point - - if running locally that endpoint would be - - `{{}}/secret/token` -- The verb should be a `POST` -- Add a Basic Auth header by selecting "Basic" from the "Auth" tab and entering a username and password of admin/password i.e. use username "admin", password "password" -- There should be no payload in the message -- You should receive a 201 response - meaning the token has been created -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response should have an `X-AUTH-TOKEN` header which you will include in the messages for challenges 33, 34, 37 and 38 - -## Example Request - -~~~~~~~~ -> POST /secret/token HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Authorization: Basic YWRtaW46cGFzc3dvcmQ= -> Accept: */* -> Content-Length: 0 -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 201 Created -< Connection: close -< Date: Sat, 24 Jul 2021 12:06:09 GMT -< X-Auth-Token: d432f0a3-a81b-4fc8-8e89-24848cc27f34 -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Content-Type: text/html;charset=utf-8 -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## Basic Auth uses Base64 Encoding - -The `Authorization` header does not send the username and password in plain text, it uses Base64 to obscure the details. - -You could see that "admin:password" converts to the Base64 string `YWRtaW46cGFzc3dvcmQ=` by using a Base64 decoder/encoder like https://www.base64decode.org/ - -Or you could decode it in the browser dev console by typing: - -```javascript -atob('YWRtaW46cGFzc3dvcmQ=') -``` - -The command to encode a string as base64 is `btoa` - -## Extras - -- try creating a base64 Authorization header by hand, without using the "Auth" tab in Insomnia - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/54058810) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/authentication/post-secret-401.md b/challenger/src/main/resources/content/apichallenges/solutions/authentication/post-secret-401.md deleted file mode 100644 index 85344c2d..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/authentication/post-secret-401.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -date: 2021-07-24T08:15:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - authentication failed 401 -seo_title: Solution: authentication failed 401 | API Challenges -description: How to solve API challenge 29 - authentication failed with username and password. -seo_description: Use this walkthrough to solve authentication failed 401 with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/authentication/post-secret-201 -schema_howto_steps: Create a POST request to /secret/token||Add Basic Authorization with credentials that should fail for this challenge||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request with no body and verify the response status is 401 -showads: true ---- - -# How to complete the authentication failed challenge - -How to complete the authentication failed with username and password challenge by adding a Basic Auth header with -the wrong details. In response the API returns a status code of 401. - -## Authentication Challenge - -Most of the challenges simply require the correct payload, and an X-Challenger header to track the session. The authentication challenges require an extra header, the value for which can only be obtained with a username and password. - -## Challenge 29 Authentication Failed - -> Issue a POST request on the `/secret/token` end point and receive 401 when Basic auth username/password is not admin/password - -- `POST` request means use the HTTP Verb POST - - e.g. `POST /secret/token` sends to the secret token endpoint -- `Basic auth` means include the [Basic Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) -- `username/password is not admin/password` the authorisation header value is base 64 encoded, and the details should not match `admin` as the username, and `password` for the password -- add the `X-CHALLENGER` header to track progress and because the authentication code we need is asociated with the `X-challenger` session -- Receive a 401 response - - -## Basic Instructions - -- Create a new request for the `/secret/token` end point - - if running locally that endpoint would be - - `{{}}/secret/token` -- The verb should be a `POST` -- Add a Basic Auth header by selecting "Basic" from the "Auth" tab and entering a username and password but make sure it is not admin/password e.g. use username "Admin1", password "Pa55word" (or anything else you want) -- There should be no payload in the message -- You should receive a 401 response - meaning "Unauthorized" because you entered the wrong username or password -- The request should have an `X-CHALLENGER` header to track challenge completion - -## Example Request - -~~~~~~~~ -> POST /secret/token HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Authorization: Basic YWRtaW46cGFzc3dvcmRk -> Accept: */* -> Content-Length: 0 -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 401 Unauthorized -< Connection: close -< Date: Sat, 24 Jul 2021 11:13:04 GMT -< Www-Authenticate: Basic realm="User Visible Realm" -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Content-Type: text/html;charset=utf-8 -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -The `Authorization` header does not send the username and password in plain text, it uses Base64 to obscure the details. - -You could see what username and password I used by typing the Base64 string `YWRtaW46cGFzc3dvcmRk` into a Base64 decoder like https://www.base64decode.org/ - -Or you could decode it in the browser dev console by typing: - -```javascript -atob('YWRtaW46cGFzc3dvcmRk') -``` - -The command to encode a string as base64 is `btoa` - -Although we add an "Authorization" header, really we are trying to "authenticate" with a set of user details. - -- Authorization is "do you have the right permissions" -- Authentication is "are you who you say you are" - -## Additional Exercises - -- try creating a base64 Authorization header by hand, without using the "Auth" tab in Insomnia - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/54057993) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-post-secret-note-bearer.md b/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-post-secret-note-bearer.md deleted file mode 100644 index f75c664a..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-post-secret-note-bearer.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -date: 2021-07-25T09:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - Use Bearer Tokens -seo_title: Solution: Use Bearer Tokens Guide | API Challenges -description: How to solve API challenge - Use bearer token as authorization mechanism -seo_description: Use this walkthrough to solve Use Bearer Tokens with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/miscellaneous/create-maximum-number-todos -schema_howto_steps: Create a GET or POST request to /secret/note||Add an Authorization Bearer token for the protected endpoint request||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the Bearer Token challenges - -Another authentication mechanism is the Bearer Token. Each API Challenger has a secret note bearer token that can be used to authenticate and GET or POST the secret note. - -## Bearer Token Secret Note Challenges - -Most of the challenges simply require the correct payload, and an X-Challenger header to track the session. The authentication challenges require an extra header, the value for which can only be obtained with a username and password. This value is obtained when completing [challenge post secret 201](/apichallenges/solutions/authentication/post-secret-201). - -The `X-CHALLENGER` header authenticates you to access a specific set of secret notes, and the `X-AUTH-TOKEN` authorizes you to gain access. - -- Authentication is "are you who you say you are" (`X-CHALLENGER`) -- Authorization is "do you have the right permissions" (`X-AUTH-TOKEN`) - -Both Challenges 37 and 38 use the Bearer authentication mechanism and are so similar that we have covered them in one post. - -Rather than use the `X-AUTH-TOKEN` header, we use the value returned in Challenge 30 for the `X-AUTH-TOKEN` but we add it as `Bearer` token authentication. - -In Insomnia, use the "Auth" tab and select "Bearer" authentication. Then the toke value is the value of the `X-AUTH-TOKEN` from Challenge 30. - -## Challenge 37 GET /secret/note (Bearer) - -> Issue a GET request on the `/secret/note` end point receive 200 when using the X-AUTH-TOKEN value as an Authorization Bearer token - response body should contain the note - -This challenge is almost a duplicate of the request used in challenge 33 - -- Create a new request for the `/secret/note` end point - - if running locally that endpoint would be - - `{{}}/secret/note` -- The verb should be a `GET` -- Ensure there is no custom header with the name `X-AUTH-TOKEN` -- Ensure there is an Auth Bearer header and the value is the same as received in the `/secret/token` response `X-AUTH-TOKEN` -- The request should have an `X-CHALLENGER` header to track challenge completion -- You should receive a 200 response and the body of the response will contain the secret note - -## Example Request - -``` -> GET /secret/note HTTP/1.1 -> Host: {{}} -> User-Agent: insomnia/2021.2.2 -> X-CHALLENGER: x-challenger-guid -> Authorization: Bearer x-auth-token-value -> Accept: */* -``` - -## Example Response - -``` -< HTTP/1.1 200 OK -< Connection: close -< Date: Sun, 25 Jul 2021 13:20:04 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -``` - -The response body would contain a "note": - -```angular2html -{ - "note": "my note is here" -} -``` - -## Challenge 38 POST /secret/note (Bearer) - -> Issue a POST request on the `/secret/note` end point with a note payload e.g. {"note":"my note"} and receive 200 when valid X-AUTH-TOKEN value used as an Authorization Bearer token. Status code 200 received. Note is maximum length 100 chars and will be truncated when stored. - -This is almost a duplicate of challenge 34. - -- Create a new request for the `/secret/note` end point - - if running locally that endpoint would be - - `{{}}/secret/note` -- The verb should be a `POST` -- Ensure there is no custom header with the name `X-AUTH-TOKEN` -- Add Bearer Token Auth where the token value is the same as received in the `/secret/token` response for the `X-AUTH-TOKEN` -- The request should have an `X-CHALLENGER` header to track challenge completion -- Add a JSON Payload of the format `{"note":"my note"}` -- Include header for `Content-type` value `application/json` -- If the text is too long it will be truncated -- You should receive a 200 response -- The body of the response will contain the secret note - -## Example Request - -``` -> POST /secret/note HTTP/1.1 -> Host: {{}} -> User-Agent: insomnia/2021.2.2 -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Authorization: Bearer x-auth-token-value -> Accept: */* -> Content-Length: 37 - -| { -| "note": "my note edited bearer" -| } -``` - -## Example Response - -``` -< HTTP/1.1 200 OK -< Connection: close -< Date: Sun, 25 Jul 2021 13:24:20 GMT -< X-Challenger: x-challenger-guid -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -``` - -Sample response body: - -```json -{ - "note": "my note edited bearer" -} -``` - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/54091910) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-secret-note-200.md b/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-secret-note-200.md deleted file mode 100644 index 42f194cf..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-secret-note-200.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -date: 2021-07-25T08:45:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - GET authorized secret note 200 -seo_title: Solution: GET authorized secret note 20 | API Challenges -description: How to solve API challenge 33 - authorized to access secret note 200 -seo_description: Use this walkthrough to solve GET authorized secret note with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/authorization/post-secret-note-200 -schema_howto_steps: Create a GET request to /secret/note||Add the X-AUTH-TOKEN header from your authenticated challenger session||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the GET Authorized secret note challenge - -To access the secret note we need to be Authenticated and Authorized, only then can we GET protected information. -The API Challenge returns a status code of 200 and the secret note when we are authorized to do so. - -## Authorization Challenge - -Most of the challenges simply require the correct payload, and an X-Challenger header to track the session. The authentication challenges require an extra header, the value for which can only be obtained with a username and password. This value is obtained when completing [challenge post secret 201](/apichallenges/solutions/authentication/post-secret-201). - -The `X-CHALLENGER` header authenticates you to access a specific set of secret notes, and the `X-AUTH-TOKEN` authorizes you to gain access. - -- Authentication is "are you who you say you are" (`X-CHALLENGER`) -- Authorization is "do you have the right permissions" (`X-AUTH-TOKEN`) - - -## Challenge 33 Authorized - -> Issue a GET request on the `/secret/note` end point receive 200 when valid X-AUTH-TOKEN used - response body should contain the note - -- `GET` request means use the HTTP Verb GET - - e.g. `GET /secret/note` sends to the secret note endpoint -- `valid X-AUTH-TOKEN used` means a custom header named `X-AUTH-TOKEN` should be added to the message with the value received from the `/secret/token` response in Challenge 30 -- add the `X-CHALLENGER` header to track progress -- Receive a 200 response because both `X-CHALLENGER` and `X-AUTH-TOKEN` are for the same user. The Response should contain the content of the secret note. - -## Basic Instructions - -- Create a new request for the `/secret/note` end point - - if running locally that endpoint would be - - `{{}}/secret/note` -- The verb should be a `GET` -- Ensure there is a custom header with the name `X-AUTH-TOKEN` and the value is the same as received in the `/secret/token` response -- The request should have an `X-CHALLENGER` header to track challenge completion -- You should receive a 200 response and the body of the response will contain the secret note - -## Example Request - -~~~~~~~~ -> GET /secret/note HTTP/1.1 -> Host: {{}} -> User-Agent: insomnia/2021.2.2 -> X-CHALLENGER: x-challenger-guid -> X-AUTH-TOKEN: x-auth-token-value -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sun, 25 Jul 2021 11:02:17 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example body of the response: - -```javascript -{ - "note": "my note edited" -} -``` - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/54089625) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-secret-note-401.md b/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-secret-note-401.md deleted file mode 100644 index 1d086296..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-secret-note-401.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -date: 2021-07-25T08:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - unauthorized secret note 401 -seo_title: Solution: unauthorized secret note 401 | API Challenges -description: How to solve API challenge 32 - unauthorized to access secret note 403 -seo_description: Use this walkthrough to solve unauthorized secret note 401 with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/authorization/get-secret-note-200 -schema_howto_steps: Create a GET request to /secret/note||Add an invalid or missing X-AUTH-TOKEN value to trigger unauthorized access||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 401 -showads: true ---- - - -# Hot to solve the Unauthorized challenge - -This post and video shows how to complete the unauthorized secret note challenge, which returns a status code of 401 Unauthorized. - -## Authorization Challenge - -Most of the challenges simply require the correct payload, and an X-Challenger header to track the session. The authentication challenges require an extra header, the value for which can only be obtained with a username and password. This value is obtained when completing [challenge post secret 201](/apichallenges/solutions/authentication/post-secret-201). - -The `X-CHALLENGER` header authenticates you to access a specific set of secret notes, and the `X-AUTH-TOKEN` authorizes you to gain access. - -- Authentication is "are you who you say you are" (`X-CHALLENGER`) -- Authorization is "do you have the right permissions" (`X-AUTH-TOKEN`) - - -## Challenge 32 Unauthorized - -> Issue a GET request on the `/secret/note` end point and receive 401 when no X-AUTH-TOKEN header present - -- `GET` request means use the HTTP Verb GET - - e.g. `GET /secret/note` sends to the secret note endpoint -- `no X-AUTH-TOKEN header present` means no custom header named `X-AUTH-TOKEN` should be added to the message -- add the `X-CHALLENGER` header to track progress -- Receive a 401 UNAUTHORIZED response because no authorization token is present - -## Basic Instructions - -- Create a new request for the `/secret/note` end point - - if running locally that endpoint would be - - `{{}}/secret/note` -- The verb should be a `GET` -- Ensure there is no custom header with the name `X-AUTH-TOKEN` -- The request should have an `X-CHALLENGER` header to track challenge completion -- You should receive a 401 response - meaning you are not authorized - -## Example Request - -~~~~~~~~ -> GET /secret/note HTTP/1.1 -> Host: {{}} -> User-Agent: insomnia/2021.2.2 -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 401 Unauthorized -< Connection: close -< Date: Sun, 25 Jul 2021 10:42:36 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: 1x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/54089275) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-secret-note-403.md b/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-secret-note-403.md deleted file mode 100644 index daf79660..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/authorization/get-secret-note-403.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -date: 2021-07-24T08:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - forbidden secret note 403 -seo_title: Solution: forbidden secret note 403 | API Challenges -description: How to solve API challenge 31 - forbidden to access secret note 403 -seo_description: Use this walkthrough to solve forbidden secret note 403 with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/authorization/get-secret-note-401 -schema_howto_steps: Create a GET request to /secret/note||Add the X-AUTH-TOKEN header from your authenticated challenger session||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 403 -showads: true ---- - -# How to complete the unauthorized secret note challenge - -When we are not authorized to access information we should receive a status code of 403 Forbidden. - -## Authorization Challenge - -Most of the challenges simply require the correct payload, and an X-Challenger header to track the session. The authentication challenges require an extra header, the value for which can only be obtained with a username and password. This value is obtained when completing [challenge post secret 201](/apichallenges/solutions/authentication/post-secret-201). - -The `X-CHALLENGER` header authenticates you to access a specific set of secret notes, and the `X-AUTH-TOKEN` authorizes you to gain access. - -- Authentication is "are you who you say you are" (`X-CHALLENGER`) -- Authorization is "do you have the right permissions" (`X-AUTH-TOKEN`) - - -## Challenge 31 Forbidden - -> Issue a GET request on the `/secret/note` end point and receive 403 when `X-AUTH-TOKEN` does not match a valid token - -- `GET` request means use the HTTP Verb GET - - e.g. `GET /secret/note` sends to the secret note endpoint -- `X-AUTH-TOKEN` means include a header named `X-AUTH-TOKEN` in the message. The `X-` implies it is a non-standard custom header -- `does not match a valid token` means that the value in the header should be different from the value returned from the `secret/token` endpoint -- add the `X-CHALLENGER` header to track progress and because the authentication code we need is asociated with the `X-challenger` session -- Receive a 403 FORBIDDEN response because the authorization token does not match the token required to access the data - -## Basic Instructions - -- Create a new request for the `/secret/note` end point - - if running locally that endpoint would be - - `{{}}/secret/note` -- The verb should be a `GET` -- Add a custom header with the name `X-AUTH-TOKEN` the value should be different from the value returned in Challenge 30 -- The request should have an `X-CHALLENGER` header to track challenge completion -- You should receive a 403 response - meaning you are not authorized - - -## Example Request - -~~~~~~~~ -> GET /secret/note HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> X-AUTH-TOKEN: bob -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 403 Forbidden -< Connection: close -< Date: Sat, 24 Jul 2021 16:18:40 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/54065276) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/authorization/post-secret-note-200.md b/challenger/src/main/resources/content/apichallenges/solutions/authorization/post-secret-note-200.md deleted file mode 100644 index 17c97355..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/authorization/post-secret-note-200.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -date: 2021-07-25T09:00:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST amend secret note 200 -seo_title: Solution: POST amend secret note 200 | API Challenges -description: How to solve API challenge 34 - amend the secret note with a POST request and receive 200 status code -seo_description: Use this walkthrough to solve POST amend secret note 200 with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/authorization/post-secret-note-401-403 -schema_howto_steps: Create a POST request to /secret/note||Add the X-AUTH-TOKEN header from your authenticated challenger session||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the POST secret note challenge - -When we are authenticated and authorized we can amend secured user information. -In the API Challenges we do this by POST request to the user's secrete note endpoint which returns a status code of 200 and amends the secret note. - -## POST Amend Secret Note Challenge - -Most of the challenges simply require the correct payload, and an X-Challenger header to track the session. The authentication challenges require an extra header, the value for which can only be obtained with a username and password. This value is obtained when completing [challenge post secret 201](/apichallenges/solutions/authentication/post-secret-201). - -The `X-CHALLENGER` header authenticates you to access a specific set of secret notes, and the `X-AUTH-TOKEN` authorizes you to gain access. - -- Authentication is "are you who you say you are" (`X-CHALLENGER`) -- Authorization is "do you have the right permissions" (`X-AUTH-TOKEN`) - - -## Challenge 34 POST Amend - -> Issue a POST request on the `/secret/note` end point with a note payload e.g. {"note":"my note"} and receive 200 when valid X-AUTH-TOKEN used. Note is maximum length 100 chars and will be truncated when stored. - -- `POST` request means use the HTTP Verb POST - - e.g. `POST /secret/note` sends to the secret note endpoint -- `with a note payload` include a `JSON` formatted object as the payload -- `valid X-AUTH-TOKEN used` means a custom header named `X-AUTH-TOKEN` should be added to the message with the value received from the `/secret/token` response in Challenge 30 -- add the `X-CHALLENGER` header to track progress and authenticate the request -- Receive a 200 response because both `X-CHALLENGER` and `X-AUTH-TOKEN` are for the same user and the payload was well formatted. - - -## Basic Instructions - -- Create a new request for the `/secret/note` end point - - if running locally that endpoint would be - - `{{}}/secret/note` -- The verb should be a `POST` -- Ensure there is a custom header with the name `X-AUTH-TOKEN` and the value is the same as received in the `/secret/token` response -- The request should have an `X-CHALLENGER` header to track challenge completion -- Add a JSON Payload of the format `{"note":"my note"}` -- Include header for `Content-type` value `application/json` -- If the text is too long it will be truncated -- You should receive a 200 response -- The body of the response will contain the secret note - - -## Example Request - -~~~~~~~~ -> POST /secret/note HTTP/1.1 -> Host: {{}} -> User-Agent: insomnia/2021.2.2 -> X-CHALLENGER: x-challenger-guid -> X-AUTH-TOKEN: x-auth-token-value -> Content-Type: application/json -> Authorization: Basic YWRtaW46cGFzc3dvcmQ= -> Accept: */* -> Content-Length: 31 - -| { -| "note": "my note is here" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sun, 25 Jul 2021 11:47:36 GMT -< X-Challenger: x-challenger-guid -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example body of the response: - -```javascript -{ - "note": "my note is here" -} -``` - -## Extras - -- Try varying the length of the note... does the system truncate as expected? - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/54090441) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/authorization/post-secret-note-401-403.md b/challenger/src/main/resources/content/apichallenges/solutions/authorization/post-secret-note-401-403.md deleted file mode 100644 index 68ab2d92..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/authorization/post-secret-note-401-403.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -date: 2021-07-25T09:15:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST Unauthorised 401 403 -seo_title: Solution: POST Unauthorised 401 403 | API Challenges -description: How to solve API challenge - fail to amend the secret note with a POST request and receive 401 and 403 status codes -seo_description: Use this walkthrough to solve POST Unauthorised 401 403 with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/authorization/get-post-secret-note-bearer -schema_howto_steps: Create a POST request to /secret/note||Add the X-AUTH-TOKEN header from your authenticated challenger session||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 401, 403 -showads: true ---- - -# How to complete the Unauthorised POST secret note challenges - -When unauthorized, the API Challenge API will return status codes of 401 and 403 and fail to amend the secret note. - -## POST Amend Secret Note Challenge - -Most of the challenges simply require the correct payload, and an X-Challenger header to track the session. The authentication challenges require an extra header, the value for which can only be obtained with a username and password. This value is obtained when completing [challenge post secret 201](/apichallenges/solutions/authentication/post-secret-201). - -The `X-CHALLENGER` header authenticates you to access a specific set of secret notes, and the `X-AUTH-TOKEN` authorizes you to gain access. - -- Authentication is "are you who you say you are" (`X-CHALLENGER`) -- Authorization is "do you have the right permissions" (`X-AUTH-TOKEN`) - -Both Challenge 35 and 36 are so similar that we have covered them in one post. - -Following on from challenge 35 were we successfully amended a post. Now we try to repeat the same requests but - -- Challenge POST Amend no AUTH TOKEN - remove the X-AUTH-TOKEN header -- Challenge POST Amend Invalid AUTH TOKEN - the X-AUTH-TOKEN header has the wrong value - -## Challenge - POST Amend no AUTH TOKEN - -> Issue a POST request on the `/secret/note` end point with a note payload {"note":"my note"} and receive 401 when no X-AUTH-TOKEN present - -- `POST` request means use the HTTP Verb POST - - e.g. `POST /secret/note` sends to the secret note endpoint -- `with a note payload` include a `JSON` formatted object as the payload -- `no X-AUTH-TOKEN present` means do not inlude a custom header named `X-AUTH-TOKEN` -- add the `X-CHALLENGER` header to track progress and authenticate the request -- Receive a 401 response because the `X-AUTH-TOKEN` is missing. - -## Example Request - -``` -> POST /secret/note HTTP/1.1 -> Host: {{}} -> User-Agent: insomnia/2021.2.2 -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 23 - -| { -| "note": "my note" -| } -``` - -## Example Response - -``` -< HTTP/1.1 403 Forbidden -< Connection: close -< Date: Sun, 25 Jul 2021 12:53:51 GMT -< X-Challenger: x-challenger-guid -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -``` - - - -## Challenge POST Amend Invalid AUTH TOKEN - -> Issue a POST request on the `/secret/note` end point with a note payload {"note":"my note"} and receive 403 when X-AUTH-TOKEN does not match a valid token - -- same basic message as previous challenge but the `X-AUTH-TOKEN` header is included, but the value does not match the value returned from challenge `/secret/token` request. - -## Example Request - -``` -> POST /secret/note HTTP/1.1 -> Host: {{}} -> User-Agent: insomnia/2021.2.2 -> X-CHALLENGER: x-challenger-guid -> X-AUTH-TOKEN: bob -> Content-Type: application/json -> Accept: */* -> Content-Length: 23 - -| { -| "note": "my note" -| } -``` - -## Example Response - -``` -< Connection: close -< Date: Sun, 25 Jul 2021 12:57:42 GMT -< X-Challenger: x-challenger-guid -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -``` - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/54091910) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/content-type-header/post-todos-415.md b/challenger/src/main/resources/content/apichallenges/solutions/content-type-header/post-todos-415.md deleted file mode 100644 index dca712c3..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/content-type-header/post-todos-415.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -date: 2021-07-17T11:15:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos (415) -seo_title: Solution: POST todos (415) Guide | API Challenges -description: How to solve API challenge POST todos 415. Failing to creating a todo due to unsupported content format. -seo_description: Use this walkthrough to solve POST todos (415) with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/manage-session/save-restore-session -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 415 -showads: true ---- - -# How to complete the challenge `POST /todos (415)` - -Most APIs will report an error when the `content-type` is specified as a format that they do not support. -Some APIs will parse the input using their default processing which can often result in a 500 error. -The API Challenges checks the `content-type` and if it is unsupported returns a 415 status code, -to see this we send a POST request to try and create a todo item but with unsupported content type. - -## POST /todos (415) - -> Issue a POST request on the `/todos` end point with an unsupported content type to generate a 415 status code - -For this challenge we issue a request with a `content-type` which is unsupported. - -- `POST` request means we will send information in the body of the message - - e.g. `POST /todos` sends to the todos endpoint -- `unsupported content type` means that we will set `content-type` to something unknown eg. `bob` -- add the `X-CHALLENGER` header to track progress -- we know it has been rejected when we receive a `415` response -- the body of the response should contain the full details of the error message - - -## Basic Instructions - -- Issue a `POST` request to end point "/todos" - - if running locally that endpoint would be - - `{{}}/todos` -- The request should have an `Content-Type` header with an unsupported value e.g `bob` -- add a valid payload, but it won't match the content type so doesn't matter, but adding a valid payload makes sure that the system is not ignoring the content type specified. -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `415` when all the details are valid. -- Check the body of the response has JSON formatted error response - - the system defaults to json for errors - -Extras: - -- try setting the 'accept' header to specify the format of the error message in the response - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: bob -> Accept: */* -> Content-Length: 106 - -| { -| "title": "create todo process payroll", -| "doneStatus": true, -| "description": "" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 415 Unsupported Media Type -< Connection: close -< Date: Sat, 17 Jul 2021 13:57:54 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: 19b81e3e-841f-41a9-91c1-373f69091a56 -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```json -{ - "errorMessages": [ - "Unsupported Content Type - bob" - ] -} -``` - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/53795763) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/content-type-header/post-todos-json.md b/challenger/src/main/resources/content/apichallenges/solutions/content-type-header/post-todos-json.md deleted file mode 100644 index 3bc222a7..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/content-type-header/post-todos-json.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -date: 2021-07-17T11:00:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos create todo with JSON -seo_title: Solution: POST todos create with JSON | API Challenges -description: How to solve API challenge POST todos JSON. Creating a todo with JSON format. -seo_description: Create a todo with JSON by setting Content-Type and Accept to application/json, posting a valid payload, and confirming challenge completion. -next_challenge: /apichallenges/solutions/content-type-header/post-todos-415 -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send a valid JSON payload that matches the field and content constraints -showads: true ---- - -# How to complete the challenge `POST /todos JSON` - -To fully specify a request and response format we should control the `content-type` and `accept` headers. In this challenge we do this to send a POST request to create a todo item with body and response in JSON format. - -## POST /todos JSON - -To create and amend items in a REST API we usually use a POST request. POST requests are defined as 'partial' requests in that they don't need to have all the fields, but they need to have enough fields to be valid for the request. - -> Issue a POST request on the `/todos` end point to create a todo using Content-Type `application/json`, and Accepting only JSON ie. Accept header of `application/json` - -For this challenge we issue a request with an accept header specifying JSON and we will send the message as JSON so we set the `content-type` header as well. - -- `POST` request means we will send information in the body of the message - - e.g. `POST /todos` sends to the todos endpoint -- `create a todo` means that we will not include a todo `id`, so a new todo is created -- `Content-Type` `application/json` means set the `content-type` header to `application/json` because we are sending a JSON formatted message -- `Accept header` of `application/json` means set the `accept` header to `application/json` so the response is formatted as JSON -- the body of the message will have to be a valid todo item, and we can see the format in the documentation, or by issuing a `GET` request on the `/todos` endpoint -- add the `X-CHALLENGER` header to track progress -- we know it has been created when we receive a `201` response -- the body of the response should contain the full details of the `todo` created in JSON format - - -## Basic Instructions - -- First issue a `GET` request on "/todos" with an `accept` header of `application/json` to see the format of a todo in JSON format - - or read the documentation at [/docs](/docs) - - copy a todo from the response to edit as payload for the `POST` message -- Issue a `POST` request to end point "/todos" - - if running locally that endpoint would be - - `{{}}/todos` -- The request should have an `Accept` header with the value `application/json` because we want the response to be in JSON -- The request should have an `Content-Type` header with the value `application/json` because our payload in the message is in JSON format -- Use the `todo` that you copied from the `GET` request, remembering to remove the `id` because when we create a todo, it will be issued with an `id` automatically - -We only need to use the minimum details, but could add a description if we wanted. - -```json - { - "title": "create todo process payroll", - "doneStatus": true, - "description": "" - } -``` - -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `201` when all the details are valid. -- Check the body of the response has JSON formatted 'todo' response - - this will contain the full details of the todo we created -- Check the `content-type` header in the response has `application/json` matching the requested accept format - - -## Additional Exercises - -- you might want to experiment with removing fields like 'description', what happens if you miss out fields? -- check the `Location` header has the endpoint we can use to retrieve the todo, issue a `GET` on that endpoint to retrieve the details of the todo - - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 106 - -| { -| "title": "create todo process payroll", -| "doneStatus": true, -| "description": "" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 201 Created -< Connection: close -< Date: Sat, 17 Jul 2021 13:34:23 GMT -< Content-Type: application/json -< Location: todos/237 -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```json -{ - "id": 237, - "title": "create todo process payroll", - "doneStatus": true, - "description": "" -} -``` - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/53795265) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/content-type-header/post-todos-xml.md b/challenger/src/main/resources/content/apichallenges/solutions/content-type-header/post-todos-xml.md deleted file mode 100644 index 250d187e..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/content-type-header/post-todos-xml.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -date: 2021-07-17T10:45:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos create todo with XML -seo_title: Solution: POST todos create with XML | API Challenges -description: How to solve API challenge POST todos XML. Creating a todo with XML format. -seo_description: Create a todo with XML by setting Content-Type and Accept to application/xml, sending a valid body, and confirming a successful response. -next_challenge: /apichallenges/solutions/content-type-header/post-todos-json -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send a valid XML payload that matches the field and content constraints -showads: true ---- - -# How to complete the challenge `POST /todos XML` - -The `content-type` should match the data in the request. In this case we will send a POST request to create a todo item with body and response in XML format. - -## POST /todos XML - - -To create and amend items in a REST API we usually use a POST request. POST requests are defined as 'partial' requests in that they don't need to have all the fields, but they need to have enough fields to be valid for the request. - -> Issue a POST request on the `/todos` end point to create a todo using Content-Type `application/xml`, and Accepting only XML ie. Accept header of `application/xml` - -For this challenge we issue a request with an accept header specifying XML and we will send the message as XML so we set the `content-type` header as well. - -- `POST` request means we will send information in the body of the message - - e.g. `POST /todos` sends to the todos endpoint -- `create a todo` means that we will not include a todo id, so a new todo is created -- `Content-Type` `application/xml` means set the `content-type` header to `application/xml` -- `Accept header` of `application/xml` means set the `accept` header to `application/xml` -- the body of the message will have to be a valid todo item, and we can see the format in the documentation, or by issuing a `GET` request on the `/todos` endpoint -- add the `X-CHALLENGER` header to track progress -- we know it has been created when we receive a `201` response -- the body of the response should contain the full details of the `todo` created - - -## Basic Instructions - -- First issue a `GET` request on "/todos" with an `accept` header of `application/xml` to see the format of a todo in XML format - - or read the documentation at [/docs](/docs) - - copy a todo from the response to edit as payload for the `POST` message -- Issue a `POST` request to end point "/todos" - - if running locally that endpoint would be - - `{{}}/todos` -- The request should have an `Accept` header with the value `application/xml` -- The request should have an `Content-Type` header with the value `application/xml` -- Use the `todo` that you copied from the `GET` request, remembering to remove the `id` because when we create a todo, it will be issued with an `id` automatically - -We only need to use the minimum details, but could add a description if we wanted. - -```xml - - true - file paperwork today - -``` - -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `201` when all the details are valid. -- Check the body of the response has XML formatted 'todo' response - - this will contain the full details of the todo we created -- Check the `content-type` header in the response has `application/xml` matching the requested accept format - - -## Additional Exercises - -- you might want to experiment with adding additional fields like 'description', what happens if you miss out fields? -- check the `Location` header has the endpoint we can use to retrieve the todo, issue a `GET` on that endpoint to retrieve the details of the todo - - - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/xml -> Accept: application/xml -> Content-Length: 93 - -| -| true -| file paperwork today -| -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 201 Created -< Connection: close -< Date: Sat, 17 Jul 2021 13:14:28 GMT -< Content-Type: application/xml -< Location: todos/226 -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```xml - - true - - 226 - file paperwork today - -``` - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/53794821) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/create-session/post-challenger-201.md b/challenger/src/main/resources/content/apichallenges/solutions/create-session/post-challenger-201.md deleted file mode 100644 index 1a862096..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/create-session/post-challenger-201.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: API Challenges Solution For - POST Challenger (201) -seo_title: Solution: POST Challenger (201) | API Challenges -description: How to use a POST request to create a Challenger session and start using the API challenges -lastmod: 2026-02-18 -seo_description: Use this walkthrough to solve POST Challenger (201) with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/first-challenge/get-challenges-200 -schema_howto_steps: Create a POST request to /challenger||Send the request with no body to create a new challenger session||Capture the returned X-CHALLENGER value for subsequent challenge requests||Send the request and verify the response status is 201 -showads: true ---- - -# POST Challenger 201 Solution - -How to complete the challenge `POST /challenger 201`. - -## POST /challenger (201) - -> Issue a POST request on the `/challenger` end point, with no body, to create a new challenger session. Use the generated X-CHALLENGER header in future requests to track challenge completion. - -- This challenge is essential if you want to persist your sessions in multi-user mode -- This challenge is optional if you want to work in single-user mode - -## Basic Instructions - -- Issue a POST request to end point "/challenger" - - `{{}}/challenger` -- The response will have an `X-CHALLENGER` header -- Use this in any future requests to track your progress -- The `LOCATION` header has a url to access your challenge status through the GUI - -## Example Request - -~~~~~~~~ -POST /challenger HTTP/1.1 -Host: localhost:4567 -User-Agent: rest-client -Accept: */* -Content-Length: 0 -~~~~~~~~ - -## Example Response - -~~~~~~~~ -HTTP/1.1 201 Created -Date: Tue, 28 Jul 2020 14:26:48 GMT -X-CHALLENGER: rest-api-challenges-single-player -Location: /gui/challenges -Content-Type: text/html;charset=utf-8 -Transfer-Encoding: chunked -Server: Jetty(9.4.z-SNAPSHOT) -~~~~~~~~ - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/39882254) - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/delete/delete-todos-id-200.md b/challenger/src/main/resources/content/apichallenges/solutions/delete/delete-todos-id-200.md deleted file mode 100644 index 677c84e9..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/delete/delete-todos-id-200.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -date: 2021-04-12T09:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - DELETE todos/id 200 -seo_title: Solution: DELETE todos/id 200 | API Challenges -description: How to solve API challenge DELETE todos/id 200 to delete a todo in the application. -seo_description: Use this walkthrough to solve DELETE todos/id 200 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/options/options-todos-200 -schema_howto_steps: Create a DELETE request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 200 -showads: true ---- - -# Delete a todo item in the application - -How to complete the challenge `DELETE /todos/id (200)` to successfully delete a todo item in the application. - -## DELETE /todos/id (200) - -> Issue a DELETE request to successfully delete a todo - -- `DELETE` request will delete a todo if the provided `id` exists `/todos/id` end point - - e.g. `DELETE /todos/3` to delete the todo with `id==3` -- `200` is an success code, in this case it means the todo was deleted -- The body of the message is empty -- add the `X-CHALLENGER` header - - -## Basic Instructions - -- Issue a `DELETE` request to end point "/todos/id" - - where `id` is replaced with the id of an existing todo - - if you don't know any then a `GET /todos` would show a list of todos, or you could `POST /todos` to create one. - - e.g using endpoint - - `{{}}/todos/id` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `200` when all the details are valid and the todo exists. -- To double check that the todo item was deleted, then you could issue a `GET` request on the todo directly and receive a `404` or issue a `GET` request on `/todos` and check it is not in the list of todos. - -## Example Request - -~~~~~~~~ -> DELETE /todos/62 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Thu, 27 Aug 2020 14:25:53 GMT -< Content-Type: application/json -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/49931699) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/first-challenge/get-challenges-200.md b/challenger/src/main/resources/content/apichallenges/solutions/first-challenge/get-challenges-200.md deleted file mode 100644 index 51cebac5..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/first-challenge/get-challenges-200.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: API Challenges Solution For - GET Challenges (200) -seo_title: Solution: GET Challenges (200) | API Challenges -description: How to use a GET request with an x-challenger header to get the progress status of all the API Challenges -lastmod: 2026-02-18 -seo_description: Use this walkthrough to solve GET Challenges (200) with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/get/get-todos-200 -schema_howto_steps: Create a GET request to /challenges||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `GET /challenges 200`. - -How to use a GET request with an x-challenger header to get the progress status of all the API Challenges. - -## GET /challenges (200) - -> Issue a `GET` request on the `/challenges` end point - -- This will show you the status of all the challenges in your REST Client, if you include an `X-CHALLENGER` guid header in your request. - -## Basic Instructions - -- Issue a GET request to end point "/challenges" - - `{{}}/challenges` -- The request should have an `X-CHALLENGER` header -- The response body shows the status of all the challenges. - -## Example Request - -~~~~~~~~ -> GET /challenges HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Thu, 27 Aug 2020 13:38:45 GMT -< Content-Type: application/json -< Location: /gui/challenges/x-challenger-guid -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## Example Response Payload - -~~~~~~~~ -{ - "challenges": [ - { - "name": "POST /challenger (201)", - "description": "Issue a POST request on the `/challenger` end point, - with no body, to create a new challenger session. - Use the generated X-CHALLENGER header in - future requests to track challenge completion.", - "status": true - }, - { - "name": "GET /challenges (200)", - "description": "Issue a GET request on the `/challenges` end point", - "status": true - } - ] -} -~~~~~~~~ - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/41106708) - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todo-404.md b/challenger/src/main/resources/content/apichallenges/solutions/get/get-todo-404.md deleted file mode 100644 index fce2c929..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todo-404.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: API Challenges Solution For - GET todo 404 -seo_title: Solution: GET todo 404 Guide | API Challenges -description: How to solve challenge GET todo 404. -lastmod: 2026-02-18 -seo_description: Use this walkthrough to solve GET todo 404 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/get/get-todos-id-200 -schema_howto_steps: Create a GET request to /todo||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 404 -showads: true ---- - -# How to complete the challenge `GET /todo 404`. - -How to solve challenge GET todo 404 by issuing a GET request on a non-existent endpoint and receive a 404 status code response. - -## GET /todo (404) - -> Issue a `GET` request on the `/todo` end point should 404 because nouns should be plural - -- This will show you an error status code 404 -- 404 means 'not found' i.e. we tried to access something that does not exist -- REST API Endpoints are usually plural e.g. `/todos` we would not normally expect an API to respond to both `/todos` and `/todo` - -## Basic Instructions - -- Issue a GET request to end point "/todo" - - `{{}}/todo` -- The request should have an `X-CHALLENGER` header -- The response status code should be `404` - -## Example Request - -~~~~~~~~ -> GET /todo HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 404 Not Found -< Connection: close -< Date: Thu, 27 Aug 2020 13:46:19 GMT -< X-Challenger: x-challenger-guid -< Content-Type: text/html;charset=utf-8 -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/41107933) - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-200-filter.md b/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-200-filter.md deleted file mode 100644 index 988941a7..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-200-filter.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -date: 2021-01-30T09:00:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - GET todos 200 filter -seo_title: Solution: GET todos 200 filter | API Challenges -description: How to solve API challenge GET todos 200 filter to use URL parameters to filter the results. -seo_description: Use this walkthrough to solve GET todos 200 filter with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/head/head-todos-200 -schema_howto_steps: Create a GET request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Add the required query parameters and confirm the filtered todo results||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `GET /todos (200) ? filter` - -How to issue a GET request on a top level entity endpoint and use a query filter to receive a subset of data. - -## GET /todos (200) ? filter - -> Issue a GET request on the `/todos` end point with a query filter to get only todos which are 'done'. There must exist both 'done' and 'not done' todos, to pass this challenge. - -- `GET` request will return all items from the `/todos` end point -- `200` is the success code meaning the request was accepted -- `?` means we need to add a URL Parameter -- `filter` means it will filter based on an attribute. In this case we are asked to filter on those which are 'done'. And this is represented by the `doneStatus` i.e. `doneStatus=true` -- We are using this request to `GET` a filter list of todo items -- Perform a `GET` first to see what the format of the message is -- Add a URL parameter to the request to repeat the `GET` and filter the list of todos e.g. `/todos?doneStatus=true` - -## Basic Instructions - -- Issue a `GET` request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- Look at the returned format for todos - -```js - { - "id": 41, - "title": "create todo process payroll", - "doneStatus": true, - "description": "" - }, -``` -- we want to use the `doneStatus` attribute as a URL parameter -- if you don't see any todos in the list with a `"doneStatus": true` then you will need to issue a `POST` request to create or amend a todo item. e.g. [challenge post secret 201](/apichallenges/solutions/authentication/post-secret-201) -- Issue a `GET` request with a URL parameter `/todos?doneStatus=true` -- The response status code should be `200` because the request is accepted -- If you get a different response code, check the URL or headers of the message because you made have made a typo. -- If you don't see any todos returned then you may need to create one e.g. [challenge post secret 201](/apichallenges/solutions/authentication/post-secret-201) - -## Example Request - -~~~~~~~~ -> GET /todos?doneStatus=true HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: application/json -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Tue, 15 Dec 2020 17:32:12 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "todos": [ - { - "id": 41, - "title": "create todo process payroll", - "doneStatus": true, - "description": "" - } - ] -} -``` - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/46603286) - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-200.md b/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-200.md deleted file mode 100644 index f9dbbb1c..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-200.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: API Challenges Solution For - GET Todos (200) -seo_title: Solution: GET Todos (200) Guide | API Challenges -description: How to solve the API challenge and GET all the Todos -lastmod: 2026-02-18 -seo_description: Use this walkthrough to solve GET Todos (200) with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/get/get-todo-404 -schema_howto_steps: Create a GET request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `GET /todos 200`. - -How to solve the API challenge and issue a GET request to return all the Todos in default JSON format. - -## GET /todos (200) - -> Issue a `GET` request on the `/todos` end point - -- This will show you all the todos in the system -- The return format is a useful guide for the syntax of the request you will send in POST messages -- Perform a `GET` prior to any amendment or deletion, to make sure that the data in the system is what you expect it to be. -- remember not to add a trailing `/` on the request e.g. `/todos/` - that is a different end point - -## Basic Instructions - -- Issue a GET request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header -- The response body shows all the todos. - -## Example Request - -~~~~~~~~ -> GET /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Fri, 28 Aug 2020 13:15:04 GMT -< Content-Type: application/json -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## Example Response Payload - -~~~~~~~~ -{ - "todos": [ - { - "id": 6, - "title": "process payroll", - "doneStatus": false, - "description": "" - }, - { - "id": 9, - "title": "tidy meeting room", - "doneStatus": false, - "description": "" - } - ] -} -~~~~~~~~ - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/41107610) diff --git a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-id-200.md b/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-id-200.md deleted file mode 100644 index 0cc69dcb..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-id-200.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: API Challenges Solution For - GET todos id 200 -seo_title: Solution: GET todos id 200 Guide | API Challenges -description: How to solve challenge GET todos id 200. -lastmod: 2026-02-18 -seo_description: Use this walkthrough to solve GET todos id 200 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/get/get-todos-id-404 -schema_howto_steps: Create a GET request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `GET /todos/id 200`. - -How to issue a GET request for an existing todo item using the id of the item and receive a 200 status code and see the todo item as JSON in the response body. - -## GET /todos/id (200) - -> Issue a GET request on the `/todos/{id}` end point to return a specific todo - -- This will show you a todo in the API response -- 200 status code means OK -- The response is the basic JSON format you use in a POST request to create a todo -- The `{id}` means, replace this with the id of an existing todo item - -## Basic Instructions - -- Issue a GET request to end point "/todos/{id}" - - `{{}}/todos/{id}` -- The request should have an `X-CHALLENGER` header -- The response status code should be `200` - -## Example Request - -~~~~~~~~ -> GET /todos/79 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Tue, 01 Sep 2020 13:35:41 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## Example Response Body - -~~~~~~~~ -{ - "todos": [ - { - "id": 79, - "title": "tidy meeting room", - "doneStatus": false, - "description": "" - } - ] -} -~~~~~~~~ - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/41108384) - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-id-404.md b/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-id-404.md deleted file mode 100644 index 5dcf23c7..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/get/get-todos-id-404.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -date: 2020-12-15T09:00:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - GET todos id 404 -seo_title: Solution: GET todos id 404 Guide | API Challenges -description: How to solve challenge GET todos id 404. -seo_description: Use this walkthrough to solve GET todos id 404 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/get/get-todos-200-filter -schema_howto_steps: Create a GET request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 404 -showads: true ---- - -# How to complete the challenge `GET /todos/id 404`. - -How to receive a 404 status code response by trying to GET a todo item by id for a non-existent todo item. - -## GET /todos/id (404) - -> Issue a GET request on the `/todos/{id}` end point for a todo that does not exist - -- This will show you a 404 status code in the API response -- 404 status code means Not Found -- The `{id}` means, replace this with the id of a non-existant todo item - -## Basic Instructions - -- Issue a GET request to end point "/todos/{id}" - - `{{}}/todos/{id}` -- The request should have an `X-CHALLENGER` header -- The response status code should be `404` because `{id}` does not exist -- an error message should be shown in the response body - -## Example Request - -~~~~~~~~ -> GET /todos/20 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 404 Not Found -< Connection: close -< Date: Thu, 27 Aug 2020 13:53:54 GMT -< Content-Type: application/json -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## Example Response Body - -~~~~~~~~ -{ - "errorMessages": [ - "Could not find an instance with todos/20" - ] -} -~~~~~~~~ - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/41109076) - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/head/head-todos-200.md b/challenger/src/main/resources/content/apichallenges/solutions/head/head-todos-200.md deleted file mode 100644 index e378c950..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/head/head-todos-200.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -date: 2021-01-24T09:00:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - HEAD todos 200 -seo_title: Solution: HEAD todos 200 Guide | API Challenges -description: How to solve API challenge HEAD todos 200 -seo_description: Use this walkthrough to solve HEAD todos 200 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/post-create/post-todos-201 -schema_howto_steps: Create a HEAD request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `HEAD /todos (200)` - -How to issue a HEAD request and see the results of a GET request without the body of the response, this can be useful for checking the existence of an item when automating. - -## HEAD /todos (200) - -> Issue a HEAD request on the `/todos` end point - -- `HEAD` request is basically a `GET` but doesn't return the body -- Use it to 'ping' an end point and see if it exists, or to check the Headers are working correctly -- Usually returns a 200 (if it exists) or a 404 (if it doesn't exist) - -## Basic Instructions - -- Issue a HEAD request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `200` because the end point exists -- Compare the response with the response from `GET /todos` - -## Example Request - -~~~~~~~~ -> HEAD /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: keep-alive -< Date: Thu, 27 Aug 2020 14:09:19 GMT -< Content-Type: application/json -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/41230531) - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/manage-session/save-restore-session.md b/challenger/src/main/resources/content/apichallenges/solutions/manage-session/save-restore-session.md deleted file mode 100644 index 6626f9f2..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/manage-session/save-restore-session.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -date: 2025-01-02T12:14:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - Save and Restore Sessions -seo_title: Solution: Save and Restore Sessions | API Challenges -description: How to solve API challenges for Saving and Restoring Challenge and Data. -seo_description: Use this walkthrough to solve Save and Restore Sessions with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/mix-accept-content/post-xml-accept-json -schema_howto_steps: Create a challenger session and keep the active challenger GUID||GET /challenger/{guid} to export challenge progress as JSON backup data||PUT /challenger/{guid} with the saved JSON to restore challenge progress||GET and PUT /challenger/database/{guid} to back up and restore todo data||Verify restored challenge progress and todos in the GUI before continuing -showads: true ---- - -# How to complete the Take a Break Challenges - -The API Challenges application has the ability to save and restore the challenge progress and the todo data. These challenges encourage you to track your session and data over time. - -The application used to save this automatically to AWS but to keep costs low we encourage doing this locally. - -These challenges use the endpoints which allow you to GET and POST the challenge progress data which allows you to save your progress. Additionally save and restore the Todo instance data in case the data is important to your progress. - -The UI offers the opportunity to save and restore the data in the browser storage to make the application easier to use, but if you are working with a REST Client or automating then these endpoints can help you save and restore your progress and data. - - -## Challenger Management - -After creating a Challenger using a POST request on the `/challenger` end point, with no body. This creates a new challenger session. The `X-CHALLENGER` header is then used in future requests to track challenge completion. The value of header is the GUID that you can use with the challenger end point. - -- GET `/challenger/{guid}` will return the Challenger Progress data in JSON that you can store and use as a payload to restore your progress. -- PUT `/challenger/{guid}` with the JSON progress data as the payload will restore the challenger session and progress in the system. If the Challenger is not already in the system then this will create the challenger and progress. If Challenger is in the system then this will reset their progress to the values in the JSON payload. - -To track progress when using these endpoints add the `{guid}` in the `X-CHALLENGER` when you use these endpoints. - -## TODO Data Management - -After creating a Challenger or restoring a session the TODO database for the user will be reset to the default values. These can be seen using a GET on the `/todos` endpoint. - -The `/challenger/database/{guid}` endpoint can be used to save or restore the TODO data. The `guid` is the Challenger GUID used in the `X-CHALLENGER` header. - -- GET `/challenger/database/{guid}` will return the todos in a format that can be restored -- PUT `/challenger/database/{guid}` with the payload returned from the `GET` method will restore the todos into memory for future API requests. - - -To track progress when using these endpoints add the `{guid}` in the `X-CHALLENGER` when you use these endpoints. - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/method-override/all-method-overrides.md b/challenger/src/main/resources/content/apichallenges/solutions/method-override/all-method-overrides.md deleted file mode 100644 index 84c3100f..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/method-override/all-method-overrides.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -date: 2025-01-01T14:54:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - Method Override Challenges -seo_title: Solution: Method Override Challenges | API Challenges -description: How to solve API challenges for Method Override DELETE, PATCH, TRACE. -seo_description: Use this walkthrough to solve Method Override Challenges with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/authentication/post-secret-401 -schema_howto_steps: Create a POST request to /heartbeat for method override testing||Set X-HTTP-Method-Override to DELETE, PATCH, or TRACE as required||Include X-CHALLENGER so each override challenge is tracked in your session||Send requests for each override verb and verify the expected status code||Repeat until all method override challenges are marked complete -showads: true ---- - -# How to complete the HTTP Method Override Challenges - -All of the method override challenges use the same mechanism so we can cover them all in this solution. - -Sometimes tools and libraries will not issue TRACE or PATCH requests. There is a specific HTTP header we can use to try and have POST requests treated as other verbs. - -The header "X-HTTP-Method-Override" is not guaranteed to work on every server, but some HTTP servers will take this header and treat the request using the value in the header: - -`X-HTTP-Method-Override: DELETE` - -This is worth understanding because it might also be used to bypass validation, or trigger functionality that the user is not authorized to trigger. - - -## POST /heartbeat - -> Issue a `POST` request to `/heartbeat` with an `X-HTTP-Method-Override` header specifying the verb you actually want - -- `POST` request can be sent by all tools -- We need to add the header `X-HTTP-Method-Override` to the request and the value should be the verb we want to send e.g. `TRACE` - - -## Basic Instructions - -Each challenge requires a different verb, but the process is the same for each, the only difference is the value of the `X-HTTP-Method-Override` header - -- Issue a POST request to end point "/heartbeat" -- The request should have an `X-HTTP-Method-Override` with the value associated with the challenge i.e. `DELETE`, `PATCH`, `TRACE` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should match the value for teh challenge overridden verb - - for `DELETE` be `405` - - for `TRACE` be `501` - - for `PATCH` be `500` as the API is simulating a server error - -NOTE: This header feature is normally implemented by the HTTP server so often development teams are not even aware that this is possible. Depending on how requests are validated in code it might be possible for someone, who has amend access using `POST` but who does not have `DELETE` access, to be able to use this header approach to delete something. - -NOTE: As an additional exercise, you might want to see if you can DELETE todos using a POST and the `X-HTTP-Method-Override` header. Experiment and see what you can achieve using this approach. - -## Example Request - -~~~~~~~~ -> POST /todos/3 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-HTTP-Method-Override: DELETE -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 108 -~~~~~~~~ - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/miscellaneous/create-maximum-number-todos.md b/challenger/src/main/resources/content/apichallenges/solutions/miscellaneous/create-maximum-number-todos.md deleted file mode 100644 index 5e770be1..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/miscellaneous/create-maximum-number-todos.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -date: 2025-01-01T15:26:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST /todos (201) all -seo_title: Solution: POST /todos (201) all | API Challenges -description: How to solve API challenges to Create maximum number of todos -seo_description: Use this walkthrough to solve POST /todos (201) all with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/miscellaneous/delete-all-todos -schema_howto_steps: Create todos repeatedly with POST /todos until the maximum limit is reached||Use valid JSON payloads and include X-CHALLENGER for every create request||Track successful creations so you can confirm the max-count boundary||Verify the final allowed create response and behavior at the limit||Check challenge status and confirm maximum todo creation is complete -showads: true ---- - -# How to complete the Create Maximum Number of Todos - -This challenge requires you to max out the number of todos in the system. - -The API Documentation says that `A maximum of 20 todos is allowed.` - -There are many ways to do this and some automated execution is probably required but since there are only 20 allowed, you could do this easily in a REST Client. - - -## Automated With Java RestAssured Example - -I have an automated Java execution using Rest Assured to complete this: - -- [C058DeleteAllTodosTest.java](https://github.com/eviltester/thingifier/blob/master/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_19_misc_challenges/C059AddMaximumNumberOfTodosTest.java) - -This uses a bunch of abstractions to keep the code simple but the basic process is: - -- `GET` the `/todos` and find out how many there are already -- Issue as many `POST` requests to create a todo as necessary to max it out - -``` - -TodosApi todos = new TodosApi(); -List currentTodos = todos.getTodos(); - -int todosToCreate = 20 - currentTodos.size(); - -while( todosToCreate > 0 ){ - Todo aTodo = todos.createTodo("my title " + - todosToCreate, "description", true - ); - idsToDelete.add(aTodo.id); - todosToCreate--; -}; - -// create a to do to throw it over the edge -Todo createMe = new Todo(); -createMe.title = "my title"; -createMe.description = "my description"; -``` - -## Completing Challenge Using Client Tools - -It is possible to complete this challenge manually by issuing all the requests by hand. This is easy to do because a TODO can be created with a single `POST` - -`POST` to `/todos` with a simple payload `{"title":"not unique"}` - -Resending this request would eventually result in: - -```json -{ - "errorMessages": [ - "ERROR: Cannot add instance, maximum limit of 20 reached" - ] -} -``` - -## Completing Challenge Using Client Tool Features - -Some tools have the ability to issue Data Driven requests, so if you can parse the GET response and create a list of ids then you could use the data driven feature of the tool. - -- [Bruno Data Driven Testing](https://docs.usebruno.com/testing/tests/data-driven-testing) -- [Postman Data Driven Community Posts](https://community.postman.com/tag/data-driven) - -Most of the API client tools also have the ability to create scripts to achieve this. - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/miscellaneous/delete-all-todos.md b/challenger/src/main/resources/content/apichallenges/solutions/miscellaneous/delete-all-todos.md deleted file mode 100644 index 5759cb84..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/miscellaneous/delete-all-todos.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -date: 2025-01-01T15:26:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - Delete /todos/id (200) all -seo_title: Solution: Delete /todos/id (200) all | API Challenges -description: How to solve API challenges to Delete all the todos -seo_description: Use this walkthrough to solve Delete /todos/id (200) all with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /gui/challenges -schema_howto_steps: GET /todos and capture all todo ids currently returned by the API||Send DELETE /todos/{id} for every id until no todos remain||Include X-CHALLENGER on each delete request to track challenge completion||GET /todos again and confirm the response contains an empty todo list||Verify the Delete All Todos challenge is marked complete in your session -showads: true ---- - -# How to complete the Delete All Todos Challenge - -This challenge requires you to delete the last todo item. This actually means you need to delete all the todo items. - -There are many ways to do this and some automated execution is probably required. - - -## Automated With Java RestAssured Example - -I have an automated Java execution using Rest Assured to complete this: - -- [C058DeleteAllTodosTest.java](https://github.com/eviltester/thingifier/blob/master/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_19_misc_challenges/C058DeleteAllTodosTest.java) - -This uses a bunch of abstractions to keep the code simple but the basic process is: - -- `GET` the `/todos` and create a list of all the ids -- Issue a `DELETE` request for each of the `/todos/id` - -``` - TodosApi api = new TodosApi(); - - List todos = api.getTodos(); - - for(Todo todo : todos) { - api.deleteTodo(todo.id); - } -``` - -## Completing Challenge Client Tools - -It is possible to complete this challenge manually by issuing all the requests by hand. - -Some tools have the ability to issue Data Driven requests, so if you can parse the GET response and create a list of ids then you could use the data driven feature of the tool. - -- [Bruno Data Driven Testing](https://docs.usebruno.com/testing/tests/data-driven-testing) -- [Postman Data Driven Community Posts](https://community.postman.com/tag/data-driven) - -Most of the API client tools also have the ability to create scripts to achieve this. - -## Deleting All `todo`s using Command Line Tools - -I'm going use standard Linux/Unix/Bash commands here. - -I can use a combination of `curl`, `jq`, and `xargs` to achieve what I want. - -- [cUrl](https://curl.se/) command line HTTP client -- [jq](https://jqlang.github.io/jq/) a command line JSON processor -- [xargs](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/xargs.html) piping parameters into commands [examples](https://en.wikipedia.org/wiki/Xargs) - -I can use all these linux commands on Windows using a WSL ((Windows Subsystem for Linux)[https://learn.microsoft.com/en-us/windows/wsl/about]) which means I can use the same commands as a mac and make this solution portable. - -I'm running Ubuntu in my WSL - -First issue a curl request to get the todos in API challenges. - -If I don't know how to create the `culr` command I can generate the `curl` command easily by using Bruno to generate the code (most REST Clients also have this feature). - -- create request in Bruno with the X-CHALLENGER header -- try it out -- right click on the item in the left side bar and `Generate Code` and then choose `Shell-Curl` - -``` -curl --request GET \ - --url https://apichallenges.eviltester.com/todos \ - --header 'X-CHALLENGER: 07466215-9bab-4bf4-9b7d-34b7ac765915' -``` - -Replace the `X-CHALLENGER` GUID with your GUID - -I will actually save the response into a file to make it easier to work with. - -``` -curl --request GET \ - --url https://apichallenges.eviltester.com/todos \ - --header 'X-CHALLENGER: 07466215-9bab-4bf4-9b7d-34b7ac765915' - > todos.json -``` - -The response is JSON, I want to parse that JSON to find all the ids, because I'm going to use that list of ids to delete them all. - -I will use JQ for that. - -The command below iterates over all the items in the todo array and extracts the id - -``` -jq '.todos[].id' todos.json -``` - -I can save this list to a file - -``` -jq '.todos[].id' todos.json > ids.txt -``` - -Then if I `cat ids.txt` I can see all the ids - -``` -> cat ids.txt -1 -8 -5 -2 -6 -3 -9 -7 -4 -10 -``` - -I then want to call curl for each of these values and I can use `xargs` to do that - -``` -cat ids.txt | xargs -I % curl --request DELETE \ - --url https://apichallenges.eviltester.com/todos/% \ - --header 'X-CHALLENGER: 07466215-9bab-4bf4-9b7d-34b7ac765915' -``` - -I can double check that I have deleted them by looking at the API Challenges progress page. - -Or just call the `GET /todos` and see an empty array - -``` -> curl --request GET \ - --url https://apichallenges.eviltester.com/todos \ - --header 'X-CHALLENGER: 07466215-9bab-4bf4-9b7d-34b7ac765915' - -{"todos":[]} -``` - - -Or I could do it all in one command: - -``` -curl --request GET \ - --url https://apichallenges.eviltester.com/todos \ - --header 'X-CHALLENGER: 880c5857-dbff-4266-b419-701efa804679' | -jq '.todos[].id' | -xargs -I % curl --request DELETE \ - --url https://apichallenges.eviltester.com/todos/% \ - --header 'X-CHALLENGER: 880c5857-dbff-4266-b419-701efa804679' -``` - -## Delete All Todos Using Command Line Tools Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/119362209) diff --git a/challenger/src/main/resources/content/apichallenges/solutions/mix-accept-content/post-json-accept-xml.md b/challenger/src/main/resources/content/apichallenges/solutions/mix-accept-content/post-json-accept-xml.md deleted file mode 100644 index b27320f9..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/mix-accept-content/post-json-accept-xml.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -date: 2021-07-18T08:00:00Z -lastmod: 2026-02-18 -title: API Testing Challenge 24 - How To - POST JSON accept XML -seo_title: Solution: POST JSON accept XML | API Challenges -description: How to solve API challenge POST /todos JSON to XML. Creating a todo with JSON and receiving response in XML. -seo_description: Use this walkthrough to solve POST JSON accept XML with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/status-codes/status-codes-405-500-501-204 -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send a valid XML payload that matches the field and content constraints -showads: true ---- - -# How to complete the challenge `POST /todos JSON to XML` - -The `content-type` and `accept` headers do not need to be the same. -API Challenges allows us to send a POST request to create a todo item using JSON but receive an XML response. - -## POST /todos XML to JSON - -> Issue a POST request on the `/todos` end point to create a todo using Content-Type `application/json` but Accept `application/xml` - -- `POST` request means we will send information in the body of the message - - e.g. `POST /todos` sends to the todos endpoint -- `create a todo` means that the payload will be valid data to create a todo item -- `using Content-Type` `application/json` means that we will set `content-type` header to `application/json` and the payload will be in JSON format -- `Accept` `application/xml` means add an `accept` header of `application/xml` to receive the response in JSON format -- add the `X-CHALLENGER` header to track progress - - -## Basic Instructions - -We can mix different accept and content-types so we can send payloads in one format, and receive responses in another format. This challenge is about sending payload in JSON but having the response in XML. - -- Issue a `POST` request to end point "/todos" - - if running locally that endpoint would be - - `{{}}/todos` -- The request should have an `Content-Type` header of `application/json` -- add a valid payload in JSON format to create a todo item e.g. - -```json - { - "title": "create todo process payroll", - "doneStatus": true, - "description": "" - } -``` - - -- add an `accept` header of `application/xml` to receive the response in XML format -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `201` when all the details are valid. -- Check the body of the response has XML formatted todo item with full details of the created item -- Check the location header for the REST API call to retrieve details of the created item - -Extras: - -- try GET the location header URL to return the created todo item - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> accept: application/xml -> Content-Length: 106 - -| { -| "title": "create todo process payroll", -| "doneStatus": true, -| "description": "" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 201 Created -< Connection: close -< Date: Sun, 18 Jul 2021 09:03:58 GMT -< Content-Type: application/xml -< Location: todos/320 -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```xml - - true - - 320 - create todo process payroll - -``` - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/53821574) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/mix-accept-content/post-xml-accept-json.md b/challenger/src/main/resources/content/apichallenges/solutions/mix-accept-content/post-xml-accept-json.md deleted file mode 100644 index f915ea70..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/mix-accept-content/post-xml-accept-json.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -date: 2021-07-17T11:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST XML accept JSON -seo_title: Solution: POST XML accept JSON | API Challenges -description: How to solve API challenge POST /todos XML to JSON. Creating a todo with XML and receiving response in JSON. -seo_description: Use this walkthrough to solve POST XML accept JSON with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/mix-accept-content/post-json-accept-xml -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Set the Accept header to the required media type and verify response format||Send a valid XML payload that matches the field and content constraints -showads: true ---- - -# How to complete the challenge `POST /todos XML to JSON` - -When an API supports multiple formats in request and response we might be able to mix and match. -In this challenge we send a POST request to create a todo item using XML but receive JSON response. - -## POST /todos XML to JSON - -> Issue a POST request on the `/todos` end point to create a todo using Content-Type `application/xml` but Accept `application/json` - -- `POST` request means we will send information in the body of the message - - e.g. `POST /todos` sends to the todos endpoint -- `create a todo` means that the payload will be valid data to create a todo item -- `using Content-Type` `application/xml` means that we will set `content-type` header to `application/xml` and the payload will be in XML format -- `Accept` `application/json` means add an `accept` header of `application/json` to receive the response in JSON format -- add the `X-CHALLENGER` header to track progress - - -## Basic Instructions - -We can mix different accept and content-types so we can send payloads in one format, and receive responses in another format. This challenge is about sending payload in XML but having the response in JSON. - -- Issue a `POST` request to end point "/todos" - - if running locally that endpoint would be - - `{{}}/todos` -- The request should have an `Content-Type` header of `application/xml` -- add a valid payload in XML format to create a todo item e.g. - -```xml - - true - file paperwork today - -``` - - -- add an `accept` header of `application/json` to receive the response in JSON format -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `201` when all the details are valid. -- Check the body of the response has JSON formatted todo item with full details of the created item -- Check the location header for the REST API call to retrieve details of the created item - -Extras: - -- try GET the location header URL to return the created todo item - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/xml -> Accept: application/json -> Content-Length: 92 - -| -| true -| file paperwork today -| -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 201 Created -< Connection: close -< Date: Sat, 17 Jul 2021 14:37:49 GMT -< Content-Type: application/json -< Location: todos/278 -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Example Response body: - -```json -{ - "id": 278, - "title": "file paperwork today", - "doneStatus": true, - "description": "" -} -``` - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/53796838) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/options/options-todos-200.md b/challenger/src/main/resources/content/apichallenges/solutions/options/options-todos-200.md deleted file mode 100644 index 2086119b..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/options/options-todos-200.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -date: 2021-04-21T09:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - OPTIONS todos 200 -seo_title: Solution: OPTIONS todos 200 Guide | API Challenges -description: How to solve API challenge 13 OPTIONS todos 200 to identify the allowed verbs for an API End Point. -seo_description: Use this walkthrough to solve OPTIONS todos 200 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/accept-header/get-todos-200-xml -schema_howto_steps: Create an OPTIONS request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `OPTIONS /todos (200)` - -We can use `OPTIONS` request to identify the allowed verbs for an API End Point. This is useful to compare these with the Swagger/Open API documentation and also to check if the unlisted verbs are actually disallowed by the API. - -## OPTIONS /todos (200) - -> Issue an OPTIONS request on the `/todos` end point. You might want to manually check the 'Allow' header in the response is as expected. - -- `OPTIONS` request will receive a response with no body, just headers if the provided end point exists i.e the `/todos` end point - - e.g. `OPTIONS /todos` to show the allowed verbs for the `todos` endpoint -- `200` is a success code, in this case it means the end point exists and the `OPTIONS` verb is allowed -- The body of the message is empty -- add the `X-CHALLENGER` header to track progress -- the important header in the response is the `allow` header as this lists all the allowed verbs to use on the end point. - - -## Basic Instructions - -- Issue an `OPTIONS` request to end point "/todos" - - if running locally that endpoint would be - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The response status code should be `200` when all the details are valid. -- Check the `allow` header in the response has valid values - -As a set of follow on exercises: - -- try `OPTIONS` on a few other endpoints in the API and see if the `allow` values are different. -- Try to issue requests for each of the allowed verbs. - - -## Example Request - -~~~~~~~~ -> OPTIONS /todos HTTP/1.1 -> Host: {{}} -> User-Agent: insomnia/2020.3.3 -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Mon, 12 Apr 2021 09:41:34 GMT -< Allow: OPTIONS, GET, HEAD, POST -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Content-Type: text/html;charset=utf-8 -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/50387322) - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-201-max-content.md b/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-201-max-content.md deleted file mode 100644 index 59eb2f57..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-201-max-content.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -date: 2024-12-31T14:04:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos 201 - max out content -seo_title: Solution POST todos 201 max out content | API Challenges -description: How to solve API challenge POST todos 201 max out content by sending request details such that all fields are maximum length. -seo_description: Use this walkthrough to solve POST todos 201 - max out with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/post-create/post-todos-413-content-too-long -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 201 -showads: true ---- - -# How to complete the challenge `POST /todos (201) max out content` - -How to complete the challenge `POST /todos (201) max out content` to successfully create a todo item in the application where the title and the description are the maximum length. - -## POST /todos (201) max out content - -> Issue a POST request to create a todo with maximum length allowed `title` and `description` fields - -- `POST` request will create a todo if the details are valid when using the `/todos` end point -- `201` is a status code meaning that we supplied valid details and a new item was created -- In this case we are asked to make `title` field and `description` field contents the maximum length -- The API Documentation for the todos endpoint says that a title `Maximum length allowed is 50` -- The API Documentation for the todos endpoint says that a description `Maximum length allowed is 200` - -## Basic Instructions - -- Issue a `POST` request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have a `title` containing 50 characters and a `description` of 200 characters length. - -```json - { - "title": "this title has just enough characters to validate.", - "doneStatus": true, - "description": "This description has just enough characters to validate because it is exactly 200 characters in length. I had to use a tool to check this - so I should have used a CounterString to be absolutely sure." - } -``` -- The response status code should be `201` because the request is valid and the todo item created -- The body of the response will be the todo details - -```json -{ - "errorMessages": [ - "Failed Validation: Maximum allowable length exceeded for title - maximum allowed is 50" - ] -} -``` - -Hints: - -- when testing for field lengths CounterString tools can be useful to generate strings of the exact length required -- a 201 response usually has a `Location` header with a URL which we could `GET` to see the created item - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 116 - -| { -| "title": "this title has just enough characters to validate.", -| "doneStatus": true, -| "description": "This description has just enough characters to validate because it is exactly 200 characters in length. I had to use a tool to check this - so I should have used a CounterString to be absolutely sure." -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 201 Created -< Connection: close -< Date: Thu, 27 Aug 2020 14:23:12 GMT -< Content-Type: application/json -< Location: /todos/11 -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "id": 11, - "title": "this title has just enough characters to validate.", - "doneStatus": true, - "description": "This description has just enough characters to validate because it is exactly 200 characters in length. I had to use a tool to check this - so I should have used a CounterString to be absolutely sure." -} -``` - - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-201.md b/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-201.md deleted file mode 100644 index 4dcf39bc..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-201.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -date: 2021-01-24T10:00:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos 201 -seo_title: Solution: POST todos 201 Guide | API Challenges -description: How to solve API challenge POST todos 201 to create a todo item in the application. -seo_description: Use this walkthrough to solve POST todos 201 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/post-create/post-todos-400 -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 201 -showads: true ---- - -# How to complete the challenge `POST /todos (201)` - -How to use a POST request to create a todo item in the application and receive a 201 status response code. - -## POST /todos (201) - -> Issue a `POST` request to successfully create a todo - -- `POST` request will make changes to the system -- `201` means an item created -- We are using this request to create a todo item -- Perform a `GET` first to see what the format of the message is -- Add a `Content-Type` of `application/json` to tell the server what format the message body is - -## Basic Instructions - -- Issue a POST request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- Use the format for the todo which you see in the `GET` response -- Do not include an 'id' in the request because that is added automatically by the system -- The response status code should be `201` because the todo is created -- IF you get a different response code, check the body of the message because you made have made a typo. Read the error message in the response to guide you. - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 108 - -| { -| "title": "create todo process payroll", -| "doneStatus": true, -| "description": "" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 201 Created -< Connection: close -< Date: Tue, 31 Aug 2021 16:20:40 GMT -< Content-Type: application/json -< Location: todos/453 -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/41230767) - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400-description-too-long.md b/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400-description-too-long.md deleted file mode 100644 index e6a98d14..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400-description-too-long.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -date: 2024-12-31T14:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos 400 - description too long -seo_title: Solution: POST todos 400 - description | API Challenges -description: How to solve API challenge POST todos 400 description too long by sending request details such that they fail validation. -seo_description: Use this walkthrough to solve POST todos 400 - with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/post-create/post-todos-201-max-content -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 400 -showads: true ---- - -# How to complete the challenge `POST /todos (400) description too long` - -How to complete the challenge `POST /todos (400) description too long` to fail to create a todo item in the application due to not passing validation for the description. - -## POST /todos (400) description too long - -> Issue a POST request to create a todo but fail validation on the `description` field - -- `POST` request will create a todo if the details are valid when using the `/todos` end point -- `400` is an error code meaning that we supplied invalid details -- In this case we are asked to make a mistake with the `description` field so that it fails validation on the server side -- The API Documentation for the todos endpoint says that a title `Maximum length allowed is 200` - -## Basic Instructions - -- Issue a `POST` request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have an error in the `description`. A valid `description` is Maximum of 200 characters so if we create a title with 201 characters it should fail validation and pass the challenge. - -```json -{ - "title": "this title is fine", - "doneStatus": true, - "description": "Should trigger a 400 error because this description is too long and exceeds the maximum length of 200 characters. We should do additional testing to make sure that 200 is valid. And check large strings" -} -``` -- The response status code should be `400` because the request is invalid -- The body of the response will be an error message array with a single message - -```json -{ - "errorMessages": [ - "Failed Validation: Maximum allowable length exceeded for description - maximum allowed is 200" - ] -} -``` - -Hints: - -- when testing for field lengths CounterString tools can be useful to generate strings of the exact length required - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 116 - -| { -| "description": "Should trigger a 400 error because this description is too long and exceeds the maximum length of 200 characters. We should do additional testing to make sure that 200 is valid. And check large strings", -| "doneStatus": true, -| "description": "should trigger a 400 error" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 400 Bad Request -< Connection: close -< Date: Thu, 27 Aug 2020 14:23:12 GMT -< Content-Type: application/json -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "errorMessages": [ - "Failed Validation: Maximum allowable length exceeded for description - maximum allowed is 200" - ] -} -``` - - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400-extra-field.md b/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400-extra-field.md deleted file mode 100644 index 1cb564ff..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400-extra-field.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -date: 2021-01-30T15:55:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos 400 extra -seo_title: Solution: POST todos 400 extra | API Challenges -description: How to solve API challenge POST todos 400 extra to trigger validation errors due to an extra field in the payload. -seo_description: Use this walkthrough to solve POST todos 400 extra with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/put-create/put-todos-400-create -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 400 -showads: true ---- - -# How to complete the challenge `POST /todos (400) extra` - -How to complete the challenge `POST /todos (400) extra` to fail to create a todo item in the application due to not passing validation when the payload contains an extra field. - -## POST /todos (400) extra - -> Issue a POST request to create a todo but fail validation due to an unrecognised field - -- `POST` request will create a todo if the details are valid when using the `/todos` end point -- `400` is an error code meaning that we supplied invalid details -- In this case we are asked to make a mistake by adding an extra field not defined in the request schema e.g. `priority="extra"` - -## Basic Instructions - -- Issue a `POST` request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have an error due to an unexpected field. - -```json -{ - "title": "a title", - "priority": "extra" -} -``` -- The response status code should be `400` because the request is invalid -- The body of the response will be an error message array with a single message - -```json -{ - "errorMessages": [ - "Could not find field: priority" - ] -} -``` - -Hints: - -- We don't just want to check for mandatory and missing content we need to make sure that the server does not try and create entities and inject new fields into the database -- For follow on exercises you might want to see what happens: - - if we duplicate fields e.g. have two `title` fields - - if we duplicate headers -- When testing APIS we need to go beyond field contents and look at the message format itself - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 116 - -| { -| "title": "a title", -| "priority": "extra" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 400 Bad Request -< Connection: close -< Date: Thu, 27 Aug 2020 14:23:12 GMT -< Content-Type: application/json -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "errorMessages": [ - "Could not find field: priority" - ] -} -``` - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400-title-too-long.md b/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400-title-too-long.md deleted file mode 100644 index 88e988c9..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400-title-too-long.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -date: 2024-12-31T14:04:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos 400 - title too long -seo_title: Solution POST todos 400 title too long | API Challenges -description: How to solve API challenge POST todos 400 title too long by sending request details such that they fail validation. -seo_description: Use this walkthrough to solve POST todos 400 - title too with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/post-create/post-todos-400-description-too-long -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 400 -showads: true ---- - -# How to complete the challenge `POST /todos (400) title too long` - -How to complete the challenge `POST /todos (400) title too long` to fail to create a todo item in the application due to not passing validation for the title. - -## POST /todos (400) title too long - -> Issue a POST request to create a todo but fail validation on the `title` field - -- `POST` request will create a todo if the details are valid when using the `/todos` end point -- `400` is an error code meaning that we supplied invalid details -- In this case we are asked to make a mistake with the `title` field so that it fails validation on the server side -- The API Documentation for the todos endpoint says that a title `Maximum length allowed is 50` - -## Basic Instructions - -- Issue a `POST` request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have an error in the `title`. A valid `title` is Maximum of 50 characters so if we create a title with 51 characters it should fail validation and pass the challenge. - -```json - { - "title": "this title has far too many characters to validate.", - "doneStatus": true, - "description": "should trigger a 400 error" - } -``` -- The response status code should be `400` because the request is invalid -- The body of the response will be an error message array with a single message - -```json -{ - "errorMessages": [ - "Failed Validation: Maximum allowable length exceeded for title - maximum allowed is 50" - ] -} -``` - -Hints: - -- when testing for field lengths CounterString tools can be useful to generate strings of the exact length required - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 116 - -| { -| "title": "this title has far too many characters to validate.", -| "doneStatus": true, -| "description": "should trigger a 400 error" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 400 Bad Request -< Connection: close -< Date: Thu, 27 Aug 2020 14:23:12 GMT -< Content-Type: application/json -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "errorMessages": [ - "Failed Validation: Maximum allowable length exceeded for title - maximum allowed is 50" - ] -} -``` - - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400.md b/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400.md deleted file mode 100644 index 922d1775..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-400.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -date: 2021-01-30T09:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos 400 -seo_title: Solution: POST todos 400 Guide | API Challenges -description: How to solve API challenge POST todos 400 to amend the request details such that they fail validation. -seo_description: Use this walkthrough to solve POST todos 400 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/post-create/post-todos-400-title-too-long -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 400 -showads: true ---- - -# How to complete the challenge `POST /todos (400)` - -How to complete the challenge `POST /todos (400)` to fail to create a todo item in the application due to not passing validation. - -## POST /todos (400) - -> Issue a POST request to create a todo but fail validation on the `doneStatus` field - -- `POST` request will create a todo if the details are valid when using the `/todos` end point -- `400` is an error code meaning that we supplied invalid details -- In this case we are asked to make a mistake with the `doneStatus` field so that it fails validation server side i.e. `doneStatus="bob"` - -## Basic Instructions - -- Issue a `POST` request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have an error in the `doneStatus`. A valid `doneStatus` is `true` or `false` so if we send in a String like `"bob"` it should fail validation. - -```json - { - "title": "create new todo", - "doneStatus": "bob", - "description": "created via insomnia" - } -``` -- The response status code should be `400` because the request is invalid -- The body of the response will be an error message array with a single message - -```json -{ - "errorMessages": [ - "Failed Validation: doneStatus should be BOOLEAN" - ] -} -``` - - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 116 - -| { -| "title": "create new todo", -| "doneStatus": "bob", -| "description": "created via insomnia" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 400 Bad Request -< Connection: close -< Date: Thu, 27 Aug 2020 14:23:12 GMT -< Content-Type: application/json -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "errorMessages": [ - "Failed Validation: doneStatus should be BOOLEAN" - ] -} -``` - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/46603655) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-413-content-too-long.md b/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-413-content-too-long.md deleted file mode 100644 index 6bbc4ad6..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/post-create/post-todos-413-content-too-long.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -date: 2024-12-31T15:35:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos 413 - content too long -seo_title: Solution POST todos 413 long content | API Challenges -description: How to solve API challenge POST todos 413 content too long by sending a payload that is too large. -seo_description: Use this walkthrough to solve POST todos 413 - content with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/post-create/post-todos-400-extra-field -schema_howto_steps: Create a POST request to /todos||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 413 -showads: true ---- - -# How to complete the challenge `POST /todos (413) content too long` - -How to complete the challenge `POST /todos (413) content too long` to fail to create a todo item in the application because the request payload sent to the API is too long. - -## POST /todos (413) content too long - -> Issue a POST request to create a todo with total payload content greater than 5000 characters - -- `POST` request will create a todo if the details are valid when using the `/todos` end point -- `413` is a status code meaning "Request Entity Too Large" meaning that we supplied a request that is too long for the server to handle -- The server is coded to only accept a maximum of 5000 characters -- To pass this challenge write 5000 characters or more in the descripton field - -## Basic Instructions - -- Issue a `POST` request to end point "/todos" - - `{{}}/todos` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should be more than 5000 characters (the example below needs to be amended) -- You can easily generate a 5000 characters String [online using a CounterString Generator](https://eviltester.github.io/TestingApp/apps/counterstrings/counterstrings.html) and replace `` with the longer string - -```json - { - "title": "this title is valid.", - "doneStatus": true, - "description": "" - } -``` -- The response status code should be `413` because the request is too long, the error message describes the valid length - - -```json -{ - "errorMessages": [ - "Error: Request body too large, max allowed is 5000 bytes" - ] -} -``` - -Hints: - -- when testing for field or message lengths CounterString tools can be useful to generate strings of the exact length required -- when testing APIs we need to do more than just test field lengths, we need to make sure the server is not vulnerable to Denial of Service attacks from the payload sizes -- As a follow on exercise: try to send in a payload of exactly 5000 bytes - -## Example Request - -~~~~~~~~ -> POST /todos HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 116 - -| { -| "title": "this title is valid.", -| "doneStatus": true, -| "description": "" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 413 Payload Too Large -< Connection: close -< Date: Thu, 27 Aug 2020 14:23:12 GMT -< Content-Type: application/json -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "errorMessages": [ - "Error: Request body too large, max allowed is 5000 bytes" - ] -} -``` - - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/post-update/post-todos-id-200.md b/challenger/src/main/resources/content/apichallenges/solutions/post-update/post-todos-id-200.md deleted file mode 100644 index 20c90db2..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/post-update/post-todos-id-200.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -date: 2021-03-07T09:30:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos/id 200 -seo_title: Solution: POST todos/id 200 Guide | API Challenges -description: How to solve API challenge POST todos/id 200 to update a todo in the application. -seo_description: Use this walkthrough to solve POST todos/id 200 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/post-update/post-todos-id-404 -schema_howto_steps: Create a POST request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `POST /todos/id (200)` - -How to use a POST request to successfully update a todo item in the application. - -## POST /todos/id (200) - -> Issue a POST request to successfully update a todo - -- `POST` request will update a todo if the provided `id` exists `/todos/id` end point - - e.g. `POST /todos/3` for a todo with `id==3` -- `200` is an success code, in this case it means the todo was updated -- The body of the message should be a `json` or `xml` partial set of `todo` details, -- and the `json` or `xml` should be defined in the `content-type` header - - -## Basic Instructions - -- Issue a `POST` request to end point "/todos/id" - - where `id` is replaced with the id of an existing todo - - if you don't know any then a `GET /todos` would show a list of todos, or you could `POST /todos` to create one. - - `{{}}/todos/id` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have a partial set of todo details. e.g. - -```json -{ - "title": "updated title" -} -``` -- The response status code should be `200` when all the details are valid. -- The body of the response will a JSON showing the full todo details, and your updated values should be present. - -```json -{ - "id": 49, - "title": "updated title", - "doneStatus": false, - "description": "" -} -``` - -NOTE: if you haven't read the documentation and don't know what format to use then issue a GET request for a single entity and the payload format for the `POST` is likely to be pretty close. You may not be allowed to use all the fields in an update, e.g. the `id` might throw an error becuase you should not be able to update the `id`. - - -## Example Request - -~~~~~~~~ -> POST /todos/49 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 32 - -| { -| "title": "updated title" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sat, 06 Feb 2021 12:08:58 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "id": 49, - "title": "updated title", - "doneStatus": false, - "description": "" -} -``` - -## Overview Video - -{{}} - -[Patreon ad free version with transcript](https://www.patreon.com/posts/48448220) - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/post-update/post-todos-id-404.md b/challenger/src/main/resources/content/apichallenges/solutions/post-update/post-todos-id-404.md deleted file mode 100644 index 73248f68..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/post-update/post-todos-id-404.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -date: 2024-01-01T11:26:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - POST todos/id 404 -seo_title: Solution: POST todos/id 404 Guide | API Challenges -description: How to solve API challenge POST todos/id 404 to try to update a todo which does not exist. -seo_description: Use this walkthrough to solve POST todos/id 404 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/put-update/put-todos-id-200-update-full -schema_howto_steps: Create a POST request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 404 -showads: true ---- - -# How to complete the challenge `POST /todos/id (404)` - -How to use a POST request to try to update a todo item in the application, but the todo item id should not exist. - -## POST /todos/id (404) - -> Issue a POST request to try and update a todo, but no todo with this id should exist - -- `POST` request will update a todo if the provided `id` exists `/todos/id` end point - - e.g. `POST /todos/3` for a todo with `id==3` -- `404` is a failure code, in this case it means no todo with this id exists -- The body of the message should be a `json` or `xml` partial set of `todo` details, -- and the `json` or `xml` should be defined in the `content-type` header -- The 404 response should have an error message explaining the problem - - -## Basic Instructions - -- Issue a `POST` request to end point "/todos/id" - - where `id` is replaced with the id of a todo that does not exist - - if you don't know any then a `GET /todos` would show a list of todos. - - `{{}}/todos/id` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have a partial set of todo details. e.g. - -```json -{ - "title": "updated title" -} -``` -- The response status code should be `404` when the details are valid and the id does not exist. -- The body of the response will be a JSON showing the error. - -```json -{ - "errorMessages": [ - "No such todo entity instance with id == 200 found" - ] -} -``` - -## Example Request - -~~~~~~~~ -> POST /todos/200 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 32 - -| { -| "title": "updated title" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 404 Not Found -< Connection: close -< Date: Sat, 06 Feb 2021 12:08:58 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur - -| { -| "errorMessages": [ -| "No such todo entity instance with id == 200 found" -| ] -| } -~~~~~~~~ - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/put-create/put-todos-400-create.md b/challenger/src/main/resources/content/apichallenges/solutions/put-create/put-todos-400-create.md deleted file mode 100644 index 6ebcaa5e..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/put-create/put-todos-400-create.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -date: 2025-01-01T11:05:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - PUT todos/id 400 -seo_title: Solution: PUT todos/id 400 Guide | API Challenges -description: How to solve API challenge PUT todos/id 400 invalid to create with PUT. -seo_description: Use this walkthrough to solve PUT todos/id 400 with request setup, key headers, and expected status codes so you can complete the challenge confidently. -next_challenge: /apichallenges/solutions/post-update/post-todos-id-200 -schema_howto_steps: Create a PUT request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 400 -showads: true ---- - -# How to complete the challenge `PUT /todos/{id} (400)` - -How to use a PUT request and fail to create a todo item in the application using PUT, receiving a 400 status response code. The request is invalid because IDs are auto generated and cannot be specified during creation. - -## POST /todos/{id} (400) - -> Issue a `PUT` request with an ID and fail to create a todo - -- `PUT` request will make changes to the system -- `400` means an issue with the request -- We are trying to use this request to create a todo item so all mandatory details are necessary -- We are including an id, that does not exist, which we want to assign to the todo item -- Add a `Content-Type` of `application/json` to tell the server what format the message body is - -## Basic Instructions - -- Issue a GET request on "/todos" to identify an ID that does not exist -- Issue a PUT request to end point "/todos/{id}" where `{id}` is a specific id that we want to create - - `{{}}/todos/{id}` -- The request should have an `X-CHALLENGER` header to track challenge completion -- Use the format for the todo which you see in a `GET` response, without the ID field -- Do not include an 'id' in the request body because that is added automatically by the system -- Do include an `id` in the URL to try and specify the id we want to create -- The response status code should be `400` because the request is invalid for this API -- Check the error message to make sure it is for the expected reason `"Cannot create todo with PUT due to Auto fields id"` -- If you get a different response code, check the body of the message because you made have made a typo. Read the error message in the response to guide you. - -NOTE: For some APIs this would be an acceptable thing to do. This particular API has logic to prevent creating items with specific IDs and this challenge helps test for it. - -## Example Request - -~~~~~~~~ -> POST /todos/300 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 108 - -| { -| "title": "create todo process payroll", -| "doneStatus": true, -| "description": "" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 400 Bad Request -< Connection: close -< Date: Tue, 31 Aug 2021 16:20:40 GMT -< Content-Type: application/json -< Location: todos/453 -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur - -| { -| "errorMessages": [ -| "Cannot create todo with PUT due to Auto fields id" -| ] -| } -~~~~~~~~ - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-200-update-full.md b/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-200-update-full.md deleted file mode 100644 index 95917e4c..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-200-update-full.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -date: 2025-01-01T12:53:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - PUT todos/id 200 full update -seo_title: Solution: PUT todos/id 200 full update | API Challenges -description: How to solve API challenge PUT todos/id 200 to update a todo in the application with a full payload. -seo_description: Master PUT /todos/{id} full updates by sending all required fields, setting correct headers, and validating the 200 response payload. -next_challenge: /apichallenges/solutions/put-update/put-todos-id-200-update-partial -schema_howto_steps: Create a PUT request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `PUT /todos/id (200) full update` - -How to use a PUT request to successfully update a todo item in the application using a full payload. - -PUT request updates are idempotent so should generate the same response each time. - -## PUT /todos/id (200) full update - -> Issue a PUT request to successfully update a todo using a full payload - -- `PUT` request will update a todo if the provided `id` exists `/todos/id` end point - - e.g. `PUT /todos/3` for a todo with `id==3` -- `200` is an success code, in this case it means the todo was updated -- The body of the message should be a `json` or `xml` partial set of `todo` details, -- and the `json` or `xml` should be defined in the `content-type` header -- the id can optionally be included in the payload, but all other fields must be - - -## Basic Instructions - -- Issue a `PUT` request to end point "/todos/id" - - where `id` is replaced with the id of an existing todo - - if you don't know any then a `GET /todos` would show a list of todos - - `{{}}/todos/id` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have a full set of todo details. e.g. - -```json -{ - "id": 3 - "title": "updated title", - "doneStatus": false, - "description": "updated description" -} -``` -- The id included in the payload should be the same as the id of the url because we cannot update the id, the id is auto generated. -- The response status code should be `200` when all the details are valid. -- The body of the response will a JSON showing the full todo details, and your updated values should be present. - -```json -{ - "id": 3, - "title": "updated title", - "doneStatus": false, - "description": "updated description" -} -``` - -NOTE: if you haven't read the documentation and don't know what format to use then issue a GET request for a single entity and the payload format for the `POST` is likely to be pretty close. - -NOTE: because you add an id to the payload you risk triggering an error validation if the id in the payload is different from the id in the URL. - -## Example Request - -~~~~~~~~ -> PUT /todos/3 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 32 - -| { -| "id": 3, -| "title": "updated title", -| "doneStatus": false, -| "description": "updated description" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sat, 06 Feb 2021 12:08:58 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "id": 3, - "title": "updated title", - "doneStatus": false, - "description": "updated description" -} -``` - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-200-update-partial.md b/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-200-update-partial.md deleted file mode 100644 index e8404c4f..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-200-update-partial.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -date: 2025-01-01T12:53:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - PUT todos/id 200 full update -seo_title: Solution: PUT todos/id 200 partial | API Challenges -description: How to solve API challenge PUT todos/id 200 to update a todo in the application with a full payload. -seo_description: Learn how partial PUT /todos/{id} updates behave, which fields can be omitted, and how to verify a 200 response with expected changes. -next_challenge: /apichallenges/solutions/put-update/put-todos-id-400-no-title -schema_howto_steps: Create a PUT request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 200 -showads: true ---- - -# How to complete the challenge `PUT /todos/id (200) partial update` - -How to use a PUT request to successfully update a todo item in the application using a partial payload. - -PUT request updates are idempotent so should generate the same response each time. A partial PUT update is different from a partial POST update. With a POST update any missing fields in the response will not be amended. With a PUT update the missing fields will be set to their default or empty values. - -This behaviour varies for different APIs. Some APIs may not allow partial PUT updates. - -## PUT /todos/id (200) partial update - -> Issue a PUT request to successfully update a todo using a partial payload - -- `PUT` request will update a todo if the provided `id` exists `/todos/id` end point - - e.g. `PUT /todos/3` for a todo with `id==3` -- `200` is an success code, in this case it means the todo was updated -- The body of the message should be a `json` or `xml` partial set of `todo` details, -- and the `json` or `xml` should be defined in the `content-type` header -- the id does not need to be included in the payload, but if it is then it should match the id in the url -- the fields included in the message should be the mandatory fields, otherwise the payload will not validate - - -## Basic Instructions - -- Issue a `PUT` request to end point "/todos/id" - - where `id` is replaced with the id of an existing todo - - if you don't know any then a `GET /todos` would show a list of todos - - `{{}}/todos/id` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have a partial set of todo details. All mandatory fields should be included e.g. - -```json -{ - "title": "partial update for title" -} -``` -- Title is a mandatory field, without a default value -- `doneStatus` is boolean and defaults to `false` -- `description` has a default value of `""` -- The id included in the payload should be the same as the id of the url because we cannot update the id, the id is auto generated. -- The response status code should be `200` when all the details are valid. -- The body of the response will a JSON showing the full todo details, and your updated values should be present. - -```json -{ - "id": 3, - "title": "partial update for title", - "doneStatus": false, - "description": "" -} -``` - -NOTE: if you haven't read the documentation and don't know what format to use then issue a GET request for a single entity and the payload format for the `POST` is likely to be pretty close. - -NOTE: because you add an id to the payload you risk triggering an error validation if the id in the payload is different from the id in the URL. - -NOTE: PUT is idempotent so the result will always be the same, regardless of the initial values of the todo prior to the update request. As a follow on exercise check that this statement is true. - -## Example Request - -~~~~~~~~ -> PUT /todos/3 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 32 - -| { -| "title": "partial update for title" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sat, 06 Feb 2021 12:08:58 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "id": 3, - "title": "partial update for title", - "doneStatus": false, - "description": "" -} -``` - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-400-no-amend-id.md b/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-400-no-amend-id.md deleted file mode 100644 index 38a36342..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-400-no-amend-id.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -date: 2025-01-01T14:24:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - PUT todos/id 400 no amend id -seo_title: Solution: PUT todos/id 400 no amend id | API Challenges -description: How to solve API challenge PUT todos/id 400 to fail updating a todo due to mismatched ids. -seo_description: Use this walkthrough to solve PUT todos/id 400 no amend id with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/delete/delete-todos-id-200 -schema_howto_steps: Create a PUT request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 400 -showads: true ---- - -# How to complete the challenge `PUT /todos/id (400) no amend id` - -How to use a PUT request to trigger validation errors when attempting to update a todo id. - -PUT request updates are idempotent so should generate the same response each time. But the id is auto generated and cannot be amended. - -## PUT /todos/id (400) no amend id - -> Issue a PUT request to fail updating a todo id - -- `PUT` request can update a todo if the provided `id` exists `/todos/id` end point - - e.g. `PUT /todos/3` for a todo with `id==3` -- `400` is an failure code, in this case it means the request was invalid -- The body of the message should be a `json` or `xml` partial set of `todo` details, -- and the `json` or `xml` should be defined in the `content-type` header -- the id must be included in the payload and changed to try and amend the saved todo id - - -## Basic Instructions - -- Issue a `PUT` request to end point "/todos/id" - - where `id` is replaced with the id of an existing todo - - if you don't know any then a `GET /todos` would show a list of todos - - `{{}}/todos/id` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have enough data to trigger an amend and try to change the id e.g. `title` and `id` -- id in payload should differ from the URL e.g. if "/todos/3" then the payload below would trigger the issue - -```json -{ - "id": 4, - "title": "updated title" -} -``` -- The id included in the payload should not be the same as the id of the url, we are trying to amend the id, but the id is auto generated. -- The response status code should be `400` when all the request is invalid. -- The body of the response will be a JSON showing the error message. - -```json -{ - "errorMessages": [ - "Can not amend id from 3 to 4" - ] -} -``` - -NOTE: if you haven't read the documentation and don't know what format to use then issue a GET request for a single entity and the payload format for the `POST` is likely to be pretty close. - -NOTE: because you add an id to the payload you risk triggering an error validation if the id in the payload is different from the id in the URL. - -## Example Request - -~~~~~~~~ -> PUT /todos/3 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 32 - -| { -| "id": 4, -| "title": "updated title" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 200 OK -< Connection: close -< Date: Sat, 06 Feb 2021 12:08:58 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "errorMessages": [ - "Can not amend id from 3 to 4" - ] -} -``` - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-400-no-title.md b/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-400-no-title.md deleted file mode 100644 index 5bdbecc2..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/put-update/put-todos-id-400-no-title.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -date: 2025-01-01T12:53:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - PUT todos/id 400 no title -seo_title: Solution: PUT todos/id 400 no title | API Challenges -description: How to solve API challenge PUT todos/id 400 to fail validation due to no title in the payload. -seo_description: Use this walkthrough to solve PUT todos/id 400 no title with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/put-update/put-todos-id-400-no-amend-id -schema_howto_steps: Create a PUT request to /todos/{id}||Include X-CHALLENGER so the challenge is tracked in your current session||Send a valid JSON payload that matches the field and content constraints||Send the request and verify the response status is 400 -showads: true ---- - -# How to complete the challenge `PUT /todos/id (400) no title` - -How to use a PUT request to trigger validation errors during updating of a todo item in the application using a partial payload. - -## PUT /todos/id (400) no title - -> Issue a PUT request to fail when trying to update a todo using a partial payload that does not include a title - -- `PUT` request will update a todo if the provided `id` exists `/todos/id` end point - - e.g. `PUT /todos/3` for a todo with `id==3` -- `400` is an failure code, in this case it means the request fails validation -- The body of the message should be a `json` or `xml` partial set of `todo` details, -- and the `json` or `xml` should be defined in the `content-type` header -- the id does not need to be included in the payload, but if it is then it should match the id in the url - - -## Basic Instructions - -- Issue a `PUT` request to end point "/todos/id" - - where `id` is replaced with the id of an existing todo - - if you don't know any then a `GET /todos` would show a list of todos - - `{{}}/todos/id` -- The request should have an `X-CHALLENGER` header to track challenge completion -- The `content-type` in the message should be `application/json` because we are sending a JSON payload -- The Payload should have a partial set of todo details. The title should not be included e.g. - -```json -{ - "description": "partial update for description" -} -``` -- Title is a mandatory field, without a default value so this will request fail validation -- Any id included in the payload should be the same as the id of the url because we cannot update the id, the id is auto generated. -- The response status code should be `400` when the details are invalid. -- The body of the response will a JSON showing the error message. - -```json -{ - "errorMessages": [ - "title : field is mandatory" - ] -} -``` - -NOTE: if you haven't read the documentation and don't know what format to use then issue a GET request for a single entity and the payload format for the `POST` is likely to be pretty close. - -NOTE: because you add an id to the payload you risk triggering an error validation if the id in the payload is different from the id in the URL. - - -## Example Request - -~~~~~~~~ -> PUT /todos/3 HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Content-Type: application/json -> Accept: */* -> Content-Length: 32 - -| { -| "description": "partial update for description" -| } -~~~~~~~~ - -## Example Response - -~~~~~~~~ -< HTTP/1.1 400 Bad Request -< Connection: close -< Date: Sat, 06 Feb 2021 12:08:58 GMT -< Content-Type: application/json -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -Returned body: - -```json -{ - "errorMessages": [ - "title : field is mandatory" - ] -} -``` - - - - - diff --git a/challenger/src/main/resources/content/apichallenges/solutions/status-codes/status-codes-405-500-501-204.md b/challenger/src/main/resources/content/apichallenges/solutions/status-codes/status-codes-405-500-501-204.md deleted file mode 100644 index 7ee3830f..00000000 --- a/challenger/src/main/resources/content/apichallenges/solutions/status-codes/status-codes-405-500-501-204.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -date: 2021-07-18T08:15:00Z -lastmod: 2026-02-18 -title: API Challenges Solution For - Status codes 405, 500, 501, 204 -seo_title: Solution Status code 405, 500, 501, 204 | API Challenges -description: How to solve API challenges that trigger a variety of status codes 405, 500, 501 and 204. -seo_description: Use this walkthrough to solve Status codes 405, 500, 501, with request setup, key headers, and expected status codes so you can complete the challenge. -next_challenge: /apichallenges/solutions/method-override/all-method-overrides -schema_howto_steps: Send requests to /heartbeat using the methods required by each status code challenge||Include X-CHALLENGER on every request so challenge progress is recorded||Verify 405, 500, and 501 responses for the matching unsupported or error methods||Complete the 204 challenge and confirm the no-content success response||Check challenge status to confirm all target status-code challenges are complete -showads: true ---- - -# How to complete the status code challenges - -We often want to use 'status codes' as a coverage indicator, e.g. if we should probably never receive a 500 range status code from the application. -The API Challenges has specific endpoints to return status codes 405, 500, 501 and 204. - -## Status Code Challenges - -These challenges were all so similar and fast to complete that we cover the solutions for them all together. They are all variants on existing requests and all use the same endpoint `/heartbeat` - -## Challenge 25 DELETE not allowed - -> Issue a DELETE request on the `/heartbeat` end point and receive 405 (Method Not Allowed) - -- `DELETE` request means use the HTTP Verb DELETE i.e. instead of GET, or POST - - e.g. `DELETE /heartbeat` sends to the heartbeat endpoint -- ` receive 405 (Method Not Allowed)` means that the status code in the response will be 405 -- add the `X-CHALLENGER` header to track progress - -## Challenge 26 Internal Server Error - -> Issue a PATCH request on the `/heartbeat` end point and receive 500 (internal server error) - -- `PATCH` request means use the HTTP Verb PATCH i.e. instead of GET, or POST - - e.g. `PATCH /heartbeat` sends to the heartbeat endpoint -- Patch is like POST, so we should really add a body, but in this case we don't need to... perhaps that's what causes the 500 error -- ` receive 500 (Method Not Allowed)` means that the status code in the response will be `500` -- add the `X-CHALLENGER` header to track progress - -## Challenge 27 TRACE Not Implemented - -> Issue a TRACE request on the `/heartbeat` end point and receive 501 (Not Implemented) - -- `TRACE` request means use the HTTP Verb TRACE i.e. instead of GET, or POST - - e.g. `TRACE /heartbeat` sends to the heartbeat endpoint -- ` receive 501 (Method Not Implemented)` means that the status code in the response will be `501` -- add the `X-CHALLENGER` header to track progress - -## Challenge 28 GET 204 - -> Issue a GET request on the `/heartbeat` end point and receive 204 when server is running - -- `GET` request means use the HTTP Verb GET - - e.g. `GET /heartbeat` sends to the heartbeat endpoint -- ` receive 204` means that the status code in the response will be `204` -- add the `X-CHALLENGER` header to track progress - - - -## Basic Instructions - -All of the requests are variants of each other, just change the verb, so once you have completed one challenge the other challenges are simple. - -All the requests are on the `/heartbeat` end point. Sometimes APIs have an endpoit used for monitoring to check that they are still functioning, but which really don't do anything else. They are requests that are fast and easy to call, but don't have any side-effects on the server. They are often automated with a CURL command in a script and are very similiar to the concept of 'ping' to see if a server is alive. - -- Create a new request for the `/heartbeat` end point - - if running locally that endpoint would be - - `{{}}/heartbeat` -- The default values for the request should be fine -- none of the requests need a payload -- The request should have an `X-CHALLENGER` header to track challenge completion - -## To complete challenge 25 use a DELETE verb - -- Set the verb to `DELETE` and issue the request. -- Check the response for status code (405) -- The request should have an `X-CHALLENGER` header to track challenge completion - -Status code 405 means that the method is not allowed, you might want to use and 'OPTIONS' request and see what verbs are allowed. - -Request: - -~~~~~~~~ -> DELETE /heartbeat HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -Response: - -~~~~~~~~ -< HTTP/1.1 405 Method Not Allowed -< Connection: close -< Date: Sun, 18 Jul 2021 10:03:24 GMT -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Content-Type: text/html;charset=utf-8 -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - - -## To complete challenge 26 use a PATCH verb - -- Set the verb to `PATCH` and issue the request. -- Check the response for status code (500) -- The request should have an `X-CHALLENGER` header to track challenge completion - -PATCH requests are a little unusual in that there is no standard for the body of the message, but like most amendment verbs a body is usually required. Since the heartbeat can't be amended, perhaps this is why the server throws an internal server error? It is often worth sending every verb to every end point just to make sure there is no server error. Because a server error usually means that the server is trying to process the request, which might mean there is code being exercised which shouldn't so it might be possible to exploit this request in more detail and possibly a security error. - -Response: - -~~~~~~~~ -> PATCH /heartbeat HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -> Content-Length: 0 -~~~~~~~~ - -Request: - -~~~~~~~~ -< HTTP/1.1 500 Server Error -< Connection: close -< Date: Sun, 18 Jul 2021 10:04:59 GMT -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Content-Type: text/html;charset=utf-8 -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## To complete challenge 27 use a TRACE verb - -- Set the verb to `TRACE` and issue the request. -- Trace is not a common verb so some tools may not implement it, in Insomnia you need to use the `Custom Method` to send it. -- Check the response for status code (501) NOT Implemented -- The request should have an `X-CHALLENGER` header to track challenge completion - -Any verb which is not implemented, but listed as valid in the OPTIONS should return a 501 response. If it is not allowed and not listed in Options then it should return a 405 status code. - -Request: - -~~~~~~~~ -> TRACE /heartbeat HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -Response: - -~~~~~~~~ -< HTTP/1.1 501 Not Implemented -< Connection: close -< Date: Sun, 18 Jul 2021 10:07:18 GMT -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Content-Type: text/html;charset=utf-8 -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## To complete challenge 28 use a GET verb - -- Set the verb to `GET` and issue the request. -- Check the response for status code 204 -- The request should have an `X-CHALLENGER` header to track challenge completion - -204 means that the request completed successfully but there is no other information to return so the body of the response should be empty. - -Request: - -~~~~~~~~ -> GET /heartbeat HTTP/1.1 -> Host: {{}} -> User-Agent: rest-client -> X-CHALLENGER: x-challenger-guid -> Accept: */* -~~~~~~~~ - -Response: - -~~~~~~~~ -< HTTP/1.1 204 No Content -< Content-Length: 0 -< Connection: keep-alive -< Date: Sun, 18 Jul 2021 10:08:00 GMT -< Access-Control-Allow-Origin: * -< Access-Control-Allow-Headers: * -< X-Challenger: x-challenger-guid -< Content-Type: text/html;charset=utf-8 -< Server: Jetty(9.4.z-SNAPSHOT) -< Via: 1.1 vegur -~~~~~~~~ - -## Extras - -- try OPTIONS on the `/heartbeat` end point to see if the functionality above matches the response from Options. Check the Allow header in the response to see which verbs are supposed to be supported. - - -## Overview Video - -{{}} - -[Patreon ad free version](https://www.patreon.com/posts/53822534) - - - diff --git a/challenger/src/main/resources/content/author/alan-richardson.md b/challenger/src/main/resources/content/author/alan-richardson.md deleted file mode 100644 index ae34593d..00000000 --- a/challenger/src/main/resources/content/author/alan-richardson.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: About Alan Richardson -seo_title: Alan Richardson Author Profile and API Testing Credentials -description: Learn about Alan Richardson, creator of API Challenges, including his API testing background, books, training, and practical engineering focus. -seo_description: Meet Alan Richardson, creator of API Challenges, and review his software testing, API automation, and training credentials to build trust in this guidance. -lastmod: 2026-04-07 -schema_type: ProfilePage -schema_howto_enabled: false ---- - -# About Alan Richardson - -Alan Richardson created API Challenges to help people learn HTTP, REST, and API testing through practical exercises and clear walkthroughs. - -He is a software testing and development consultant, trainer, and author with many years of hands-on experience designing and testing web and API systems. His work focuses on teaching realistic skills you can apply immediately in automation, exploratory testing, and debugging. - -Alan is the author of *Automating and Testing a REST API* and has published tutorials, talks, and videos that support testers, developers, and engineering teams at different skill levels. - -## Credentials and Links - -- Role: Software Testing and Development Consultant -- Main site: [EvilTester.com](https://www.eviltester.com/) -- Detailed about page: [About Alan Richardson](https://www.eviltester.com/page/about/) -- LinkedIn: [linkedin.com/in/eviltester](https://www.linkedin.com/in/eviltester/) -- GitHub: [github.com/eviltester](https://github.com/eviltester) -- YouTube: [youtube.com/eviltester](https://youtube.com/eviltester) - -If you are learning with API Challenges, this page provides author context so you can better evaluate the experience and advice behind the content. diff --git a/challenger/src/main/resources/content/changes.md b/challenger/src/main/resources/content/changes.md deleted file mode 100644 index d2a9eeec..00000000 --- a/challenger/src/main/resources/content/changes.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: API Challenges Change Log -seo_title: Change Log Guide for API Testing | API Challenges Guide -description: Change log for the API Challenges -lastmod: 2026-04-07 -seo_description: Track API Challenges updates, including new guides, improvements, and feature changes that help you learn API testing more effectively over time. -showads: true ---- - -# Change Log - -## 2026/04/07 - Author page and formatting - -- Added [Author page](/author/alan-richardson) -- Various page formatting tweaks and minor content edits - -## 2025/04/24 - Heavyweight Desktop Client Tool Reviews - -- Added detailed reviews of [Postman](/tools/clients/postman), [Katalon](/tools/clients/katalon), [SoapUI](/tools/clients/soapui), [Insomnia](/tools/clients/insomnia) -- Amended CSS styling on side menu - -## 2025/04/22 - Lightweight Desktop Client Tool Reviews - -- Created a page of summary reviews for all the evaluated clients [Summary Reviews](/tools/clients/summary-reviews) -- Added detailed reviews of [Httpie](/tools/clients/httpie), [Kreya](/tools/clients/kreya), [Milkman](/tools/clients/milkman), [Yaak](/tools/clients/yaak) -- Added LinkedinIn 'contact' in the footer - -## 2025/04/20 - Expanded API Clients Page - -Updated the [API Clients Page](/tools/clients): - -- Added a link to [Open Source APi Clients list](https://github.com/stepci/awesome-api-clients) - -## 2025/04/19 - Bruno Client Demo - -- Added video overview of the [Bruno Client](/tools/clients/bruno) - -## 2025/04/13 - Simple API Overview - -- Added video of [Simple API](/practice-modes/simpleapi), showing how to use it with Bruno -- Added page for [Bruno](/tools/clients/bruno) -- Added page for [cURL](/tools/clients/curl) - -## 2025/03/30 - added Toolshop - -- Added page for [Toolshop](/practice-sites/apps/toolshop). The Toolshop is a UI and API with a range of API calls and . - - -## 2025/03/02 - added RestListicator - -- Added page for [RestListicator](/practice-sites/apps/restlisticator) showing how to run, and sample exercises to get started. The RestListicator API is a simple (basic authentication) CRUD API. - - -## 2025/02/22 - added pages for apps: Device Registry - -- Added page for [Device Registry API](/practice-sites/apps/deviceregistry) showing how to run, and sample exercises to get started. The Device Registry API is a simple (basic authentication) CRUD API with Open API documentation and a Swagger UI. - - -## 2025/02/16 - added pages for apps: Tracks, Best Buy, FX Trade Hub - -- Added page for [Best Buy API Playground](/practice-sites/apps/bestbuy) showing how to run, and sample exercises to get started. The Best BUY API Playground is a full CRUD API with Open API documentation and a Swagger UI. The application also has a lot of data available when it starts. -- Added page for [FX Trade Hub](/practice-sites/apps/fxtradehub) showing how to run, and sample exercises to get started. The FX Trade Hub is a simple API with a Swagger UI. Has a lot of default data installed and is suitable for full CRUD operation testing. -- Added page for [tracks](/practice-sites/apps/tracks) showing how to run, and sample exercises to get started. Tracks offers a Web App UI and a REST API and was the case study application used in my book [Automatinc and Testing a REST API](https://www.eviltester.com/page/books/automating-testing-api-casestudy/) -- Removed [TODO API Sample](https://github.com/g33klady/TodoApiSample) from list of practice apps because during testing in docker the api failed to work diff --git a/challenger/src/main/resources/content/gui/multiuser.md b/challenger/src/main/resources/content/gui/multiuser.md deleted file mode 100644 index 288255a5..00000000 --- a/challenger/src/main/resources/content/gui/multiuser.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: API Challenges Multi-User Instructions -seo_title: Multi-User Instructions | API Challenges Guide -description: How to use the API Challenges in multi-user mode to track your progress through the gamified API Testing learning exercises. -lastmod: 2026-02-18 -seo_description: Follow this API Challenges Multi-User guide to set up your workflow correctly, avoid common pitfalls, and keep steady progress while completing API. ---- - -# Multi-User Help - -Getting started with the REST API Challenges can be slightly harder in multi-user mode because you need to tell the API, who you are, so that it can track your individual progress against the challenges. - -You don't need to do any of this if you download the application and run it in single user mode. - -## Video Explanation - -{{}} - - -## Create a Challenger Session - -The first thing we have to do is Create a Challenger Session. - -And we do that by issuing a `POST` request against the `/challenger` end point. - -If it worked then you should receive a `201` `Created` response. With no body. But with a bunch of headers. - -e.g. - -~~~~~~~~ -Date: Tue, 21 Jul 2020 11:05:21 GMT -X-CHALLENGER: 9a32ef9f-0ebd-4b30-b975-314460bfd1d1 -Location: /gui/challenges/9a32ef9f-0ebd-4b30-b975-314460bfd1d1 -Content-Type: text/html;charset=utf-8 -Transfer-Encoding: chunked -Server: Jetty(9.4.12.v20180830) -~~~~~~~~ - -When you look at the response to this message you should see an `X-CHALLENGER` header with a GUID. - -To track your progress against challenges. - -Every request you send to the API should have an `X-CHALLENGER` header with the guid you received. - -e.g. - -~~~~~~~~ -X-CHALLENGER: 9a32ef9f-0ebd-4b30-b975-314460bfd1d1 -~~~~~~~~ - -Also, note in the original response there was a `Location` header. - -If you visit that url for this application then you should see the challenges for your `Challenger` session. - -e.g. - -~~~~~~~~ -{{}}/gui/challenges/9a32ef9f-0ebd-4b30-b975-314460bfd1d1 -~~~~~~~~ - -Then you should see the status of your challenges. - ---- - -### How to Play Summary - -In multi user mode i.e. when using the `apichallenges.eviltester.com` installation, you will need to create an `X-CHALLENGER` session. - -Sessions can be stored in localstorage in the browser, or on your local machine (using the API) so you can revisit it and continue your challenges later. - -To start a session: - -- issue a `POST` request to `{{}}/challenges` -- the response status code will be `201` if successful. -- The response will contain an `X-CHALLENGER` header with a unique session value -- Add this header to every request you make on the API, and challenge completion status will be stored against this session. - -To view the status of your session: - -- issue a `GET` request to `{{}}/challenges` -- the response will contain all the challenges and the status of the challenges for your session - -To view the status of your session in the GUI: - -- visit `{{}}/gui/challenges/[unique-session-value]` -- where `[unique-session-value]` is the guid that you were issued as the `X-CHALLENGER` header - - -## 10 Minutes of data - -In multi-user mode the data for the application is reset every 10 minutes, this means that we delete your session data from memory if your session has not been used. Issuing an API request with an X-CHALLENGER header, will keep your data alive in memory. - -## Session Persistence - -Each challenger session progress is stored on the cloud. The sessions are deleted periodically if they are not used. - -You can restore a session's progress, by using the same `X-CHALLENGER` header and if available it will be loaded back into the system and you can continue your challenges. - -If you view the challenges page: - -- [{{}}/gui/challenges](/gui/challenges) - -Then your challenger guid should have been stored in the browser local storage, to make it easier for you to find and restart your session. - -Even though sessions are stored when the application is not running, so it is still worth making notes about the challenges you have completed so you can track your progress and complete them in various ways, with different tools and by automating. - diff --git a/challenger/src/main/resources/content/learning.md b/challenger/src/main/resources/content/learning.md deleted file mode 100644 index 874efdc3..00000000 --- a/challenger/src/main/resources/content/learning.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Learning Utilities and Resources -seo_title: Learning Utilities and Resources | API Challenges -description: A list of HTTP REST API learning tutorials and recommended books and practice sites for API Testing. -lastmod: 2026-02-18 -seo_description: Start learning API testing with curated tutorials, practical resources, and a clear path from fundamentals to confident hands-on execution. -showads: true ---- - -# Learning API Testing - -## Tutorial And Reference Content - -Read through the Tutorial and Reference content to understand the basics of: - -- [Web Applications](/tutorials/web-basics) -- [HTTP Basics](/tutorials/http-basics) -- [HTTP Verbs and Methods](/tutorials/http-verbs) -- [REST API Basics](/tutorials/rest-api-basics) -- [Testing APIs](/tutorials/testing-apis) -- [Summary](/tutorials/summary) - -## Download Some Tools - -- [HTTP/REST Clients](/tools/clients) -- [HTTP Proxies](/tools/proxies) - -## Experiment with the API Challenges - -- [Read the API Challenges Documentation](/docs) -- [Try the Challenges](/gui/challenges) -- [Read the Solutions if you get stuck](/apichallenges/solutions) - -## Experiment with Other APIs - -We have an extensive list of [Practice Sites](/practice-sites), which includes tips on how to use them. - -We are in the process of creating longer tutorial guides with exercises for each of the listed sites. - -- [Swapi](/practice-sites/swapi) - -## Experiment with our HTTP Request Mirror Mode - -The Mirror mode is a good way to test out your tooling and see the details of your requests without using a proxy. - -[Learn About the Mirror Mode Here](/practice-modes/mirror) - -## Experiment with our API Simulation Mode - -The API has a simulation mode, it uses hard coded data in responses, but tries to mimic some conditions. - -e.g. it expects you to post a specific JSON payload or XML payload and responds 'as if' you sent it. But... it also checks if you sent valid json, or valid xml, and responds based on your headers e.g. returning XML if you ask for it. - -The simulator is stateless and does not track your usage, making it deterministic for multiple users. - -The simulator is a good place to get started because it will respond nicely... unless you mess up the request syntax. - -[Learn About the Simulator Here](/practice-modes/simulation) - -## Automating and Testing a REST API Book - -Alan Richardson wrote a book [Automating and Testing a REST API](https://www.eviltester.com/page/books/automating-testing-api-casestudy/) - -Buying the book helps support this web site and application. - -## Challenge Tutorials - -The Challenges have solution tutorials. - -These are available: - -- bundled into the app [API Challenge Solutions](/apichallenges/solutions) -- and on the [EvilTester.com](https://www.eviltester.com/categories/api-challenges/) blog. - -[Read the API Challenge Solutions](/apichallenges/solutions) - -## Open Source Workshops - -All the material from Alan Richardson's REST API Training workshops have been released to Github. - -[Find the Workshop Material Here](https://www.eviltester.com/post/rest-api-workshops/) diff --git a/challenger/src/main/resources/content/practice-modes/mirror.md b/challenger/src/main/resources/content/practice-modes/mirror.md deleted file mode 100644 index caf9ae17..00000000 --- a/challenger/src/main/resources/content/practice-modes/mirror.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: API Challenges Mirror Mode -seo_title: HTTP Mirror Mode | API Challenges Practice Mode -description: See the raw HTTP request sent to the API server and check if your REST API tool sends what you expect. -lastmod: 2026-02-18 -seo_description: Use API Challenges Mirror Mode to practice safely, understand request-response behavior, and build confidence with guided exercises before advanced testing. ---- - -The API Challenges Mirror Mode shows you the HTTP request that you sent to the API server, an easy way to see if your REST API tool is performing as instructed or did it amend your request in ways you didn't expect? Did it add extra headers, or worse, remove headers that you wanted to include? - -# Mirror Mode - -## Overview of Mirror Mode - -{{}} - -[Patreon ad free video](https://www.patreon.com/posts/54382928) - -## About Mirror Mode - -The API has a mirror mode, this allows you to experiment with different verbs and configurations. - -You will see, in your API tool, a response showing you the details of the request that you sent. - -There are two mirror end points: - -- `mirror/request` -- `mirror/raw` - -The `mirror/request` end point will try to honour the `accept:` header in the response, so if you ask for `application/json` then the response will be json format. - -The `mirror/raw` end point will always send the request back as raw text format. - -This endpoint can be very useful for seeing what your HTTP Rest Client is sending to the server. You can spot any additional http headers that the client has added and see if the HTTP Client has combined any headers, or dropped any headers. - -## Accessing The Mirror Mode - -To access the mirror mode on the public cloud make requests to: - -- {{}}/mirror/request -- {{}}/mirror/raw - - - -## Request EndPoint - -e.g. to use the request endpoint: - -``` -GET {{}}/mirror/request -``` - -Will return 200... everything (almost) returns a 200. - -The `mirror/request` endpoint will use the `Accept` header to format the response. - -If you want the response in XML or JSON then add the relevant `Accept` header. - -e.g. - -``` -GET /mirror/request HTTP/1.1 -Accept: application/json -Content-Length: 0 -Host: localhost:4567 -``` - -Would return the response as json - -``` -HTTP/1.1 200 OK -Date: Sat, 17 Feb 2024 13:09:34 GMT -Content-Type: application/json -access-control-allow-origin: * -x-challenger: x-challenger-guid -access-control-allow-headers: * -Server: Jetty(9.4.12.v20180830) -Content-Length: 320 - -{ -"details":"GET {{}}/mirror/request -\n\nQuery Params\n==..." -} -``` - - -## Raw EndPoint - -e.g. to use on the public cloud version of apichallenges: - -``` -GET {{}}/mirror/raw -``` - -``` -GET /mirror/raw HTTP/1.1 -Accept: application/json -Content-Length: 0 -Host: localhost:4567 -``` - -Will return 200. - -The `mirror/raw` endpoint will not use the `Accept` header to format the response and will always return a `text` representation. - -``` -HTTP/1.1 200 OK -Date: Sat, 17 Feb 2024 13:13:58 GMT -Content-Type: text/plain -access-control-allow-origin: * -x-challenger: x-challenger-guid -access-control-allow-headers: * -Server: Jetty(9.4.12.v20180830) -Content-Length: 276 - -GET {{}}/mirror/raw - -Query Params -============ - -IP -======= -127.0.0.1 - -Raw Headers -======= -Accept: application/json -Content-Length: 2 -Host: {{}} - -Processed Headers -======= -host: localhost:4567 -content-length: 2 -accept: application/json - -Body -==== - - -``` - -## OPTIONS, HEAD - -Only `options` and `head` respond differently... because `options` and `head` should respond differently. - -Useful for getting started and getting used to your tooling. - - -## Why is this Mirror Mode Useful? - -The mirror mode is another way of seeing the 'true' request received. - -You can configure most API tools to use a Proxy like [BurpSuite](https://portswigger.net/burp) or [OwaspZAP](https://www.zaproxy.org/) and you will see the actual request that the tool sends. - -You can also use the Insomnia Timeline to see the request. - -In Postman you can use the Postman Console to see the requests. - -The Mirror Mode shows you the request received by the server. When run on Localhost there are no intermediate systems so you can see what the tooling sends in the logs. - -When run on [apichallenges.eviltester.com](https://apichallenges.eviltester.com/practice-modes/mirror) you see that the Cloud environment adds additional headers in to the request. - -Additionally the REST Client we use may add or amend headers. - -Very often we are not aware of this level of amendment when testing and may not test for this. - -The Mirror mode makes it clear that there are multiple systems involved in issuing a request and they can all pose a risk to the system or our testing. e.g. some REST Clients will not send duplicate headers: some will combine headers, some will pick the first (or last) header. - -## Swagger OpenAPI File - -You can download a simple Swagger [OpenAPI File for mirror mode](/mirror/docs/swagger). diff --git a/challenger/src/main/resources/content/practice-modes/simpleapi-openapi.md b/challenger/src/main/resources/content/practice-modes/simpleapi-openapi.md deleted file mode 100644 index d0d2f341..00000000 --- a/challenger/src/main/resources/content/practice-modes/simpleapi-openapi.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Simple API Open API Swagger File -seo_title: Simple API OpenAPI Swagger | API Practice Mode -description: Download the Open API files for the Simple API. -lastmod: 2026-02-18 -seo_description: Use Simple API Open API Swagger to practice safely, understand request-response behavior, and build confidence with guided exercises before advanced. -showads: true ---- - -# Simple API Open API Files - -Download an OpenAPI/Swagger JSON file to use in your REST Client. - -## File Download Links - -Two OpenAPI/Swagger formatted files are available. - -- [Download Normal OpenAPI File](/simpleapi/docs/swagger) - to use API like a User -- [Download Permissive OpenAPI File](/simpleapi/docs/swagger?permissive) - less validation, more suitable for testing - -## Normal OpenAPI File Explained - -The Normal OpenAPI File is intended for use as though you were a user. - -It only lists endpoints that are valid to use, and has additional validation on the URL Parameters that you can enter. - -When this type of file is loaded into a Swagger UI Generation application it makes it easy to USE the API but makes it harder to TEST the API. - -- [Download Normal OpenAPI File](/simpleapi/docs/swagger) - -## Permissive OpenAPI File Explained - -The Permissive OpenAPI File is intended for testing. - -It lists all the end points with more Verbs i.e. even verbs that the API defines as not available. - -The parameters are also possible to send as empty and type validation is not performed on the parameter values. This makes it possible to use Swagger UI applications to test more extreme situations. - -- [Download Permissive OpenAPI File](/simpleapi/docs/swagger?permissive) - diff --git a/challenger/src/main/resources/content/practice-modes/simpleapi.md b/challenger/src/main/resources/content/practice-modes/simpleapi.md deleted file mode 100644 index 6c91750f..00000000 --- a/challenger/src/main/resources/content/practice-modes/simpleapi.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: API Challenges Simple API -seo_title: Simple API Practice Mode | API Challenges Practice Mode -description: The Simple API is a multi-user REST API that you can use to practice testing without any authentication. -lastmod: 2026-02-18 -seo_description: Use API Challenges Simple API to practice safely, understand request-response behavior, and build confidence with guided exercises before advanced testing. ---- - - - -# Simple API - -The API Challenges Simple API is an easy-to-use API where you can GET, DELETE, PUT and POST without any authentication. - -## Overview of Simple API - -{{}} - - -[Patreon ad free video](https://www.patreon.com/posts/126496992) - -## About Simple API - -To help you get started with API testing and practice using your tools, we have created the Simple API. - -The Simple API has a single end point `/simpleapi/items` and you can `GET`, `DELETE`, `PUT` and `POST` without -requiring any authentication or authorization, making Simple API a recommended first step in your API Testing learning journey. - -Data will refresh automatically when low, and there is a limit to the number of items that can be added. - -Because the API has no unstructured text fields e.g. `description`, there is no way to add any potentially offensive -or personal information. - -To create a new item you need to add a unique `ISBN`. We have added an endpoint to generate a random ISBN `/simpleapi/randomisbn`. - -Or you can click the button below and copy and paste the value into your APi call. - -{{}} - -## Why did we create this? - -We noticed that most APIs, including our API Challenges, require some sort of authentication to use the full capabilities -of the API, and we wanted to make learning easier. - -We wanted to put no barriers between yourself and your learning how to use APIs. - -The [documentation](/simpleapi/docs) explains the data formats and the validations. Additionally, you can download an -Open API Swagger File to load into your API tool of choice and start testing straight away. - -## Swagger OpenAPI File - -You can download a Simple API Swagger [OpenAPI File for Simple Api](/practice-modes/simpleapi-openapi). diff --git a/challenger/src/main/resources/content/practice-modes/simulation.md b/challenger/src/main/resources/content/practice-modes/simulation.md deleted file mode 100644 index 25e9767e..00000000 --- a/challenger/src/main/resources/content/practice-modes/simulation.md +++ /dev/null @@ -1,341 +0,0 @@ ---- -title: API Challenges Simulation Mode -seo_title: Simulation Mode | API Challenges Practice Mode -description: A simulated API tutorial - follow the guided instructions and learn how to use your API Tool without any side-effects or risk. -lastmod: 2026-02-18 -seo_description: Use API Challenges Simulation to practice safely, understand request-response behavior, and build confidence with guided exercises before advanced testing. ---- - -# Simulation Mode - -The API has a simulation mode, it uses hard coded data in responses, but tries to mimic some conditions. - -## Overview of Simulation Mode - -{{}} - -[Patreon ad free video](https://www.patreon.com/posts/54383023) - -## About Simulation Mode - -The API simulation mode, uses hard coded data for responses, but tries to mimic conditions e.g. JSON vs XML payloads. - -The simulator is stateless and does not track your usage, making it deterministic for multiple users. Which means: - -* Entities created do not show in the 'entities' call, but can be retrieved by a 'GET' -* Entities deleted do not show in the 'entities' and respond to a 404, but the delete for them will return a 200... you can only delete 'specific' entities, other entities will respond with a forbidden request. -* There are 'inconsistencies' but they are logical based on the needs of a stateless simulator. Use the actual API that underpins the challenges or the Simple API if you want a 'real' API. - -## How to Use - -Work through the requests in sequence to achieve a fairly logical interaction. - -- try different tooling, the only difference then will be the tool because the API is fairly forgiving and no-one else can interfere with your practice. Use it to learn the tools. -- try different automated execution approaches. The API is simple, there are only a few requests and sequences, so use it to learn a new automated execution tool. It won't change as you are automating, if something goes wrong then it is most likely some nuance of the tool. - -This simulator is designed to make starting with API testing as simple as possible. - -## Suggested Request Sequence - -Try the verbs and payloads listed below as a way of making sure your tooling is setup and you understand the absolute basics about API usage and Testing. - -### Step 1 - GET all the Entities - -Use your API Client to issue a `GET` request on the `/sim/entities` end point. - -- `GET` verb will retrieve information from the server. -- The endpoint is a top level endpoint for a domain entity. -- Most REST APIs use a plural endpoint like `entities` or `todos` - -``` -GET {{}}/sim/entities -``` - -You should see a `200` status code, which means that the request was a success. - -In the response you should see a JSON payload with 10 items. - -* Entities 1-10 -* Get all the entities in the simulator - -### Step 2 - GET a single Entity - -Issue a GET request to `/sim/entities/1` - -- by adding `/1` to the endpoint we are requesting the entity with `id` equal to `1` - -``` -GET {{}}/sim/entities/1 -``` - -* The response should have a status code of `200`, meaning `OK`, the API found the item -* The response should Return the details of entity number 1 - -For extra practice try and `GET` other entities (2-8) listed in the `/sim/entities` response - -> NOTE: Entities 1-8 are suitable for getting with a 200 response. 9 and 10 are reserved for later simulated scenarios so if you GET them then you may not get the response you are expecting - -### Step 3 - Try to GET an Entity that does not exist - -A 404 status code is returned when the API cannot find the item requested. - -There was no entity with `id` equal to `13` so if we try to `GET` that we will receive a `404` status code in the response. - -``` -GET {{}}/sim/entities/13 -``` - -* Entity does not exist, receive a 404 response -* Try other values for the `id` and see that you receive 404 responses - -### Step 4 - Use POST to create an Entity - -A `GET` request is used to retrieve information. We can use `POST` to create new items or amend existing items. - -- We need to issue a `POST` request to the `/sim/entities` end point. -- We use the top level endpoint without an `id` because we want to create a new entity (if we add an `id` then we are trying to amend a specific entity) -- We also need to add a `body` with the details of the entity we want to create - -``` -POST {{}}/sim/entities -``` - -With body: - -``` - {"name": "bob"} -``` - -> NOTE: we've tried to make the simulator easy to use so it doesn't actually matter what you add as the payload, you'll still see the response below. - -In response you will see that a new entity has been created with `id` 11: - -``` -{ - "id": 11, - "name": "bob", - "description": "" -} -``` - -The response will also have a `201` status code, which indicates that a new entity was created. - -If you look at the headers in the response you will also see a `Location` header containing the endpoint where the created item can be found. - -``` -HTTP/1.1 201 Created -Content-Type: application/json -Location: /sim/entities/11 -``` - -You can now issue a `GET` request on `/sim/entities/11` and you'll see the entity returned again. - -``` -{ - "id":11, - "name":"bob", - "description":"" -} -``` - -> NOTE: entity with id 11 does not show in the entities list because no state is stored on the server. - -### Step 5 - Use POST to Amend an Entity - -With this API you can also use `POST` to amend an entity. We'll amend entity id `10` - -Entity 10 is listed in the `/sim/entities` response as: - -``` -{ - "id":10, - "name":"entity number 10", - "description":"" -} -``` - -Issuing a `POST` on endpoint `/sim/entities/10` means that we want to amend a specific entity. - -``` -POST {{}}/sim/entities/10 -``` - -Add a payload which contains the information you want to update: - -``` - {"name": "eris"} -``` - -You will receive a `200` status code. - -* Amend an entity...note we assume you are amending to the payload above, because that is what we return. -* Will amend Entity with ID 10, to have the name `eris` - -Once you amend you can GET this item and check it has amended - -``` -GET {{}}/sim/entities/10 -``` - -``` -{ - "id": 10, - "name": "eris", - "description": "" -} -``` - -NOTE: because this is a simulator and nothing has changed on the server if you issue a GET on `/sim/entities` it will show the original name "entity number 10" - -### Step 6 - Use PUT to Amend an Entity - -You can also use `PUT` with this API to amend the entity. - -Let's assume that someone has amended the name back to the original "entity number 10", which we can see if we `GET` - -``` -GET {{}}/sim/entities -``` - -Entity 10 is listed in the `/sim/entities` response as: - -``` -{ - "id":10, - "name":"entity number 10", - "description":"" -} -``` - -So we will issue a `PUT` request to amend entity with id `10` to have the name `eris` - -``` -PUT {{}}/sim/entities/10 -``` - -With the payload in the body as: - -``` - {"name": "eris"} -``` - -`PUT` and `POST` are not always interchangeable with APIs. For each API you use, read the documentation to learn how the API works. - - -### Step 7 - DELETE an Entity - -We can use the `DELETE` HTTP verb to delete a specific item. - -The only entity we can delete is `id` equal to `9` - -``` -DELETE {{}}/sim/entities/9 -``` - -This should respond with a status code of `204` meaning `OK` successfully completed, but no additional information to show in the body of the response. - -To check that something is deleted, try to GET it and make sure that you receive a 404 response. - -``` -GET {{}}/sim/entities/9 -``` - -* if you GET id 9 then you will find it 404's (because it was deleted) - -Additional thing to try with `DELETE`: - -* DELETE an entity listed in the `/sim/entities/id` call is forbidden when id < 9 -* Try to DELETE all the entities by using `DELETE` on `/sim/entities` you should see a status `405` meaning that the HTTP verb (method) is not allowed -* Try to DELETE an entity that does not exist e.g. `/sim/entities/56` you should see a `404` status with an error message - -### STEP 8 - Find out what Verbs are allowed with OPTIONS - -APIs will often report errors if you use an HTTP Verb on an endpoint that you are not allowed to. - -To find out what Verbs, or Methods, we are allowed to use, issue an `OPTIONS` request on the endpoint. - -``` -OPTIONS {{}}/sim/entities -``` - -By looking at the `Allow` header in the response we can see that we are allowed to `GET, POST, PUT, HEAD, OPTIONS` - -If we tried to `DELETE` or `PATCH` then we should receive an appropriate status code of `405` - -Try and see: - -``` -DELETE {{}}/sim/entities -``` - -`DELETE` correctly returns a `405` response as we expected. - -And: - -``` -PATCH {{}}/sim/entities -``` - -Patch returns a `501` status, which indicates a problem with the API. No API should return a `500` range status code as it indicates a server problem: - -- `1xx` - Information response -- `2xx` - OK -- `3xx` - Redirect response (check the `Location` header) -- `4xx` - Invalid request - you've sent an incorrect message -- `5xx` - Server error - something went seriously wrong with the API - -### STEP 9 - HEAD request - -From the `OPTIONS` request we can see that we are allowed to issue `HEAD` requests. - -A `HEAD` request is like a `GET` but only the headers are returned. The headers should be the same as those returned from a `GET` request. - -Try this by issuing a `HEAD` request. - -``` -HEAD {{}}/sim/entities -``` - -And compare the response with a `GET` request: - -``` -GET {{}}/sim/entities -``` - -The headers should be the same. - -## Other Things to Try - -If you followed all the Simulator steps then you've managed to use your HTTP API Client tool to issue a range of different request types. - -If you want to explore the tool more then you could try the experiments below, or move on directly to the [Simple API](/practice-modes/simpleapi) - -* Try changing the Accept header on GET requests to `application/xml` and `application/json` -* POST/PUT an entity listed in the `/sim/entities/id` call is forbidden for id < 10 -* PATCH and TRACE should be 501 for all end endpoints -* any other `/sim/*` endpoints should respond with a 404 - - -## Automating Examples - -The Simulator is a very simple set of endpoints to automate because it doesn't matter what order the tests run and the results are idempotent so they will always be the same. - -I have created an example set of very simple Java `@Test` methods using RestAssured which automate the Simulator. - -[Simulator Automated Execution Coverage](https://github.com/eviltester/thingifier/blob/master/challengerAuto/src/test/java/uk/co/compendiumdev/simulator/SimulatorHttpTest.java) - -## Swagger OpenAPI File - -You can download a simple Swagger [OpenAPI File for simulation mode](/sim/docs/swaggercd ..). - -## Simulation Mode Walkthrough - Insomnia - -{{}} - -[Patreon ad free video](https://www.patreon.com/posts/54383155) - -## Simulation Mode Walkthrough - Postman - -{{}} - -[Patreon ad free video](https://www.patreon.com/posts/54383110) diff --git a/challenger/src/main/resources/content/practice-sites.md b/challenger/src/main/resources/content/practice-sites.md deleted file mode 100644 index a7a296dd..00000000 --- a/challenger/src/main/resources/content/practice-sites.md +++ /dev/null @@ -1,347 +0,0 @@ ---- -title: API Testing Practice Sites -seo_title: API Testing Practice Sites | API Challenges Guide -description: A list of the best API Practice sites and applications to improve your Testing and Automating. -lastmod: 2026-02-18 -seo_description: Discover API practice sites with realistic scenarios and endpoint variety so you can choose better targets and improve testing skills faster. -showads: true ---- - -# API Practice Sites - -There are many lists of 'free to use' APIs online. Most of these are not designed for practicing testing or automating. I have listed a few collation sites at the bottom of this page. - -All of the sites and applications listed here are designed for practice. This carefully curated list only presents the best applications to use to improve your testing and automating. - -You can practice against endpoints that expose meaningful behavior such as validation errors, authentication flows, status-code variation, and realistic CRUD lifecycle operations. - -You will find several categories of API practice environments: simulators, GET-only public APIs, full CRUD APIs, and applications you can run locally. - -Start with simulators to build confidence in request construction and getting used to tools, then move to persistent APIs where `POST`, `PUT`, `PATCH`, and `DELETE` save data. That transition is essential for learning test data management, session control, and cleanup strategies used in real API test projects. - -Each practice API entry is designed to support hands-on API test design. Follow the linked guides to run focused exercises, compare docs to real responses, and build stronger assertions. If your goal is API automation practice, these sites also help you build reusable request models, negative tests, and end-to-end API scenarios across multiple API styles. - - -## Online and Free to Use - -### API Simulators - -API Simulators provide end points, and simulate a functional API in that the information is hard coded or randomly generated and no create, update or delete action will persist to the server (`POST, PUT, DELETE, PATCH`). - -Simulators are a good place to start, and practice with tooling. But do not stay with them too long. Try to move on to Real APIs and applications quickly so you get the experience of controlling the data in the system through API Interaction. - -There are many API simulators so we have only listed a few of the best here. - ---- - -#### API Challenges Simulator - -[API Challenges Simulator](https://apichallenges.eviltester.com/practice-modes/simulation) - -- [follow the instructions and work through the API Challenges Simulator](/practice-modes/simulation) -- An API Simulator (i.e. it always response the same way and you can't update data on the back-end, but the system will respond as though you did. -- Follow the instructions on the linked page and issue `GET`, `POST`, `PUT` and `DELETE` requests. - ---- - -#### HTTPBin - -[httpbin.org](https://httpbin.org) - -- [read our longer write up of HttpBin with exercises to try here](/practice-sites/httpbin) -- This API can help you get used to your tools. -- It has a set of endpoints that you can call for documented outcomes. -- e.g. a `DELETE` call to `https://httpbin.org/delete/404` will return payload, using any other verb will return a `405 Method Not Allowed` status code. -- I think it is a useful exercise to work through all the API endpoints through the Swagger interface, make sure you know what they do, and then replicate the request through an API client. This should be a useful way of getting used to the functionality in your API client. - ---- - -#### JSON Placeholder - -[JSON Placeholder](https://jsonplaceholder.typicode.com/) - -- [read our longer write up of JSON Placeholder with exercises to try here](/practice-sites/jsonplaceholder) -- An API For `GET` requests. -- Acts as a simulator for Update and Delete requests -- Useful for getting used to your REST Client Tooling. -- notable for the [usage guide](https://jsonplaceholder.typicode.com/guide/) showing example usages as JavaScript that you can run from the Browser Developer Tools console. This can be a useful approach to experiment when exploring and testing a system. - ---- - -#### Fake REST API - -[Fake REST API](https://fakerestapi.azurewebsites.net/index.html) - -- [read our longer write up of Fake REST API with exercises to try here](/practice-sites/fakerestapi) -- Another simulator with a Swagger front end to experiment easily, but do make sure to use a REST Client as well. -- The `POST` and `PUT` requests don't update the server but they are validated so you can see an additional approach to JSON validation error reporting. -- A fun API to spend some time exploring. - ---- - -### GET Only APIs - -These APIs are full APIs, but they will only respond to `GET` requests. This usually also means they support `OPTIONS` and `HEAD` requests. They are typically not designed for practicing testing but they can be an easy option for getting used to your REST API Client tooling. - -Additionally they often have a lot of data so can support Query parameters for filtering, pagination and limiting data. Read the documentation for each API and experiment in your chosen REST Client. - ---- - -#### Swapi Star Wars API - -[swapi the Star Wars API](https://swapi.dev) - -- [read our longer write up of SWAPI with exercises to try here](/practice-sites/swapi) -- A `GET` based API (no updates allowed) with a variety of data end points -- All endpoints return star wars information -- The UI can help you get started but make sure to use a REST Client on the API to get used to your tooling. -- `HEAD` and `OPTIONS` are also supported - ---- - -#### Random User Me - -[Random User Me](https://randomuser.me) - -- [read our longer write up of Random User Me with exercises to try here](/practice-sites/randomuser) -- A `GET` based API for returning random user details. -- Useful for experimenting with Query parameters to filter, paginate and download -- returns a random User, supports different formats but this is controlled by a `format` URL parameter rather than an `Accept` header, which is a pity, but having different formats can help you practice with additional data formatting and parsing tooling. -- There is no Swagger spec so you can use this as an exercise in modelling the API by hand. - ---- - -#### Dummy JSON - -[dummyJSON](https://dummyjson.com/) - -- [read our longer write up of Dummy JSON with exercises to try here](/practice-sites/dummyjson) -- An API for JSON requests and responses. -- A lot of endpoints for different types of data. -- Supports filters and limits as URL Query parameters. -- Simulate Update requests (`POST, PUT, DELETE`) -- I originally classified this exclusively as a simulator but because I could craft endpoints that triggered 500 server responses I added it as a GET Only API (Update requests like PUT, POST, PATCH, DELETE are simulated) - ---- - -#### Others - -- [pokeapi.co](https://pokeapi.co) - like swapi.dev but for Pokemon data - ---- - -### Full APIs - -These are full APIs online, which means you don't have to install anything and can persist data on the server so `PUT, POST, PATCH, DELETE` are usually supported. These APIs usually have the additional step of creating an authentication key or registering a session but the instructions will usually walk you through this. - - ---- - -#### API Challenges - -[API Challenges](/gui/challenges) - -- Our API Challenges is a fully functional TODOs management application. -- Create a session by issuing a `POST` request to `https://apichallenges.eviltester.com/challenger`, you will see an `X-CHALLENGER` header in the response and if you add that header into all your requests you will be able to update data and view your solved challengs progress in the UI. -- It is not deliberately buggy so if you experience problems, you've either found a bug or still have to learn to use the HTTP Client tooling. -- There are 50+ challenges to complete which will guide you through an API testing process. -- Since it is a working API you can make up your own challenges and try whatever tests you want. - ---- - -#### GO Rest - -[Go REST](https://gorest.co.in) - -- A functional API that persists data to the server in a shared session so **do not use any private data or personal information**. -- To use the ` PUT, POST, PATCH, DELETE` request you need to request an access token by logging in using GitHub, Google or Microsoft. Then add this as a Bearer token in your request. -- It performs data validation, reporting all errors in the response making it easy to work with. -- The documentation is not automatically generated which is good. You have to read the docs, and model the API then experiment with it. -- No swagger file is provided so you create the request collection as you test. -- After reading the documentation and performing some tests I did find a difference between the docs and the implementation so I don't think the system is bug free, which makes it a test application as well as a practice application. -- Also provides a GraphQL endpoint, which I haven't experimented with. - ---- - -#### Simple API - -[Simple API](/practice-modes/simpleapi) - -- [read our longer write up of Simple API with exercises here](/practice-sites/simpleapi) -- An API for JSON and XML requests and responses. -- A simple data structure to avoid personal data. -- No Authentication required for updates. -- Update requests are supported (`POST, PUT, DELETE`). -- Two OpenAPI specification files: one for using the API, one to support testing - ---- - -#### Toolshop API - -[Toolshop API](https://api.practicesoftwaretesting.com/api/documentation) - -- [read our longer write up of Toolshop API with exercises here](/practice-sites/apps/toolshop) -- Toolshop API has a UI and an API backend. This allows you to experiment with the UI, open the network tab and see the relevant API requests, or use the API and double-check the results on screen in the UI. -- Multiple versions of the Toolshop are available - with bugs and without -- The Open API Swagger file is one of the most complete that I've seen, and is a great example of the capabilities of the format. -- Some endpoints require that you master the authentication process which requires logging in as a user, then using the returned token as an HTTP Bearer Token. -- The DELETE endpoints seemed to require logging in as an admin user. -- The Database is shared, so don't add any private information (e.g. your name and email) if you create a new user. -- The error messages returned from the server in the message are useful aids to helping you craft a valid payload. - ---- - -#### Expand Testing Notes API - -[Notes API from Expand Testing](https://practice.expandtesting.com/notes/api/api-docs/) - -- I'm not sure if the API is supposed to be deliberately buggy or not since it comes from a Testing company it might be. But keep your eyes open for inconsistencies when testing. -- It provides a swagger UI interface, but remember to go beyond that and use a REST Client. -- It is interesting to see alternative approaches in an API e.g. seeing an Options response that is human-readable (this approach is also used in Restful Booker). -- You need to 'register', then 'login', find the token and then pass this in as a `X-AUTH-TOKEN` to use the API properly. By doing this your data is saved separate from other users, which means you will find testing easier as no-one else is interfering with your test data. -- There is a [UI that uses the API](https://practice.expandtesting.com/notes/app/) so experiment with this and vew the API calls in the network tab. - ---- - -### APIs Not for First Time Use - -I found the following APIs to be buggy, either deliberately or not. So I wouldn't recommend them as your first API to practice with. Once you are confident with how APIs work, and understand the tooling, then I'd recommend testing with them. - ---- - -#### Restful Booker - -[Restful Booker](https://restful-booker.herokuapp.com/apidoc/index.html) - -- A deliberately buggy REST API. -- The bugs can make it hard to identify if you are doing something wrong, so I wouldn't start with this one, use this only after you've practiced with some more functional APIs. Otherwise you risk confusing yourself or thinking that your tools are not working correctly. -- Once you've explored a few APIs then this is worth working through. It is designed to be used in an API Training course so adding 'bugs' in the app works well in that scenario because an instructor can guide you through the issues and answer questions. - ---- - -#### Swagger Petstore - -[Swagger petstore](https://petstore.swagger.io) - -- Nothing in the documentation led me to believe that this is designed to be deliberately buggy. -- But it did seem to have many functional issues when I tried it so in that sense it is worth exploring, but if you are not used to testing APIs it might frustrate you. -- Because it is swagger based it has a GUI, but you can download the Swagger file and load it into your API Client of choice. -- Or use the Network tab to view the requests and then replicate them in a REST Client. -- It is a CRUD interface, and you can seem to make changes on the server. - ---- - -#### Automation Exercise Endpoints - -This was interesting and a bit weird so I wouldn't recommend it as a First Time API. It felt more like a 'puzzle API' since it wasn't well documented and I had to explore request formats to get it working. - -[Automation Exercise.com](https://automationexercise.com/api_list) - -- This site has a list of API End Points that you can use to practice testing. -- An interesting example of non-standard payloads and requests which made interacting with it a bit of a puzzle. -- Hard to know if it is deliberately buggy or not. But I made notes of lots of inconsistencies. It kept me entertained for an hour or so. -- Data is saved to the server so you can practice CRUD operations. - ---- - -#### Reqres - -I found Reqres a little hard to understand from the documentation. It seems as though it has a hard coded set of users `/api/users` and then a hard coded set of data that it returns from `/api/` e.g. `/api/things`. - -- [reqres](https://reqres.in) - - Supports `JSON` only - - Has hard coded data. - - Provides a Swagger [API interface and documentation](https://reqres.in/api-docs/) this can be useful to get started but doesn't cover the generic endpoints so you'll need to use a REST Client for that. - - There is also a hard coded set of 12 'things' which will respond to any endpoint e.g. `GET https://reqres/in/api/books/1` will return the hardcoded 'thing' with ID 1 (it won't be a 'book' but the API responds as though the `/api/books/` end point exists) - - Simulates `DELETE, PUT, POST, PATCH` operations. - - -## Compile and run yourself apps - -To have the most control over your practice testing session you can download an application and run it locally. All API calls would then be made to `http://localhost`. You won't experience any issues with SSL, HTTPs, Certificates. There will be no issues with using private data because it will be stored locally. - -Getting started with these applications can be a little harder than using the online versions and may require installing Java, Node or Docker. - -### Best Buy API Playground - -- Best Buy API Playground is a full API that can be run locally using Node or Docker -- Full range of CRUD operations is supported using `GET`, `POST`, `PATCH`, `DELETE` -- Large set of data is provided -- Swagger UI is available -- Open API Definition is downloadable - -[Read a full overview of Best BUY API Playground with sample exercises](/practice-sites/apps/bestbuy) - -### Device Registry - -- Device Registry is a test API with multiple users for managing 'devices' -- Can be run locally if you have Python installed. -- Can be run from Docker using the instructions in our guid -- Has a Swagger UI to allow easy exploration of the API -- Has a Redoc UI for an additional UI based exploration of the API -- API calls are authenticated using two hard coded user/password combinations - -[Read a full overview of Device Registry API with sample exercises](/practice-sites/apps/deviceregistry) - -### RestListicator - -- RestListicator is a simple API that can be run locally using Java or Docker -- Can be run in two modes, one with known bugs, one without deliberately added bugs -- Full range of CRUD operations is supported using `GET`, `POST`, `PATCH`, `DELETE` -- Application accepts and returns `XML` and `JSON` -- Open API Definition is downloadable from the GitHub Repo - -[Read a full overview of RestListicator with sample exercises](/practice-sites/apps/restlisticator) - -### Tracks - -- Tracks is a full todo app that can be run locally if you have Ruby on Rails installed, or use a [virtual machine from Turnkey](https://www.turnkeylinux.org/tracks) or [run from docker](https://github.com/eviltester/tracksdocker). -- I recommend running it from Docker -- Tracks is the case study application used in the book [Automating and Testing a REST API](https://www.eviltester.com/page/books/automating-testing-api-casestudy/) - -[Read a full overview of Tracks with sample exercises](/practice-sites/apps/tracks) - -### FX Trade Hub - -- FX-TradeHub is a simple API simulating a trading application. -- Can be run locally if you have Node installed or from a Docker file. -- Has a Swagger UI for easy exploration - -[Read a full overview of Fx Trade Hub with sample exercises](/practice-sites/apps/fxtradehub) - -### More Applications - -I've listed a few applications here initially and will expand the instructions to include walkthroughs and setup guides in future updates. - -- [Api Challenges](https://github.com/eviltester/thingifier/tree/master/challenger) - - API challenges can be run locally if you install a Java SDK and maven as pre-requisites. -- [JSON Server](https://github.com/typicode/json-server) - - Can be run locally if you have Node and npm installed. - - - - - -## Free to Use API lists - -The important thing to remember about list of free to use APIs is that the APIs are not designed for testing. They will either be `GET` interfaces to retrieve data or you will need to register and create an authentication key. - -I've included a few collated lists for completeness if you want to go beyond the practice APIs listed above. - -- [Free APIs](https://free-apis.github.io) -- [Ultimate API Challenge](https://theultimateapichallenge.com/challenges) -- [Rapid API - Free Public APIs](https://rapidapi.com/collection/list-of-free-apis) -- [Postman Collections for Public APIs](https://www.postman.com/explore/collections) -- [API List](https://apilist.fun) diff --git a/challenger/src/main/resources/content/practice-sites/apps/bestbuy.md b/challenger/src/main/resources/content/practice-sites/apps/bestbuy.md deleted file mode 100644 index 8898b0a9..00000000 --- a/challenger/src/main/resources/content/practice-sites/apps/bestbuy.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Best Buy API Playground - An old version of the Best Buy API - Practice API -seo_title: Best Buy API Playground Practice Guide for Testers -description: Best Buy API Playground is an old version of the Best Buy API. Notable for comprehensive query filter operations. -lastmod: 2026-02-18 -seo_description: Practice with Best Buy API Playground - using realistic endpoints and scenarios that help you improve API testing decisions, and request quality. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [BestBuy API Playground](https://github.com/BestBuy/api-playground). - -# Best Buy API Playground - A Practice GET API - -## About Best Buy - -The Best Buy API Playground is a node application which implements an older version of the Best Buy API. It comes pre-populated with production-like data. - -Since this is a practice version of the API, update and delete operations are enabled in addition to the expected `GET` verbs. These would not be available in the production version of the API. - -Summary: - -- Best Buy API Playground is a full API that can be run locally using Node or Docker -- Full range of CRUD operations is supported using `GET`, `POST`, `PATCH`, `DELETE` -- Large set of data is provided -- Swagger UI is available -- Open API Definition is downloadable - -## Running The Application Using Node - -If you have node installed then the API can best started by cloning the repository and using `npm`. - -Full instructions are on the [GitHub page](https://github.com/BestBuy/api-playground), copied here for ease of reference: - -``` -git clone https://github.com/bestbuy/api-playground/ -cd api-playground -npm install -npm start -``` - -The Best Buy API Playground will now be accessible on `http://localhost:3030` - -I personally prefer to run the application using Docker as described below. - -## Running The Application Using Docker - -Create the Docker image locally: - -``` -git clone https://github.com/BestBuy/api-playground -cd api-playground -docker build --tag bestbuy . -``` - -Then run the Docker image, making sure you map the exposed port `3030`: - -``` -docker run -d --name=bestbuy-api-playground -p 3030:3030 "bestbuy" -``` - -I map the port to `3030` on localhost to make the Swagger documentation URL work with the default values. - -## Documentation End Points - -You can then access: - -- the basic api documentation at - - `http://localhost:3030` -- A Swagger UI Front End at - - `http://localhost:3030/docs/` -- A Swagger API Definition file at - - `http://localhost:3030/swagger.json` -- A PostMan collection has been bundled with the app at - - `http://localhost:3030/postman/API.postman_collection.json` -- The Queries documentation shows some examples of queries to use on the API. This is useful because the GET API supports a complex set of query filters using SQL-like operators. - - `http://localhost:3030/queries` - -## API Endpoints - -The application is bundled with sample data so you will be able to query the data on the back end immediately. - -The main endpoints are: - -- `/products` - products available in the database. -- `/categories` - product categories and their subcategories/path. -- `/stores` - returns a list of Best Buy store locations. -- `/services` - returns a list of services available at Best Buy stores. -- `/version` - returns the current version of the application. -- `/healthcheck` - returns information about the application's health. - - -## Exercises - -### Exercise - `GET` all the main endpoints - -- Issue a GET request on each of the top level endpoints listed above -- Initially do this using the Swagger UI `http://localhost:3030/docs/` -- Repeat the exercise using a REST API Client to gain more control. You can download the Swagger file to pre-populate the requests in your REST Client. - -### Exercise - Explore the Example Query Formats - -- Use the list of Query formats on the Query Example page - - `http://localhost:3030/queries` -- Initially do this by clicking on the links -- Copy the URLs into your REST client and repeat the exercise -- Amend Query parameters to explore the results - -### Exercise - Create and Update the Data - -- Explore the endpoints to create and amend the data in the backend -- Use `OPTIONS` do check if the endpoints honour the verbs listed - -### Exercise - Does the API only support JSON? - -- The response format can sometimes be influenced using the `accept` header to tell the server that the client will accept a different format for the response. -- By default the server responds with `JSON`. -- What happens if you ask the server for `application/xml`? -- What other formats could you ask for? - -## Some Observations - -### Create and Updating Data - -- When creating data I noticed that the system was fussy around headers, but in a misleading way. Sending through a valid payload in 'text/plain' format I received the information that it `should have required property 'name'` rather than content-type not accepted. -- The server checks payloads for extra and unexpected fields. -- Server responds with all validations that the request fails, not just the first one, which is useful for debugging requests. -- Duplicate fields are allowed with the last one being accepted -- JSON validation fails with a `500` error rather than a `400` error suggesting that a general exception is thrown but not trapped to return a `400` (client) error. NOTE: the swagger documentation suggests that a `400` will be returned -- The swagger documentation did not show the field validation rules so I experimented to find validation lengths. And then used an online counterstring tool to check the length calculations [counterstring generator](https://eviltester.github.io/TestingApp/apps/counterstrings/counterstrings.html) -- Service names do not need to be unique -- Trying to amend an existing item using `POST` returned a `404` rather than a `405 Method Not Allowed`, which also contradicts the methods listed when I asked for `OPTIONS` - -### `accept` Header - -- The API examples are all `JSON` so I tried asking the API for `application/xml` I received a `500` error. I rather expected a `400` error because the request was asking for something the API did not support, rather than the server throwing a `500` General Error. -- Since the `xml` request failed I wondered what would happen with a `text/html` response and I found it interesting that the server did actually respond with an error in HTML format. - - diff --git a/challenger/src/main/resources/content/practice-sites/apps/deviceregistry.md b/challenger/src/main/resources/content/practice-sites/apps/deviceregistry.md deleted file mode 100644 index f18ab450..00000000 --- a/challenger/src/main/resources/content/practice-sites/apps/deviceregistry.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: Device Registry - A simple API with authentication - Practice Test API -seo_title: Device Registry API Practice Guide with Authentication -description: Device Registry is a simple API with endpoints protected by basic authentication. -lastmod: 2026-02-18 -seo_description: Practice with Device Registry - A simple using realistic endpoints and scenarios that help you improve API testing decisions, and request quality. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [Device Registry](https://github.com/AutomationPanda/device-registry-fastapi). - -# Device Registry - Simple Authenticated API - -## About Device Registry - -[Device Registry](https://github.com/AutomationPanda/device-registry-fastapi) is an API to simulate a a management system for devices, it is written in Python and can be run locally if you have Python installed. To make it easy to run, we have also created a Docker file that makes it easy to get started. - -The API has a Swagger UI and a Redoc UI. - -Summary: - -- [Device Registry](https://github.com/AutomationPanda/device-registry-fastapi) -- Can be run locally if you have Python installed. -- I created a Docker file which can run the application if you use Docker [here](https://github.com/eviltester/thingifier/tree/master/docker) -- Has a Swagger UI to allow easy exploration of the API -- Has a Redoc UI for an additional UI based exploration of the API -- API calls are authenticated using two hard coded user/password combinations: `engineer` : `Muh5devices`, and `pythonista` : `I<3testing`, - - -## Running The Application - -The repo provides instructions for running locally with Python, additionally we created a Docker file to make running the app easier. - -### Using Python - -Instructions for running the application are available in the project readme. - -- [github.com/AutomationPanda/device-registry-fastapi](https://github.com/AutomationPanda/device-registry-fastapi) - -An earlier version of the application using flask is also available written using flask, but this guide covers the fastapi version: - -- [Device Registry Service](https://github.com/AutomationPanda/device-registry-flask) - -### Using Docker && cURL - -Create the Docker image locally by downloading our Dockerfile for the project or cloning the apichallenges github repo. - -#### Download just the Docker file - -Create the Docker image locally by downloading our Dockerfile for the project: - -``` -mkdir fxtradehub -cd fxtradehub -curl https://raw.githubusercontent.com/eviltester/thingifier/refs/heads/master/docker/device-registry/Dockerfile -o Dockerfile -docker build --tag devregfastapi . -``` - -Then you can run the image: - -``` -docker run -it --name=devregfastapi -p 8000:8000 devregfastapi -``` - -And visit: - -- `http://localhost:8000/docs` to see the Swagger Docs UI. -- `http://localhost:8000/redoc` to see the Redoc Documentation UI. - -#### Cloning the API Challenges repo - -Create the Docker image locally by downloading our Dockerfile for the project: - -``` -git clone https://github.com/eviltester/thingifier -cd thingifier -docker build -t devregfastapi -f ./docker/device-registry/Dockerfile . -``` - -Then you can run the image: - -``` -docker run -it --name=devregfastapi -p 8000:8000 devregfastapi -``` - -- `http://localhost:8000/docs` to see the Swagger Docs UI. -- `http://localhost:8000/redoc` to see the Redoc Documentation UI. - - -## API Endpoints - -The API is a CRUD interface using verbs: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`. - -The main endpoints: - -- `/authenticate` - used to get a bearer token -- `/devices` - main endpoint for CRUD requests for a `device` entity -- `/devices/{id}/report` - return a text report of the given device -- `/status` - return the status of the service - -Most of the endpoints are authenticated. - -The API supports Create, and Update using `POST`, `PUT`, and `PATCH`: - -- `POST` is used to create items -- `PUT` will update an item using a full payload -- `PATCH` will update an item using a partial payload - -You can either use Basic Authentication using either of the two configured users: - -- `engineer` : `Muh5devices` -- `pythonista` : `I<3testing` - -Or make a basic authenticated `GET` request on the `/authenticate` api to receive a bearer token which can be used in requests. - -## Exercises - -A set of suggested exercises to explore the API. - -### Exercise - Use the Redoc UI to Explore the Documentation - -- The Redoc UI `http://localhost:8000/redoc` will allow you to see the documentation and examples for the endpoints -- You can not use the UI to issue requests, but it can be helpful when exploring the API in another REST Client to copy and paste endpoint URLs or payload data - - -### Exercise - Use the Swagger UI to Explore the Functionality - -- The Swagger UI `http://localhost:8000/docs` will allow you to issue the accepted requests for the API -- You can use the `/status` endpoint without any authentication -- Check that the other endpoints require authentication -- Add a username and password as the HttpBasic authentication details in the `[Authorize]` dialog and then issue requests -- Use the Swagger UI to issue `GET` requests and explore the data -- Use the Swagger UI to amend the data in the system. - - -### Exercise - Generate a Bearer token and use this as the authentication mechanism - -- Issue either a `GET` or `HEAD` request on the `/authenticate` endpoint -- Copy the bearer token into the `[Authorize]` dialog, remember to remove the username and password -- Check that you are able to make the same authenticated requests that you could using the HttpBasic authentication approach. -- Did you see what happens if you use both username/password and bearer token? - -### Exercise - Use a REST Client to Explore the API - -- Having gained familiarity with the normal usage of the API, use a REST client to explore the API in more depth. -- There are links to an OpenAPI spec from either of the documentation interfaces - - -### Exercise - Explore the Validation - -- The API documentation does not describe the validation rules, try and trigger validation errors. - -### Exercise - Explore Amending Values - -- Use the `POST` verb to amend existing entities, does it work, or is it only for creating entity instances? -- Use the `PUT` and `PATCH` verbs to explore the different amendment approaches (full, partial) - -### Exercise - Explore the Security - -- There are two users in the test database. Each user is only allowed to access their content. -- Check if all the verbs and endpoints are limited to each user's data. - -## Some Observations - -Some observations made when I followed the sample exercises. It is generally recommended that you perform the exercises first, prior to reading this information. - - -### Exploring the Redoc UI - -- There is no information on lengths or content, perhaps there is no validation? -- The report endpoint says it returns text, but the endpoint spec says it returns json - test this early. - -### Explore API using Swagger - -- Validation is performed by the swagger UI, really need to use REST API to test the validation -- Swagger does not allow using both HttpBasic and Bearer token - need to try that in a REST Client. -- Type returned by `/report` is `text/plain` and not `application/json` as suggested by the docs - -### Exploring Validation - -- Was able to trigger validation on 'extra' fields e.g. trying to create device with id and owner, adding an unsupported field e.g. "bob" -- All fields except owner and id seem to be mandatory on creation -- There are errors reported for JSON validation -- Types are not enforced, data is auto converted to String e.g. "name": 2 -- Duplicate fields - last one is chosen -- No duplicate value checks e.g. serial number can be duplicated -- Seems to be no validation other than - is it convertable into a String - so even `""` is allowed -- Since the `/device/{id}/report` uses the `name` as the 'filename' it is possible to create items with names which are not allowed as filenames and Swagger fails to return them e.g. `"name": "\\, /, :, *, ?, \", <, >, I"` - the response has invalid filename format `filename="\, /, :, *, ?, ", <, >, I.txt"` - I had to put this through a proxy to be sure - this will break some clients that try to parse the headers e.g. the browser for swagger -- There is method validation e.g. I can't `POST` to `/devices/1` - -### Exploring Security - -- When not authenticated the system responds with `unauthenticated` -- When authenticated the system leaks information about the other users' content e.g. `GET /devices/3` results in `"Forbidden"` so I know that some user has a device with `id = 3`, but I don't know who. - - - diff --git a/challenger/src/main/resources/content/practice-sites/apps/fxtradehub.md b/challenger/src/main/resources/content/practice-sites/apps/fxtradehub.md deleted file mode 100644 index 69ab2ea9..00000000 --- a/challenger/src/main/resources/content/practice-sites/apps/fxtradehub.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -title: FX Trade Hub- A Productivity GTD App - Practice Web App and API -seo_title: FX Trade Hub - Dummy Node API | Practice API and Web App -description: Fx Trace Hub is an API to simulate a trading system. It is written for Node and can be run locally if you have node installed.. -lastmod: 2026-02-18 -seo_description: Practice with FX Trade Hub- A using realistic endpoints and scenarios that help you improve API testing decisions, request quality, and confidence. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [FX-TradeHub](https://github.com/sd576/FX-TradeHub-API). - -# FX Trade Hub - A Web Application with API - - -## About FX Trade Hub - -[FX-TradeHub](https://github.com/sd576/FX-TradeHub-API) is an API to simulate a trading system. It is written for Node and can be run locally if you have node installed. To make it easy to run, we have also created a Docker file that makes it easy to get started. - -The API has a Swagger UI. - -Summary: - -- [FX-TradeHub](https://github.com/sd576/FX-TradeHub-API) -- Can be run locally if you have Node installed. -- I created a Docker file which can run the application if you use Docker [here](https://github.com/eviltester/thingifier/tree/master/docker) -- Has a Swagger UI to allow easy exploration of the API - - -## Running The Application - -### Using Node - -Instructions for running the application are available in the project readme. - -- [github.com/sd576/FX-TradeHub-API](https://github.com/sd576/FX-TradeHub-API) - - -### Using Docker && cURL - -Create the Docker image locally by downloading our Dockerfile for the project or cloning the apichallenges github repo. - -#### Download just the Docker file - -Create the Docker image locally by downloading our Dockerfile for the project: - -``` -mkdir fxtradehub -cd fxtradehub -curl https://raw.githubusercontent.com/eviltester/thingifier/refs/heads/master/docker/fxtradehub/Dockerfile -o Dockerfile -docker build --tag fxtradehub . -``` - -Then you can run the image: - -``` -docker run -it --name=fxtradehub -p 3000:3000 fxtradehub -``` - -And visit `http://localhost:3000/api-docs` to see the Swagger Docs UI. - -#### Cloning the API Challenges repo - -Create the Docker image locally by downloading our Dockerfile for the project: - -``` -git clone https://github.com/eviltester/thingifier -cd thingifier -docker build -t fxtradehub -f ./docker/fxtradehub/Dockerfile . -``` - -Then you can run the image: - -``` -docker run -it --name=fxtradehub -p 3000:3000 fxtradehub -``` - -And visit `http://localhost:3000/api-docs` to see the Swagger Docs UI. - - -## API Endpoints - -The API is a CRUD interface using verbs: `GET`, `POST`, `PUT`, `DELETE`. - -The top level url path for the api is `/api`. - -Then sub paths for the endpoints: - -- `/counterparties` -- `/counterparties/{id}` -- `/settlements` -- `/settlements/{counterpartyId}` -- `/settlements/{counterpartyId}/{currency}` -- `/trades` -- `/trades/{id}` - - -eg. `GET http://localhost:3000/api/counterparties` - -It is a fun API to test. It is still in its early stages, and as it is a hobby project test API... expect to be able to find bugs. - -## Exercises - -A set of suggested exercises to explore the API. - -### Exercise - Use the Swagger UI to Explore the Data - -- The Swagger UI `http://localhost:3000/api-docs` will allow you to issue the accepted requests for the API -- Use the Swagger UI to issue `GET` requests and explore the data -- Use the Swagger UI to amend the data in the system. -- Read the Swagger UI documentation carefully and see if you spot any issues - -### Exercise - Use a REST Client to Explore the API - -- Having gained familiarity with the normal usage of the API, use a REST client to explore the API in more depth. -- When calling `OPTIONS` does the endpoint honour all the verbs listed? - - -### Exercise - Explore the Validation - -- The API documentation does not describe all the validation rules, try and trigger validation errors. -- Vary the input data to see which fields have domain validation rules and which do not e.g. does an email have to be an email? -- Identify which fields need to be present and which are optional. -- Are you happy with all the field validations? - -### Exercise - Explore Amending Values - -- Use the `PUT` verb to amend existing entities -- When an amend payload using `PUT` is accepted, does it always amend the entity correctly? -- Any fields that you identified as optional during creation. Can you amend an existing entity with those values to make the field value optional? - - -## Some Observations - -These endpoints were made at a specific point in time. The system may have changed since, and you might not be able to repeat the observations below. They are provided here to give you some insight into the type of testing I performed. - -### Swagger UI - -- When reviewing the Swagger documentation I spotted some inconsistencies in the status code descriptions with the endpoint e.g. `/counterparties` would never return a `404` for a missing counterparty even though this is suggested by the documentation. - -### Exploring the API - -- I found it interesting that the ID 'looks like' it should be an integer `1` but is actually a string `001` and I had to use the full `001` to access specific items. -- Creating data was interesting. When experimenting with APIs I tend to `GET` and then use the payload in a `POST` to try and create data. But in this instance when creating a counterparty the phone number would not validate as the returned number `+44 20 7116 1000` exceeded the maximum length of 15. -- I like to call endpoints with verbs which are not listed as valid to see the response. Ideally I'll see `405 Method not allowed` rather than a `404` - -### Exploring the Validation - -- I tend to check for handling of invalid request formats, not just field formats. Ideally I expect to see a message about malformed JSON with a `400` response as `500` errors can be harder to diagnose. -- Some fields validate as requiring a value and it is not possible to amend them to be `null`, although it is possible to create them as `null`. -- The validation on the primary key allowed me to create an empty or null id, and then subsequently would not be able to amend or delete this entity. -- I like to create entities with minimum payloads to see if the system tells me what should exist, the error report for invalid counterparty items with not enough fields seems to be "Counterparty with this ID already exists" - -### Exploring the Amendment - -- When amending I like to check if I can amend fields to be `null`, particularly with a `PUT` as I view a `PUT` as overwriting and not a partial update. For this API implementation `PUT` is treated as a partial update and if a field is missing it is not updated on the backend. diff --git a/challenger/src/main/resources/content/practice-sites/apps/restlisticator.md b/challenger/src/main/resources/content/practice-sites/apps/restlisticator.md deleted file mode 100644 index 650808cf..00000000 --- a/challenger/src/main/resources/content/practice-sites/apps/restlisticator.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -title: RestListicator - A Simple REST API for practicing testing - Practice API -seo_title: RestListicator API Practice Guide for REST Testing -description: A very simple REST API with user management and full HTTP Verb support, runnable via Docker. -lastmod: 2026-02-18 -seo_description: Practice with RestListicator - A Simple using realistic endpoints and scenarios that help you improve API testing decisions, and approach. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try RestListicator part of the [TestingApp](https://github.com/eviltester/TestingApp). - -# RestListicator - A Software Testing Practice REST API - - -## About RestListicator - -The RestListicator is the first Software Testing API Practice application created by [Alan Richardson](https://eviltester.com). It was written to support [API Testing Training](https://github.com/eviltester/automating-rest-api/tree/master/text/basic-tutorial). - -The application is written in Java and full instructions are in the GitHub repository for compiling and running locally. It can also be run using Docker with full instructions for doing so contained below. - -QuickStart Guide to run the application on `localhost:4567`: - -``` -docker run -it -p 4567:4567 eviltester/restlisticator:latest -``` - -The deliberately buggy version can be downloaded and run from Docker hub with: - -``` -docker run -it -p 4567:4567 eviltester/restlisticator:latest-buggy -``` - -The application has text documentation when visiting the url in a browser: - -- `http://localhost:4567/listicator` - -Additionally a hand created OpenAPI spec file can be downloaded from the Github repository: - -- [github.com/eviltester/TestingApp/tree/master/java/testingapps/restlisticator/docs/swagger](https://github.com/eviltester/TestingApp/tree/master/java/testingapps/restlisticator/docs/swagger) - -Summary: - -- RestListicator is a simple API that can be run locally using Java or Docker -- Can be run in two modes, one with known bugs, one without deliberately added bugs -- Full range of CRUD operations is supported using `GET`, `POST`, `PATCH`, `DELETE` -- Application accepts and returns `XML` and `JSON` -- Open API Definition is downloadable from the GitHub Repo - -## Running The Application Using Docker Hub Image - -The easiest way to run RestListicator on Docker is to use the image posted to Docker Hub. - -The image has been pushed to the `restlisticator` repository `eviltester/restlisticator` - -- https://hub.docker.com/r/eviltester/restlisticator - -The images have been tagged with the RestListicator version, or `latest` and both the buggy and non-buggy version have been tagged e.g. - -Non Buggy version: - -- `eviltester/restlisticator:latest` -- `eviltester/restlisticator:1.2.2` - -Buggy version: - -- `eviltester/restlisticator:latest-buggy` -- `eviltester/restlisticator:1.2.2-buggy` - -The image can be downloaded and run as follows: - -``` -docker run -it -p 4567:4567 eviltester/restlisticator:latest-buggy -``` - -This will run the buggy version on `localhost:4567`. - - -## API Endpoints - -The application has one main functional Entity `list`, but it also has user access and multi-session access. - -Three users are created when the application starts, each with different permissions. - -Use Basic authentication to switch between users. - -- username: `superadmin`, password: `password` -- username: `admin`, password: `password` -- username: `user`, password: `password` - -The API root is `http://localhost:4567/listicator`, with all the endpoints existing below this e.g. `http://localhost:4567/listicator/heartbeat` - -The main endpoints are: - -- `/heartbeat` - is the server alive? -- `/lists` - manage the List entities - create, amend lists -- `/lists/{guid}` - create, amend, delete a List -- `/users` - user management - create, delete -- `/users/{username}/password` - amend a User's password -- `/users/{username}/apikey` - amend a User's api key -- `/feature-toggles` - superadmin can toggle app features on and off -- `/sessionid` - get a unique sessionid to isolate your testing from other users, use the session id in a X-SESSIONID header - -`GET` endpoints are mostly not authenticated. The endpoints beneath `/users` are authenticated i.e. only authenticated user can `GET` their details. Authentication can also be controlled using `X-API-AUTH` header. The api key for each user is visible when the application starts, and in the response headers made by the user. - -The application can accept and return `JSON` or `XML`. The response format is controlled by the `accept` header and the request format by the `content-type` header. - - -## Exercises - -### Exercise - Run the application and read the documentation - -- Run the application, either using the Java instructions on Github or with Docker -- Visit `http://localhost:4567/listicator` and read the documentation - -### Exercise - `GET` all the main endpoints - -- Issue a GET request on each of the top level endpoints -- Experiment with the `accept` header to return `XML` or `JSON` -- Check that you receive authentication errors for the appropriate user endpoints. - -### Exercise - Basic Auth as normal user - -- Explore the API as 'user' using Basic Auth -- You should now be able to experiment with creating, amending - - minimum information for a list is title e.g. `{title:"this is a title"}` -- Try changing the password of the user -- Try Deleting a list as 'user' - -### Exercise - Use parameters to filter User list - -- Documenation for `GET` '/users' lists some filters that can be applied to username - - can be filtered with `?username=exactmatch` e.g. `?username=admin` - - can be filtered with `?username="partialmatch"` e.g. `?username="adm"` -- Search for partial match on "r" -- Search for users with "admin" in their username -- Search for users with an absolute name e.g. user - -### Exercise: Use DELETE to Delete a list - -- DELETE a 'list' that you created using the `guid` - -_hint: `user` cannot delete_ - -### Exercise: Use POST to Amend a List - -- default `user` password is `password` -- minimum information for a list is `title` e.g. `{title:"this is a title"}` -- Basic Auth required for a `POST` request to `/lists/{guid}` - -How do you know it amended the list? - -Can you amend all the fields? `title`, `description`, `guid`, `createdDate`, `amendedDate` - -### Exercise: Use OPTIONS to explore capabilities of `/lists` - -- [OPTIONS](https://tools.ietf.org/html/rfc7231#section-4.3.7) - -_hint: check the headers_ - -### Exercise - Explore PUT - -- Use PUT to create a new 'list' -- Use PUT to amend a list -- Does PUT work with multiple lists? -- Does PUT work with users? -- Test the PUT end points with different payloads - - -### Exercise - Explore Status Codes - -Send requests to deliberately trigger the following status codes: - -- `200 OK` -- `201 Created` -- `204 No Content` -- `400 Bad Request` -- `401 Unauthorized` -- `403 Forbidden` -- `404 Not Found` -- `405 Method Not Allowed` -- `409 Conflict` - -Bonus points for a `5xx` error e.g. `500` - -### Exercises: Explore HEAD - -- [HEAD](https://tools.ietf.org/html/rfc7231#section-4.3.2) - - does `HEAD` implementation match the standard? - - compare `HEAD` to `GET` is it the same information? - - which parts of the API respond to `HEAD`? - - does the documentation match the implementation? - -### Exercises: Explore PATCH - -- [PATCH](https://tools.ietf.org/html/rfc5789) is implemented just like `post` - - does it comply with the [JSON Merge Patch](https://tools.ietf.org/html/rfc7396) standard? -- The implementation does not comply with the [JavaScript Object Notation (JSON) Patch](https://tools.ietf.org/html/rfc6902) standard - - read the JSON Patch standard -- The implementation does not comply wiht the [XML Patch Operations standard](https://tools.ietf.org/html/rfc5261) - - read the XML Patch Standard - -## Some Observations - -These observations were made using the buggy version of the application. - -### Explore `GET` on the main endpoints - -- `GET` a list that does not exist generated a `400` without an error message not a `404` e.g. `lists/bob` -- `GET` on a user that does not exist does not show different error to one which does, if I'm not authenticated then I just see `401`, that's good -- `GET` on a listicator endpoint that does not exist returns a 404 in HTML mode rather than an API - content-type text/html - suggests it is more of a 'web app' and only some endpoints are API endpoints - - - - diff --git a/challenger/src/main/resources/content/practice-sites/apps/toolshop.md b/challenger/src/main/resources/content/practice-sites/apps/toolshop.md deleted file mode 100644 index 307d2fda..00000000 --- a/challenger/src/main/resources/content/practice-sites/apps/toolshop.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -title: Toolshop API - A Web Application and API for Practicing Testing and Automating -seo_title: Toolshop API Practice Guide for Web and API Testing -description: Toolshop API is an online public application and API supporting logins and has multiple versions with bugs and without bugs. -lastmod: 2026-02-18 -seo_description: Practice with Toolshop API - A Web using realistic endpoints and scenarios that help you improve API testing decisions, request quality, and confidence. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [Toolshop API](https://api.practicesoftwaretesting.com/api/documentation). - -# Toolshop API - A Public API and Web Application for practicing Testing and Automating - -[Toolshop API](https://api.practicesoftwaretesting.com/api/documentation) by [Roy de Kleijn](https://www.linkedin.com/in/roydekleijn/) - -## About Toolshop API - -Toolshop API has a [UI](https://practicesoftwaretesting.com/) and an API backend. This allows you to experiment with the UI, open the network tab and see the relevant API requests, or use the API and double-check the results on screen in the UI. - -Multiple versions of the Toolshop API are available. - -- A [deliberately buggy version of the API](https://api-with-bugs.practicesoftwaretesting.com/api/documentation) with [buggy UI](https://with-bugs.practicesoftwaretesting.com/) is available if you want to explore for issues. -- -- The [main version](https://api.practicesoftwaretesting.com/api/documentation) with [UI](https://practicesoftwaretesting.com/) where bugs are not intended, or perhaps not perceived as bugs, remember every API is different. - -In actual fact, there are (at the time of writing), 5 non-buggy versions, each corresponding to a 'sprint' of development so you can test the application in the different stages of its development. The links can be found in the [source documentation](https://github.com/testsmith-io/practice-software-testing?tab=readme-ov-file#urls-hosted-versions): - -- Sprint 1 [UI](https://v1.practicesoftwaretesting.com/) and [API Swagger UI](https://api-v1.practicesoftwaretesting.com/api/documentation) -- Sprint 2 [UI](https://v2.practicesoftwaretesting.com/) and [API Swagger UI](https://api-v2.practicesoftwaretesting.com/api/documentation) -- Sprint 3 [UI](https://v3.practicesoftwaretesting.com/) and [API Swagger UI](https://api-v3.practicesoftwaretesting.com/api/documentation) -- Sprint 4 [UI](https://v4.practicesoftwaretesting.com/) and [API Swagger UI](https://api-v4.practicesoftwaretesting.com/api/documentation) -- Sprint 5 (the default public version) [UI](https://practicesoftwaretesting.com/) and [API Swagger UI](https://api.practicesoftwaretesting.com/api/documentation) -- Buggy Version of Sprint 5 [UI](https://with-bugs.practicesoftwaretesting.com/) and [API Swagger UI](https://api-with-bugs.practicesoftwaretesting.com/api/documentation) - -To explore the application, you really need to know the default users (these are also listed in the source documentation): - -- admin - email `admin@practicesoftwaretesting.com` and password `welcome01` -- user 1 - email `customer@practicesoftwaretesting.com` and password `welcome01` -- user 2 - email `customer2@practicesoftwaretesting.com` and password `welcome01` - -To practise API testing, I recommend: - -- using Sprint 5 -- starting with the non-buggy version for the majority of sessions -- use the buggy version after working with the 'non-buggy' version - -Summary: - -- The Swagger file is one of the most complete that I've seen, and is a great example of the capabilities of the format. -- The Toolshop API allows GET,POST, PUT methods without authentication. -- Some endpoints require that you master the authentication process which requires logging in as a user, then using the returned token as an HTTP Bearer Token. -- The DELETE endpoints seemed to require logging in as an admin user. -- The admin user also receives 'more' data in the responses, so its worth experimenting with the different users. -- The Database is shared, so don't add any private information (e.g. your name and email) if you create a new user. -- The error messages returned from the server in the message are useful aids to helping you craft a valid payload. - -Links: - -- Sprint 5 Online Public Hosted [Web App UI](https://practicesoftwaretesting.com/) and [API](https://api.practicesoftwaretesting.com/api/documentation) -- Sprint 5 deliberately buggy version of the [Web App UI](https://with-bugs.practicesoftwaretesting.com/) and [API](https://api-with-bugs.practicesoftwaretesting.com/api/documentation) -- [Testsmith-io on Github](https://github.com/testsmith-io) where you can find the code for the application and many examples of automating systems. - - [Code for the PracticeSoftwareTesting application](https://github.com/testsmith-io/practice-software-testing) - - -## API Endpoints - -The public application is bundled with sample data so you will be able to use the data on the back end immediately. - -The main endpoints are: - -- `/brands` -- `/carts` -- `/categories` -- `/messages` -- `/images` -- `/invoices` -- `/payment` -- `/products` -- `/reports` - various end points for specific reports -- `/users` - - -## Exercises - -### Exercise - Explore the Application and API Documentation - -Choose one of the hosted versions (sprint 1 -5, buggy) and explore the UI and Swagger API documentation. - -e.g. - -- Sprint 5 UI [practicesoftwaretesting.com](https://practicesoftwaretesting.com/) -- Sprint 5 API Swagger [api.practicesoftwaretesting.com/api/documentation](https://api.practicesoftwaretesting.com/api/documentation) - -As you use the UI, open the network tab and you can see how the API calls are used in the context of the UI functionality. You should also see some endpoints which require authorisation and authentication. - -### Exercise - Explore the API without Authentication - -You can use the Swagger UI to make initial API calls against the backend without any authentication. - -### Exercise - Explore the API with User Authentication - -You can use the Swagger UI to make API calls authenticated as one of the default users. - -- make a `POST` call to `/users/login` with a customer email and password - -e.g. - -```json -{ - "email": "customer@practicesoftwaretesting.com", - "password": "welcome01" -} -``` - -- take the returned `access_token` and include it in all future requests -- if you are using the Swagger UI then use the `[Authorize]` button in the UI and paste in the bearer token - -### Exercise - Explore the API with Admin Authentication - -- During your earlier exploration with User authentication you will have found methods and endpoints which you could not access. Check those methods now using an admin authenticated login session. - -### Exercise - Explore the API using a REST Client - -- Download and import the Open API file into your REST Client and make authenticated calls from the REST API. -- To make authenticated calls through the REST Client, include the `access_token` as a bearer token in the request: - - `'Authorization: Bearer eyJ0eXAiOi...` - -### Exercise - Explore the API and data validation rules - -- The Schema in the Swagger UI does not describe max and min fields or validation so we have to explore the data and rely on error messages to describe the data validation. -- Try and trigger data validation errors so that you understand what field validation rules are being used. - -### Exercise - Explore the API with Trickster Intent - -- The API allows amendment of data without authentication -- Explore the endpoints with the view of amending data such that it impacts the UI - -### Exercise - Explore the API with Security Intent - -- we don't necessarily need to know much about security to look at an API from a security perspective -- e.g. can you change data that you should not have access to? - -## Some Observations - -### Explore the Application and API Documentation - -- Using the UI I wasn't sure what API calls require what level of authentication but I could see authentication failures in the network tab. The Swagger UI does show a padlock against calls, but I wasn't sure what level of authentication was required. -- I found it interesting when I looked at the UI that some `POST` requests did not require authentication e.g. uploading files to messages, amending brands etc. - - This makes 'trickster intent' a viable mind set - -### Explore the API without Authentication - -- I like to use default data when I'm exploring using Swagger and I noticed that the `/related` call on a `/product` does not return a 404 instead it returns a 500 i.e. `/product/1/related` -- I was interested to see that the search endpoints support database wildcard characters i.e. `%`, which makes me think that I should explore other database specific values. - -### Explore the API with Trickster Intent - -- Since I can amend Brand data with a put, without authentication I was thinking what can I enter that might trigger issues on the front end. -- This is beyond data validation because I'm now thinking about data with an impact in the UI rendering. -- I found that the system accepts uni-code characters which can impact the rendering of the site ways that normal text does not. - -### Explore the API with Security Intent - -- The user management is vulnerable, possibly by design because it is a testing app, to insecure access e.g. I should not be able to change the password but that is what the forgotten password request does -- Endpoints also reveal if emails are already in use - - diff --git a/challenger/src/main/resources/content/practice-sites/apps/tracks.md b/challenger/src/main/resources/content/practice-sites/apps/tracks.md deleted file mode 100644 index 12cbf88c..00000000 --- a/challenger/src/main/resources/content/practice-sites/apps/tracks.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: Tracks - A Productivity GTD App - Practice Web App and API -seo_title: Tracks - A Productivity GTD App | Practice API and Web App -description: Tracks is a mature application implementing the Getting Things Done productivity method with a Web GUI and API. -lastmod: 2026-02-18 -seo_description: Practice with Tracks - A Productivity GTD using realistic endpoints and scenarios that help you improve API testing decisions, and request quality. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [Tracks](https://www.getontracks.org/). - -# Tracks - A Web Application with API - -## About Tracks - -[Tracks](https://www.getontracks.org/) is a Ruby on Rails application which implements the Getting Things Done productivity methodology. - -## Running Tracks - -The application page has '[how to run instructions](https://github.com/TracksApp/tracks/wiki/Installation)'. - -It can be run from: - -- a [TurnKey Virtual Machine](https://www.turnkeylinux.org/tracks) -- an [Official Docker Image](https://hub.docker.com/r/tracksapp/tracks) - -I found the official Docker image hard to get started, and also for my book [Automating and Testing a REST API](https://www.eviltester.com/page/books/automating-testing-api-casestudy/) I used versions 2.2.3 and 2.3.0. - -I created Docker images that make it easy to get started with versions 2.2.3 and 2.3.0 - -- [github.com/eviltester/tracksdocker](https://github.com/eviltester/tracksdocker) - -As a summary of the content from the above `eviltester/tracksdocker` repo, you can use Docker to start either version as follows: - -- v2.2.3 - - `docker run -p 80:80 eviltester/tracks:2.2.3` -- v.2.3.0 - - `docker run -p 80:80 eviltester/tracks:2.3.0` - -After running through docker you can access the application by visiting `http://localhost:80`. - -If you find that you can't login or can't create an admin session then try opening the link in incognito mode... if that works then delete the "_tracksapp_session" cookie in your normal browser window. - -## Tracks API - -The documentation for the Tracks API is available in the application itself from the help menu of the UI: - -- REST API documentation `http://localhost:80/integrations/rest_api` - -The API uses Basic Authentication where you supply the username and password of the admin user you created when you started the image. - -The API payloads are `XML` rather than `JSON`. - -The available endpoints are: - -- `/todos.xml` -- `/todos/{ID}.xml` -- `/tickler.xml` -- `/done.xml` -- `/hidden.xml` -- `/calendar.xml` -- `/contexts.xml` -- `/contexts/{ID}.xml` -- `/contexts/{ID}/todos.xml` -- `/projects.xml` -- `/projects/{ID}.xml` -- `/projects/{ID}/todos.xml` - -You may find it easier to create data in the back end using the UI before issuing API requests. - -## Automating Tracks - -Some examples for automating the Tracks API can be found in the source code for my book [Automating and Testing a REST API](https://www.eviltester.com/page/books/automating-testing-api-casestudy/) - -- [github.com/eviltester/tracksrestcasestudy](https://github.com/eviltester/tracksrestcasestudy) - -## Exercises - -### Exercise - Create Data using the UI and issue `GET` requests - -- Create a Project -- Create a Todo in the project -- Issue `GET` requests to return information on the end points - -### Exercise - Create `todo` Data Using the API and `POST` - -- each of the GET endpoints returns an XML response e.g. `/todos.xml` returns an array of todos -- use the `todo` element as a request payload with `POST` to create a todo -- experiment with the payload to find out what fields are optional and which are mandatory -- remember to `GET` the data to check the `todo` was created as expected - -### Exercise - Amend `todo` Data Using the API and `PUT` - -- to amend a todo you need to use the `/todos/{id}.xml` endpoint -- `{id}` would be replaced with the id of the todo you wish to amend -- experiment with the various fields and see what it is possible to amend and what it is not possible to amend - -### Exercise - Explore the Documentation - -- The API Documentation illustrates the use of the API using `cURL` -- Try the `cURL` commands listed so you learn to use `cURL` on your machine -- Some of the payloads you will send through to the API are quite large, so experiment with the `-d` [data](https://curl.se/docs/manpage.html#-d) paramenter to read information from a file -- The default response output from cURL may not include all the header information you need to check the responses. Try using `cURL` in `-verbose` mode [verbose mode](https://curl.se/docs/manpage.html#-d) - -## Some Observations - -### Response Content Type - -- The response content type of `text/html` may interfere with your REST Client's ability to show the response -- By returning `text.html` the REST Client may attempt to render the response as HTML rather than showing the raw XML -- In Bruno you can view the `raw` response and see the XML - -### Request Payload - -- I found it interesting that I could create a todo with a duplicate id, and that it would be created with a new id, rather than reporting the attempt as a duplicate diff --git a/challenger/src/main/resources/content/practice-sites/dummyjson.md b/challenger/src/main/resources/content/practice-sites/dummyjson.md deleted file mode 100644 index 506528d2..00000000 --- a/challenger/src/main/resources/content/practice-sites/dummyjson.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Dummy JSON - A Mix of API Simulator and GET API - Practice API -seo_title: DummyJSON API Practice Site Guide for Mixed Endpoints -description: An overview of the Dummy JSON practice API site, explaining how to use it to learn about APIs and practice with API tooling. -lastmod: 2026-02-18 -seo_description: Use Dummy JSON - A Mix of API to practice API testing with realistic scenarios and endpoint variety that strengthen request design and validation skills. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [DummyJSON](https://dummyjson.com/). - -# Dummy JSON - An API Simulator and functional API - -Dummy JSON is a mix of an API Simulator with canned responses to requests, and also functional API endpoints that you can actually test. - -## About Dummy JSON - -When I first found Dummy JSON I had categorised it as an API Simulator. An API that responds with the same data, no matter what values are in the request. But there are so many endpoints in this API that after hunting around while practicing I found a few endpoints that I could explore for testing. - -The [Docs](https://dummyjson.com/docs) often have a `[Show Output]` button under the examples so you can explore the basic functionality of the endpoint without a REST client. - -Additionally the examples in the Docs can be pasted into the Browser Dev Tools JavaScript console and you can experiment with JavaScript and Dev Tools. - -Don't let the examples on the Intro page of the docs confuse you, there is no endpoint called `RESOURCE`. The examples here are to show general parameters you can use with any of the endpoints e.g. `recipes`, `carts`, etc. - -Many of the endpoints are simple GET endpoints for canned data: - -- `products` -- `carts` -- `recipes` -- `users` -- `posts` -- `comments` -- `todos` -- `quotes` - -These all return canned data on `GET` and some are documented as providing simulated responses for `DELETE`, `POST`, `PUT`, `PATCH`. - -The `image` endpoint, because it generates custom responses based on the input is backed by a real server function, not just canned data. This makes it a viable target for exploratory testing. - -## Links - -- The main site [dummyjson.com](https://dummyjson.com/) -- The documentation [dummyjson.com/docs](https://dummyjson.com/docs) -- The source code [github.com/Ovi/DummyJSON](https://github.com/Ovi/DummyJSON) - -## Summary - -- An API for JSON requests and responses. -- A lot of endpoints for different types of data. -- Supports filters and limits as URL Query parameters. -- Simulate Update requests (`POST, PUT, DELETE`) -- More than an API simulator some endpoints can be pushed to trigger 500 server responses and validation messages - - -## Exercises - -I've created a set of suggested exercises in case you need some prompting for practice. - -### Exercise - Explore the API Via the Docs - -- [dummyjson.com/docs](https://dummyjson.com/docs) - - read through the documentation and find some requests which have `[Show Output]` buttons - - click the buttons to see the response - -### Exercise - Explore the API Via the Docs with the Browser Dev Tool Network tools Open - -- [dummyjson.com/docs](https://dummyjson.com/docs) - - open the browser dev tools and have the network tab open so you can see the request and response - - read through the documentation and find some requests which have `[Show Output]` buttons - - click the buttons to see the response - - check in the Network tab to see the actual request - - Spoiler - that's right, it didn't issue any requests, but now you know not to trust a UI exclusively and to make sure you have the ability to observe network traffic when working with web applications and APIs - -### Exercise - Use the Examples in the Docs with the Browser Dev Tool Network tools Open - -- [dummyjson.com/docs](https://dummyjson.com/docs) - - open the browser dev tools and have the network tab open so you can see the request and response - - read through the documentation and find some requests which have `[Show Output]` buttons - - All of the JavaScript shown on the page can be pasted into the JavaScript console so you can learn how to issue HTTP requests from the dev tools, and you can see the request and response in the Network tabs. - -### Exercise - Use the API - -- Download a REST Client -- Make some API calls that copy what the Doc examples do -- Make sure to try requests for all verbs listed i.e. `GET`, `POST`, `PUT`, `PATCH`, `DELETE` - -### Exercise - Explore the API with functions the Docs do not mention - -- [dummyjson.com/docs](https://dummyjson.com/docs) -- Read the docs and you'll see that some endpoints are only documented as having `GET` responses - - e.g. `https://dummyjson.com/recipes/1` -- What happens if you issue a `DELETE` request? or a `POST`, `PUT` etc. -- Do you get the responses you expected? - -### Exercise - Explore the `image` endpoint as a testing target - -- [dummyjson.com/docs/image](https://dummyjson.com/docs/image) -- The image generated is based on the parameters passed in via the request -- Does the end point validate all inputs? -- This might be an interesting test of your REST Client - does it show the image or do you have to test the endpoint in a browser? -- If your REST Client doesn't render the image in the client you might want to explore some of the other REST Clients and see if they can. - - -## Some Observations - -### Unsupported Verbs - -- When exploring the unsupported or unlisted verbs for some of the endpoints e.g. `recipes` is only documented as supporting `GET` -- I found that `DELETE`, `POST` etc. returned a 404 instead of, what I expected, [405](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405) -- This made me then explore the path structure of the URL a little longer - -### Image testing - -- There are obviously some parameters that are not well validated and can cause issues. - - Did you find any which trigger server errors? - - Did you find any parameters which behaved differently than you expected with invalid data? - - - - - diff --git a/challenger/src/main/resources/content/practice-sites/fakerestapi.md b/challenger/src/main/resources/content/practice-sites/fakerestapi.md deleted file mode 100644 index 6b2e41de..00000000 --- a/challenger/src/main/resources/content/practice-sites/fakerestapi.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Fake REST API - A Simulator API -seo_title: Fake REST API - A Simulator API | API Practice Site Guide -description: An overview of the Fake REST API for practicing tooling requests. -lastmod: 2026-02-18 -seo_description: Use Fake REST API - A Simulator to practice API testing with realistic scenarios and endpoint variety that strengthen request design and validation skills. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [Fake Rest API](https://fakerestapi.azurewebsites.net/index.html). - -# Fake REST API - An API which returns JSON data - -[Fake Rest API](https://fakerestapi.azurewebsites.net/index.html) is an API that returns a set of canned data. It also simulates the effects of update methods like POST, PUT, DELETE, PATCH. - -## About JSON Placeholder - -[Fake Rest API](https://fakerestapi.azurewebsites.net/index.html) is an API that returns canned data. It also claims to simulate the effects of update methods like POST, PUT, DELETE, PATCH by returning what would have happened if the request you made had taken effect. - -The Data represents 5 types of entities: - -- activities -- authors -- books -- coverphotos -- users - -Each of these has its own endpoint e.g. - -- `/api/v1/activities` -- `/api/v1/authors` -- etc. - -The API is simple enough for beginners to explore but the simulated responses from update requests are different from many APIs. - -Initial exploration can be done using the swagger interface itself. This allows you to make `GET`, `PUT`, `POST`, `DELETE` requests. - -Remember it is a simulator, rather than an API, so your updates do not make back end changes, i.e. issuing a GET after a PUT will not show you the changes you submitted. - -The Swagger interface validates the inputs so you'll need to use a REST Client if you want to test it with invalid values to check for parameter validation. - -This API Simulator offers the benefit of an [Open API Specification File](https://fakerestapi.azurewebsites.net/swagger/v1/swagger.json). You can download this and use it to seed the requests in a REST Client, or study the file to guide your exploration. - -## Links - -- The main site and documentation [fakerestapi.azurewebsites.net/index.html](https://fakerestapi.azurewebsites.net/index.html) -- The API Open API file [fakerestapi.azurewebsites.net/swagger/v1/swagger.json](https://fakerestapi.azurewebsites.net/swagger/v1/swagger.json) - - -## Summary - -- A simulator with a Swagger front end to experiment easily -- Do make sure to use a REST Client as well to explore parameter validation. -- The `POST` and `PUT` requests don't update the server but they are validated so you can see an additional approach to JSON validation and error reporting. -- Even as a simulator it has bugs so you can explore the functionality and find issues. -- A fun API to spend some time exploring. - -## Exercises - -I've created a set of suggested exercises in case you need some prompting for practice. - -### Exercise - Explore the API Via the Swagger Interface - -- Use the Swagger front end to make various calls on the endpoints -- You can only input valid values for parameters -- Hint: You may be able to find some inconsistencies between the input data and simulated responses - -### Exercise - Use the API with a REST Client using the Open API file - -- Download a REST Client -- Download the `swagger.json` file -- Import the `swagger.json` into your REST Client and explore conditions you could not using the swagger interface - - e.g. `OPTIONS`, `HEAD`, non-integer parameters, invalid endpoint urls, etc. - -### Exercise - Use the POST verb to create instances - -- The `POST` simulator is quite interesting -- Create instances with minimal fields and all fields -- Try adding duplicate fields in the payload -- What happens if you add extra fields? -- Which fields will auto convert between types e.g. String to Integer, Integer to String, etc. - -### Exercise - Explore the DELETE verb on endpoints - -- The `DELETE` simulator is also quite interesting -- Delete something that is not supposed to exist, do you get the response you expect? - - -## Some Observations - -- Swagger File - - The swagger file does not have any `host` or base path entries so I had to amend all the request URLs in the REST client. - - The swagger file itself is a good example of the type of files you are likely to see in the real world. -- Request Content - - I found it interesting that the request content is not consistent between calls. e.g. the same number of activities is returned, with the same IDs, but the due dates would change. I didn't spend enough time exploring this but I did wonder what varied between requests and I'm tempted to return and explore this in a future session. - - This regeneration of due dates also makes it more difficult to automate because you can't directly compare the instances returned from `/activities` and individual call e.g. `/activities/30`. So as a simulator it is not quite consistent -- 40xs - - I thought the link to the HTTP standard with `400`, `404`, `405`, `415` responses was a nice touch. It is non-standard and you'd never see this in the real world, but if you are just learning APIs then this might be useful to help understand the response you are seeing. - - The 404s are different between simulated end points and general URL calls to the application -- `POST` simulator - - The `POST` simulator was quite accurate and encouraged me to explore the field values and types -- `OPTIONS` - - The `OPTIONS` verb did not seem to be supported, but I thought it was a nice touch that `405` responses contained the `allow` header -- `coverphotos` - - I wasn't expecting the cover photos url to actually lead to something, but the urls themselves don't seem to be url encoded so might not work if you fed them into a follow on HTTP GET request - - diff --git a/challenger/src/main/resources/content/practice-sites/httpbin.md b/challenger/src/main/resources/content/practice-sites/httpbin.md deleted file mode 100644 index 7da8ea30..00000000 --- a/challenger/src/main/resources/content/practice-sites/httpbin.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: HttpBin - An Attp Request and Response Service - Practice HTTP -seo_title: HTTPBin API Practice Site Guide for HTTP Testing -description: An overview of the HttpBin site, explaining how to use it to learn about HTTP Requests and Responses. -lastmod: 2026-02-18 -seo_description: Use HttpBin - An Attp Request to practice API testing with realistic scenarios and endpoint variety that strengthen request design and validation skills. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [HttpBin.org](https://httpbin.org/). - -# HttpBin - An Http Request and Response Server - -HttpBin is not really an API, but it is an excellent test bed for exploring HTTP requests, responses and message formats. - -## About HttpBin - -HttpBin should not be viewed as an API. It does not conform to any API standards, nor does it implement any state. - -But it is a very good place to expand your knowledge of HTTP requests. - -[HttpBin.org](https://httpbin.org/) presents a Swagger style API to make HTTP requests to the server. - -The site can also be run easily from docker: - -```bash -docker run -p 80:80 kennethreitz/httpbin -``` - -Above command will run the server via docker locally on port 80 `localhost:80` - -It is possible to explore the server endpoints using the swagger interface but I think you'll gain more value from making requests from an HTTP client as you can explore more combinations. - -There are actually endpoints that you can explore as a tester since the Dynamic Data endpoints respond based on the inputs provided. - -I think the site is useful for working through each of the endpoints, thinking through what you expect the endpoint to respond with, then making the request from an HTTP client and checking the result against your expectations. - -The [mdn web docs for HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) are a great reference to have open as you explore the endpoints. - -## Links - -- The main site [HttpBin.org](https://httpbin.org/) -- The source code https://github.com/postmanlabs/httpbin - -## Summary - -- A server for HTTP requests and responses. -- A lot of endpoints for different types of verbs and request contents. -- Good for working through the endpoints and checking your expectations - - -## Exercises - -I've created a set of suggested exercises in case you need some prompting for practice. - -### Exercise - Explore the HTTP Methods in an HTTP/REST Client - -HttpBin offers endpoints for some HTTP verbs: `/delete`, `/get`, `/patch`, `/post`, `/put` - -For each endpoint, think through what you expect before: - -- issue a request with the verb associated with the endpoint e.g. `DELETE /delete` -- issue a request with a verb not associated with the endpoint e.g. `GET /delete` -- issue a request on the endpoint with trailing path params e.g. `/delete/`, `/delete/1` - -### Exercise - Explore the Auth Methods in the Swagger Endpoint - -The Auth endpoints allow you to explore valid and invalid authentication. Each of the authentication URLs define the parameters for the authentication scheme and if you issue a GET on that endpoint with the valid authentication setup then you will see an authenticated request. - -- explore each authentication method (note: some you may not be able to authenticate via swagger) -- try and configure the username and passwords to accept empty values - -### Exercise - Explore the Auth Methods in an HTTP/REST Client - -The Auth endpoints allow you to explore valid and invalid authentication. Each of the authentication URLs define the parameters for the authentication scheme and if you issue a GET on that endpoint with the valid authentication setup then you will see an authenticated request. - -- explore each of the endpoints using the HTTP Client and you should be able to authenticate with all methods - -### Exercise - Explore the Status Codes - -- Use the list of status codes from [mdn web docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) and issue them for the various verbs -- Use status codes which are outside the range of valid responses - -### Exercise - Explore the Request Inspection Endpoints - -The Request Inspection Endpoints are very similar to the [Api Challenges Mirror Mode](https://apichallenges.eviltester.com/practice-modes/mirror). When you make a request, the response lists your headers, or ip address, or whatever is being mirrored. - -- Try the endpoints with a variety of HTTP Clients and see if the requests are what you expect -- Are there any additional headers that you didn't expect? Can you override or prevent these headers being sent? -- Are any of the headers that you see injected by the HTTP server? How would you know if the HTTP Server or the HTTP Client is adding them? -- Add invalid headers, and header formats, and multiple headers to see if they are sent to the server - -These are good endpoints for making sure that your HTTP client is doing what you ask of it. - -### Exercise - Explore the Dynamic Data Endpoints - -The Dynamic Data Endpoints respond based on the input so it is possible to explore their handling of: - -- valid and invalid data (e.g. data that is not Base64 encoded) -- missing parameters -- data that is too large - -## Some Observations - -### Exploring HTTP Methods - -- when exploring the HTTP Method endpoints I found that it conformed more to my expectations than some of the simulation APIs - - I received `404` when expected - - I received `415` when expected - - `HEAD` and `OPTIONS` also worked as I expected - - -### Exploring Request Inspection Endpoints - -- when exploring the headers endpoint, the only way I can determine if the client is adding the header, or if it is the HTTP server, is by configuring my REST Client to use an HTTP Proxy like BurpSuite. -- My Http Proxy was also the only way I found of issuing a clean request to the server. -- I also found it interesting that when I amended the "Host" header in my REST Client (Bruno) to be an invalid host like "bob", my Proxy refused to send the message to the server. So I discovered a guard in my Proxy that I didn't know about. - - diff --git a/challenger/src/main/resources/content/practice-sites/jsonplaceholder.md b/challenger/src/main/resources/content/practice-sites/jsonplaceholder.md deleted file mode 100644 index 98d8fd4c..00000000 --- a/challenger/src/main/resources/content/practice-sites/jsonplaceholder.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: JSON Placeholder - Simulator API - Example API -seo_title: JSONPlaceholder API Practice Site Guide for Testers -description: An overview of the JSON Placeholder simulator API for practicing tooling requests. -lastmod: 2026-02-18 -seo_description: Use JSON Placeholder - to practice API testing with realistic scenarios and endpoint variety that strengthen request design and validation skills. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [JSON Placeholder](https://jsonplaceholder.typicode.com/). - -# JSON Placeholder - An API which returns JSON data - -[JSON Placeholder](https://jsonplaceholder.typicode.com/) is an API that returns a set of canned data. It also simulates the effects of update methods like POST, PUT, DELETE, PATCH. - -## About JSON Placeholder - -[JSON Placeholder](https://jsonplaceholder.typicode.com/) is an API that returns canned data. It also simulates the effects of update methods like POST, PUT, DELETE, PATCH by returning what would have happened if the request you made had taken effect. - -The Data represents 6 types of entities: - -- posts -- comments -- albums -- photos -- todos -- users - -Each of these has its own endpoint e.g. - -- `/posts` -- `/comments` -- etc. - -The API is simple enough for beginners to explore. - -Initial exploration can be done using the browser itself with GET requests by amending the URL. - -e.g. - -- https://jsonplaceholder.typicode.com/posts - -This will allow you to explore the data structures, but you will want to use a REST Client to explore the other methods. - -Remember it is a simulator, rather than an API, so your updates do not make back end changes, i.e. issuing a GET after a PUT will not show you the changes you submitted. - -It is still fun to explore and it does have what I would consider to be bugs so hunt around and see if you can find some. - -## Links - -- The main site and documentation [jsonplaceholder.typicode.com/](https://jsonplaceholder.typicode.com/) -- The API usage guide [jsonplaceholder.typicode.com/guide/](https://jsonplaceholder.typicode.com/guide/) -- The source code [github.com/typicode/jsonplaceholder](https://github.com/typicode/jsonplaceholder) - -If you want to experiment with creating your own simple mock API you can do so with the sister project from typicode [json-server](https://github.com/typicode/json-server) - -## Summary - -- GET API -- Canned Data -- Simulates Update requests -- Multiple Entity types and response formats -- Useful for experimenting with tools in a safe environment -- Remember it is a simulator, rather than an API, so your updates do not make back end changes - -## Exercises - -I've created a set of suggested exercises in case you need some prompting for practice. - -### Exercise - Explore the API Via the Browser URL - -- Make a GET request from the Browser URL e.g. - - https://jsonplaceholder.typicode.com/posts - - https://jsonplaceholder.typicode.com/posts/1 -- Explore the Routes and Resources listed in the main documentation using GET requests - - https://jsonplaceholder.typicode.com/ - -### Exercise - Use the API with a REST Client - -- Download a REST Client -- Make some GET API calls using the REST Client - -### Exercise - Explore the Docs - -- [Documentation](https://jsonplaceholder.typicode.com/) and [Guide](https://jsonplaceholder.typicode.com/guide/) -- Read the docs -- Use the examples in the docs as a basis for exploration -- experiment with different HTTP Verbs: GET, POST, OPTIONS, HEAD -- The easiest way to issue a POST, is to issue a GET and then use the response as the POST request - -### Exercise - Explore the Verbs and Payloads - -- experiment with different HTTP Verbs: GET, POST, OPTIONS, HEAD -- Do you get the responses and status codes you expect? - -### Exercise - Automate from the Browser JavaScript Console - -- Did you try and run all the examples in the [guide](https://jsonplaceholder.typicode.com/guide/) from the developer JavaScript console? -- Open the developer console (Right Click, Choose Inspect), then choose the Console -- Paste each of the examples into the console - -### Exercise - Try backend validation - -- The API is JSON based so try an invalid JSON payload -- Only JSON is supported, what happens if you pass in XML? (with `content-type: application/xml`) - - -## Some Observations - -- Validation - - I found the back end validation interesting. - - The content type was very important for triggering the validation. - - Malformed JSON (if sent through with content type `application/json`) resulted in 500 errors rather than 400 errors, which suggests the server hasn't been coded to validate responses properly. If this was a real API rather than a simulator then I would want to test this further as there might be vulnerabilities. - - Any content, sent through with content type `application/xml` did not result in a JSON error, the fact that it was XML was ignored. -- URL Handling - - URLs were slightly non standard in the sense that both `/posts` and `/posts/` were treated as the same URL, but normally `/posts/` would result in a 404 because an ID would be expected e.g. `/posts/1` - - POST to `https://jsonplaceholder.typicode.com/posts/3` resulted in a 404, but I would expect it to have been a `405` METHOD NOT ALLOWED because this API simulator uses `PUT` to amend entities - - - - diff --git a/challenger/src/main/resources/content/practice-sites/randomuser.md b/challenger/src/main/resources/content/practice-sites/randomuser.md deleted file mode 100644 index 41c09eb6..00000000 --- a/challenger/src/main/resources/content/practice-sites/randomuser.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: RandomUser - Random User Generator - Example API -seo_title: RandomUser API Practice Site Guide for Data Testing -description: An overview of the Random User Generator API, explaining how to use it to learn about APIs and practice with API tooling. -lastmod: 2026-02-18 -seo_description: Use RandomUser - Random User to practice API testing with realistic scenarios and endpoint variety that strengthen request design and validation skills. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [RandomUser.me](https://randomuser.me/documentation). - -# RandomUser.me - An API which returns random data - -RandomUser.me is an API that returns a randomly generated user. - -## About RandomUser.me - -RandomUser.me is a Random Data Generation API. It supports multiple versions, and can be used to generate multiple users at the same time. Because it is GET based you can use the Browser to explore the API without any additional tooling. - -The Random Data represents a 'user' or 'person'. - -The API is simple enough for beginners to explore. - -Because the API is only documented as supporting GET, it is possible to explore the API using a browser by amending the URL. - -e.g. - -- https://randomuser.me/api/ -- https://randomuser.me/api/?format=xml - -The application supports multiple versions, so if you want to explore earlier versions and test the evolution of the API you can add the version number in the endpoint URL. - -e.g. - -- https://randomuser.me/api/0.1/ - -The version numbers are listed in the [Changelog](https://randomuser.me/changelog). - -Random APIs can be difficult to test, but RandomUser.me has a `seed` parameter which can be used to control the random generation. - -i.e. the same user will be generated for each call to https://randomuser.me/api/?seed=123 - -There are a lot of parameters in the URL to explore, all of which are listed in the documentation. - -e.g. - -- Include and Exclude fields from the response -- Controlling output format -- Creating users of specific nationalities - - -## Links - -- The main site [randomuser.me](https://randomuser.me) -- The documentation [randomuser.me/documentation](https://randomuser.me/documentation) -- The source code [github.com/RandomAPI/Randomuser.me-Node](https://github.com/RandomAPI/Randomuser.me-Node) - -## Summary - -- A Random User Generator -- Designed for GET requests only -- Supports multiple versions by embedding the version number in the API URL -- Can return different formats e.g. JSON, CSV, XML -- Can generate multiple users by using the results parameter `?results=5000` -- Seed can be used to generate specific users for repeated calls - -## Exercises - -I've created a set of suggested exercises in case you need some prompting for practice. - -### Exercise - Explore the API Via the Browser URL - -- make a request from the Browser URL e.g. https://randomuser.me/api/?seed=123&results=2 -- Use network tab to view the requests and responses - -### Exercise - Use the API with a REST Client - -- Download a REST Client -- Make some API calls using the REST Client - -### Exercise - Explore the Docs - -- [Documentation](https://randomuser.me/documentation) -- Read the docs -- Use the docs as a basis for exploration -- experiment with different HTTP Verbs: GET, POST, OPTIONS, HEAD - -### Exercise - Try all the endpoints and params listed in the docs - -- try all the end points listed -- try all the query parameters listed - - - - - - - diff --git a/challenger/src/main/resources/content/practice-sites/simpleapi.md b/challenger/src/main/resources/content/practice-sites/simpleapi.md deleted file mode 100644 index 449188d5..00000000 --- a/challenger/src/main/resources/content/practice-sites/simpleapi.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -title: Simple API - A Multi-verb API with no security requirements - Practice API -seo_title: Simple API Practice Site Guide for Multi-Verb Testing -description: An overview of the Simple API practice API site, explaining how to use it to learn about APIs and practice with API tooling. -lastmod: 2026-02-18 -seo_description: Use Simple API - A Multi-verb to practice API testing with realistic scenarios and endpoint variety that strengthen request design and validation skills. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [SimpleAPI](/practice-modes/simpleapi). - -# Simple API - A functional API that supports GET and Update - -Simple API is a fully functional API allowing you to retrieve and update from the server. - -## About Simple API - -Simple API is a fully functional API allowing you to retrieve and update from the server. It is suitable as an entry level practice API because it requires no authentication and there is no risk of exposing private data. - -The API allows access to a store inventory `item` entity which has: - -- `id` - an auto generated number id -- `isbn` - a 13 digit numeric value that must be unique -- `type` e.g. `cd`, `dvd`, `book`, `blu-ray` -- `price` -- `numberInStock` - -The API refreshes when there are a small number of records so that there is always content in the API to work with. - -Only 100 items can be stored in the API at any point in time. - -All of the fields of the 'item' are numeric or enum, so there is no chance of encountering any risky user generated data or to enter any personal data. - -The only complexity involved is generating an ISBN that matches the format required. - -There is an endpoint in the API to return a random ISBN `/simpleapi/randomisbn` or you can use the Button below to generate a random data value suitable for using in the API. - -{{}} - -Two OpenAPI files are available for the API, one with validation, one without validation. These can be imported to a REST client or a Swagger UI to help you get started with the API. - -The API Supports two main endpoints: - -- `items` -- `item` - -Verbs supported are: - -- `GET` at an `/items` level to see all items -- `POST` at an `/items` level to create an item -- `GET` a specific item with id e.g. `/items/1` -- `PUT` and `POST` to update a specific item -- `DELETE` to delete a specific item e.g. `/items/1` - -The API is useful for getting used to creating, updating, reading and deleting entities from an API. - -The API also supports both XML and JSON payloads by amending the `content-type` and `accept` headers in the request. - -## Links - -- The main site [SimpleAPI](/practice-modes/simpleapi) -- The documentation [SimpleAPI/docs](/simpleapi/docs) -- Data viewer for current items [SimpleAPI/gui/entities](/simpleapi/gui/entities) -- OpenAPI Files [simpleapi-openapi](/practice-modes/simpleapi-openapi) - -## Summary - -- An API for JSON and XML requests and responses. -- A simple data structure to avoid personal data -- No Authentication required for updates -- Update requests are supported (`POST, PUT, DELETE`) - - -## Exercises - -I've created a set of suggested exercises in case you need some prompting for practice. - -### Exercise - Explore the Data in the Backend - -The SimpleAPI has a Data Explorer front end so that you can view the data in the backend. - -- view the data to get a feel for the Entity that is used -- [apichallenges.eviltester.com/simpleapi/gui/instances](https://apichallenges.eviltester.com/simpleapi/gui/instances?entity=item&database=__default) - -### Exercise - Read the Documentation - -The Simple API has: - -- An [About Page](https://apichallenges.eviltester.com/practice-modes/simpleapi) - - There is an ISBN generator here which might help when creating requests in the API to create and amend items -- [Documentation](https://apichallenges.eviltester.com/simpleapi/docs) - -### Exercise - try the Open API files in a Swagger UI - -- Any Swagger UI can be used to load in the Open API files e.g. -- Open the [PetStore Swagger UI](https://petstore.swagger.io/) -- Paste into the url `[Explore]` field either of the [Simple API OpenAPI](/practice-modes/simpleapi-openapi) Urls - - Normal `https://apichallenges.eviltester.com/simpleapi/docs/swagger` - - Permissive `https://apichallenges.eviltester.com/simpleapi/docs/swagger?permissive` -- Try each of the API files and see what difference they make to your testing e.g. the Permissive files does not have validation on the input parameters so you can explore more conditions. - - OpenAPI files are designed for usage, not for testing, you should not rely on a Swagger UI for testing purposes. - -### Exercise - Explore the `/items` endpoint - -- The main endpoints `/items` will list all of the items in the system and allow creation of a new item -- Explore these with `GET`, `OPTIONS`, `HEAD` -- Try to create an item using `POST` on `/items` - - if you don't know the format of the payload then copy one of hte item objects listed in the response of `GET /items` - - you should not include an `id` in the `POST` request - - the `isbn` field will need to be unique, you can generate a random `isbn` on this page or the [About](/practice-modes/simpleapi) page - - -### Exercise - Explore the `/items/{id}` endpoint - -- The individual endpoint `/item/{id}` will allow you to work with a single item using its id -- Try to amend an item with `POST` on `/items/{id}` for an item that exists -- Try to amend an item with `PUT` on `/items/{id}` for an item that exists -- Try to delete an item with `DELETE` on `/items/{id}` for an item that exists -- Check that the API responds with `404` when the item id does not exist - -### Exercise - Explore the endpoints with all verbs - -- CRUD operations are based around the `GET`, `PUT`, `POST` and `DELETE` verbs -- Make sure you call the endpoints with other HTTP verbs e.g. `PATCH` and `TRACE` - -### Exercise - Explore the `/items/{id}` field validations - -- When you create or amend an item, check that the system enforces the validations for the fields listed in the documentation and the Open API spec - - The ISBN must be unique in the system - - The ISBN must match a specific format, try the different variations of the format - - e.g. `384252925993-2`, `618-3051270614`, `9114-91-557340-1`, `2799488037490` - - find more matching variations than the examples above - - an easy only tool for generating values from Regex can be found [here](https://onlinetools.com/random/generate-random-data-from-regexp) - - Check the field validations of the other fields as well - -### Exercise - Explore the system data population validations - -- The documentation says that there can be only 100 items, is that true? -- What is the minimum number of items that can be in the system? Can it be brought down to 0, 1, 2, 3, ...? - -### Exercise - Explore the random ISBN endpoint - -- There is an endpoint for generating random ISBNs `GET /simpleapi/randomisbn` -- Does it work they way you would expect? - -### Exercise - Explore the `accept` header - -- The Simple API supports both `JSON` and `XML` -- You should be able to request `XML` as the response payload by using an `accept` header with the value `application/xml` -- Do all endpoints and calls support this? - -### Exercise - Explore the `content-type` header - -- The Simple API supports both `JSON` and `XML` -- You should be able to send request payloads as `XML` by using a `content-type` header with the value `application/xml` -- Do all endpoints and calls support this? -- Do you think you could mix and match `content-type` and `accept` so that you can send `JSON` and receive `XML`? -- Could you send `XML` and receive `JSON`? - - diff --git a/challenger/src/main/resources/content/practice-sites/swapi.md b/challenger/src/main/resources/content/practice-sites/swapi.md deleted file mode 100644 index 6a0182db..00000000 --- a/challenger/src/main/resources/content/practice-sites/swapi.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -title: Swapi - Star Wars API - Example Site -seo_title: SWAPI API Practice Site Guide for Endpoint Exploration -description: An overview of the Star Wars API application, explaining how to use it to learn about APIs and practice with API tooling. -lastmod: 2026-02-18 -seo_description: Use Swapi - Star Wars API - to practice API testing with realistic scenarios and endpoint variety that strengthen request design and validation skills. -showads: true ---- - -In addition to our [API Challenges](/gui/challenges) you should practice on as many sites as possible. Try [Swapi](https://swapi.dev). - -# Swapi.dev - A Web Application which uses API - -Swapi is an API that returns data about Star Wars characters, planets and starships. - -## About Swapi - -Swapi is a Star Wars API that is suitable for beginners to API testing. It can be explored without installing any tools and is well documented. - -The [main page](https://swapi.dev) describing the API has an input form that you can use to work with the API without having to install any tools. If you do use the front end system rather than a REST API Client, you will have to use the Browser Network tab to see the headers of the response. - -The main site suggests that there are only 3 endpoints: - -- `/people` -- `/planets` -- `/starships` - -Each of the endpoint urls accept the ID of the entity to access the information e.g. - -- `/people/1` - -Each of the endpoints also support a `/schema` path which the documentation says returns a definition of the response. - -Looking through the [Documentation](https://swapi.dev/documentation) we can see that there are actually more endpoints e.g. films, vehicles, species, planets. - -There are also URL parameters that can be explored: - -- `?format=wookiee` -- `?search=text` - -For extra fun, the documentation page lists different implementations of the API in other languages. One exercise, could be to compare the different implementations. You might even automate the main implementation and then try an repeat the automated execution against a different implementation to see how they compare. - -The application also has a 'secret' UI when you make API calls from the Browser URL where you can issue OPTIONS, GET and can see the response headers e.g. use https://swapi.dev/api/people/1 in the browser URL - -## Links - -- The main site [swapi.dev](https://swapi.dev) -- The documentation [swapi.dev/documentation](https://swapi.dev/documentation) -- The source code [github.com/Juriy/swapi](https://github.com/Juriy/swapi) - -## Summary - -- A Star Wars API App -- A GET based API (no updates allowed) with a variety of data end points -- All endpoints return star wars information -- The UI can help you get started making requests without any tools, but make sure to use a REST Client on the API to explore fully. -- HEAD and OPTIONS are also supported -- Only supports the GET verb... or does it? -- Make sure you read the documentation to find all the features -- There is no OpenAPI specification file - -## Exercises - -I've created a set of suggested exercises in case you need some prompting for practice. - -### Exercise - Explore the API Via the APP - -- [swapi.dev](https://swapi.dev) - - make a request from the GUI - - Use network tab to view the requests and responses - -### Exercise - Use the Built-in REST Interface in the Browser - -- visit https://swapi.dev/api/people/1 -- explore the functionality of the different UI - -### Exercise - Use the API - -- Download a REST Client -- Make some API calls that copy what the UI Did - -### Exercise - Explore the Docs - -- [Swapi documentation](https://swapi.dev/documentation) -- Read the docs -- Use the docs as a basis for exploration -- use an HTTP client to issue requests e.g. Postman, cURL, Insomnia.rest -- try to generate different HTTP Status codes: 200, 400, 404, 405 -- experiment with different HTTP Verbs: GET, POST, OPTIONS, HEAD - -### Exercise - Try all the endpoints and params listed in the docs - -- try all the end points listed -- try all the query parameters listed - -### Exercise – Try all the endpoint variants - -e.g. - -- /people -- /people/1 -- /people/1/ -- /people/schema - -### Exercise - Compare the search results to the entity list results - -- issue `/people` then identify a search term to try like `?search=sky ` -- How do you know the results are correct? - -### Exercise – Create an OpenAPI spec file for the API and try running the spec through the various tools - -- Load it into Swagger UI and try to use it to make API calls -- Use Dredd to automatically cover API endpoints - -## Some Observations - -### HTTP OPTIONS Verb - Example swapi.dev - -Swapi has a very unusual OPTIONS output - -e.g. swapi.dev - -OPTIONS - https://swapi.dev/api/people/1/ - -~~~~~~~~ -{ - "name": "People Instance", - "description": "", - "renders": [ - "application/json", - "text/html", - "application/json" - ], - "parses": [ - "application/json", - "application/x-www-form-urlencoded", - "multipart/form-data" - ] -} -~~~~~~~~ - -Hint: Swapi supports GET, OPTIONS and HEAD - try them all, then double check this statement. (Trust no-one). - -### Non-standard URL parsing - -The API is non-standard in the sense that it accepts trailing “/”. - -Normally `/people/1` and `/people/1/` would be treated as different endpoints and `/people/1/` would return a 404 and not map on to the person with ID 1. - -Seeing this as a tester made me think of using URL formats that I would not normally use e.g. - -- How many “/” can I add? -- Can I add leading and trailing “/” ? -- Could the ID be anywhere in the list of leading and trailing slashes? I.e. “////3////” -- What other characters might be valid as part of the URL path? E.g. could I add “\”, what else could I add? - -The interesting part of exploring the API is that I don’t know if these non-standard features are by design or if they are bugs. - -One thing I might want to do is look through the source code and try to decide if any of the observations are bugs or if they are there intentionally. - - - - diff --git a/challenger/src/main/resources/content/seo-metadata-test-page.md b/challenger/src/main/resources/content/seo-metadata-test-page.md deleted file mode 100644 index b8b9bc99..00000000 --- a/challenger/src/main/resources/content/seo-metadata-test-page.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: SEO Metadata Test Page -seo_title: Open Graph Metadata Test Page for Validation | API Challenges -description: Base description for SEO metadata test coverage. -lastmod: 2026-02-18 -seo_description: Search snippet with Alan's "special" chars & context. -meta_robots: noindex,nofollow -og_image: /images/social/apichallenges-og-1200x630.png -og_image_alt: OG preview image for API Challenges metadata tests -og_type: article -twitter_card: summary -twitter_site: @apichallenges -schema_breadcrumb_enabled: false -schema_howto_enabled: true -schema_howto_steps: Open the metadata test page||Validate title and social tags -schema_video_enabled: true -schema_video_id: dQw4w9WgXcQ ---- - -# SEO Metadata Test Page - -This page exists to validate Open Graph and SEO metadata rendering behavior. diff --git a/challenger/src/main/resources/content/sponsors.md b/challenger/src/main/resources/content/sponsors.md deleted file mode 100644 index ff392b0a..00000000 --- a/challenger/src/main/resources/content/sponsors.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Sponsors of API Challenges -seo_title: Sponsors of Guide for API Testing | API Challenges -description: Sponsor the API challenges and support the ongoing development and hosting for this free practice and tutorial site. -lastmod: 2026-02-18 -seo_description: Explore Sponsors of API Challenges with practical guidance and actionable next steps designed to improve API testing skills through hands-on practice. -showads: true ---- - -# Support and Sponsorship - -This application is made available free to the public thanks to the generosity of our sponsors and supporters. - -Their support helps: - -- pay for hosting -- support ongoing development -- support creation of associated free training material - -If you are a company or corporation that would like to sponsor the project then contact [Alan Richardson](https://linkedin.com/in/eviltester) for more details. - -## Current Platinum sponsor - -[![EvilTester.com log](https://www.eviltester.com/siteimages/evil_laugh_cleaned_transparent_h300.png)](https://eviltester.com) - -- [EvilTester.com](https://eviltester.com) free online training and books related to Software Testing and Development. Also including The Evil Tester Show podcast, and YouTube video channel. - -## Gold Sponsors - -[![Bespoke Software Development Consultancy and Training](https://compendiumdev.co.uk/images/cut_down_logo_152_171_e.png)](https://compendiumdev.co.uk) - -[Want to become a corporate sponsor of API Challenges?](https://eviltester.com/page/sponsor/corporate/) - -## Patreon - -Thank you to everyone who helps supports this project by being a part of our Patreon programme: - -- [Patreon](https://patreon.com/eviltester) - -## Historic Sponsors - -Thank you to all previous sponsors: - -- [EvilTester.com](https://eviltester.com) -- [Compendium Developments](https://compendiumdev.co.uk) diff --git a/challenger/src/main/resources/content/tools/clients.md b/challenger/src/main/resources/content/tools/clients.md deleted file mode 100644 index e9dd62d6..00000000 --- a/challenger/src/main/resources/content/tools/clients.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -title: HTTP Rest Clients - Introduction -seo_title: HTTP Rest Clients - Introduction | API Testing Tools -description: An overview of the most popular, and mostly free, HTTP and REST Clients. -lastmod: 2026-02-18 -seo_description: Compare popular API clients, see where each tool fits best, and choose the right option for exploratory testing, debugging, and daily request work. -showads: true ---- - -# REST HTTP Client Tools Are Essential - -We Need Tools. - -Web Services are designed for software and other applications to call, we humans need tools to access them. - -We might be tempted to immediately think of tools for automating, but the tools listed here are being included because they support interactive exploration and testing of REST APIs. - -For automating I would use a library like Rest Assured, or an HTTP library. - -For exploratory testing, I would use one of the listed tools. - ---- - -## Example REST API and HTTP Client Tools - -I have used all of the following tools. - -At the moment I use [Bruno](https://www.usebruno.com/) for my exploratory testing. I configure it to feed all traffic through the [BurpSuite](https://portswigger.net/burp/communitydownload) proxy to capture all my traffic and allow me to save and review my testing. - -### Command Line Clients - -- [cURL](https://curl.se) - - command line based - - API examples often shown in cURL - - recommended that you learn this eventually - - [download](https://curl.se/download.html) - - [Read our More Detailed Review of cURL](/tools/clients/curl) - -### GUI Clients - -I recommend and primarily use Bruno. It is a very capable free client, with paid plans if you want to use the tool for automated execution. - -- [Bruno](https://www.usebruno.com/) - - An [open source](https://github.com/usebruno/bruno) REST Client which can import Swagger files and is easy to configure. - - [Read our More Detailed Review of Bruno](/tools/clients/bruno) - -**Recommended free lightweight GUI Clients for evaluation:** - -- Bruno [review](/tools/clients/bruno) - good all-rounder, free for commercial use. **Recommended** [usebruno.com](https://usebruno.com) -- Milkman [review](/tools/clients/milkman) - a little rough around the edges, but open source and free for commercial use [Milkman on github](https://github.com/warmuuh/milkman) -- Kreya [review](/tools/clients/kreya) - proxy can only be configured via environment variables but capable when configured [Kreya](https://kreya.app) -- Yaak [review](/tools/clients/yaak) - lacks scripting, free for commercial use if run from source. [yaak.app](https://yaak.app/) - -**Recommended free heavyweight GUI Clients for evaluation:** - -- Insomnia - [review](/tools/clients/insomnia) - easy to use without logging in, login required for multiple collections [Insomnia.rest](https://insomnia.rest/) -- Postman - [review](/tools/clients/postman) - requires logging in, feature packed, UI takes time to learn [GetPostman.com](https://getpostman.com/) - -We have evaluated [many Desktop GUI Clients here](/tools/clients/summary-reviews). - - -### Online Clients - -Online clients are useful to get started quickly. One issue with Online clients is that they may experience [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) issues with some requests (e.g. `OPTIONS` is particularly problematic) - -But they are very useful to experiment with an API quickly. - -- [Hoppscotch](https://hoppscotch.io) - - A well implemented browser based client. Import Postman and Swagger (Open API) collections. -- [RestTestTest](https://resttesttest.com/) - - This is a very basic online client. Recommend using the Developer Tool Network tab to view the requests and responses. - - -## Features Useful For Testing - -I look for the following features in an API Client that I want to use for Exploratory Testing. - -I do not really use API Clients for Automating, I use them to support my testing. For Automating I tend to use code and HTTP libraries so the features below may not be the same review criteria that you apply. - -- Essential - - Send HTTP or HTTPS requests - - View the actual requests and responses - - Proxy support - - Can create invalid requests -- Optional - - Read Open API files - - Supports global environment variables e.g. for host - - Different payload body types - - Easy to add headers - - custom headers - - auth header support - - override body type headers - - Variables in headers e.g. for session headers - - Custom Verb support - - Repeating or cloning earlier requests - - View history of requests/responses - - Does not send diagnostic information to server -- Bonus - - Data driven requests - - Output log of test sessions - - Scriptable for customisation - not automating - - Free for commercial use - -### Optional Features - -The Optional features are there to make my Testing a little easier. I can live without them. I have used Proxy tools for HTTP API testing, and when testing in this way there is very little support for easy HTTP Request construction. - -Most of the features are usability features. Does the tool make it easy to edit the requests? Can I create variables that I share across requests? e.g. to add custom authentication headers, or change the Host URL in a single place rather than amending on every request. - -I like to be able to create a set of requests by opening an Open API file. - -Any feature that makes things easier is useful but not essential. - -A history of requests and responses is useful to see what I just did, and to let me see what the tool thinks it has sent and received. But if I have proxy capability then I can use the Proxy to do this. - -Most people using the REST Clients will be using them in a commercial setting. It is important to look at the license of the tool. Some tools are 'free' but the license forbids use of the tool in a commercial environment without purchasing a license. - -I would rather tools did not send diagnostic information about how I use them to a server. If it does then I want an option to switch it off. What if there is a bug and it starts sending the actual requests and payloads? How can I fully trust the tool over time? - -### Essential Features - -It sounds obvious, but I need to be able to make any HTTP or HTTPS request. Very often online tools are limited by CORS headers, I need tools that let me explore a wide range of requests. This includes invalid requests. I cannot allow my testing to be constrained by validations imposed upon me by the tooling. e.g. I need to be able to send invalid JSON in payloads to see how the API responds. - -I also need Proxy tool support to be able to send every request through a Proxy. This lets me see exactly what is sent and received. Allows me to do basic data driven testing using fuzzing, and allows me to keep a log of my testing. - -### Bonus Features - -The bonus items are there because many tools don't have them, but if I'm testing for long periods of time then it is useful to: - -- feed a set of data into a request and see what happens -- export what I did while testing to a log file as evidence. It is sometimes possible to get this from a Proxy. -- I like tools which are scriptable then I can use them in adhoc ways. - -Scriptable needs to be easy to use and I mainly use it to support me, rather than automate. e.g. generating random data, outputting information to logs. - - -## Why test interactively and not just automate? - -- observe traffic -- create varied requests -- experiment fast -- setup data -- send 'invalid' requests -- exploratory testing of API -- test while API still 'flexible' -- Interactive CRUD testing - CREATE, READ, UPDATE, DELETE - -'Testing' is a different activity from 'Automating'. - -Testing requires variation, observation and exploration, so we need tools that can help us send requests, easily vary requests and inspect the responses to find problems. - -Automating helps us identify unanticipated changes in behaviour that we previously asserted as acceptable. - -## REST API Client Lists - -- A list of Open Source API Clients is available at the [Awesome API Clients Github list](https://github.com/stepci/awesome-api-clients). This is a well maintained and up to date list containing more clients than are listed here. -- Our list of reviewed and considered [Desktop GUI Clients](/tools/clients/summary-reviews). diff --git a/challenger/src/main/resources/content/tools/clients/bruno.md b/challenger/src/main/resources/content/tools/clients/bruno.md deleted file mode 100644 index e5a966fe..00000000 --- a/challenger/src/main/resources/content/tools/clients/bruno.md +++ /dev/null @@ -1,276 +0,0 @@ ---- -title: Bruno HTTP Rest Client - Overview and Review -seo_title: Bruno API Client Review for Exploratory Testing -description: An overview of the Bruno HTTP REST API Client. -lastmod: 2026-02-18 -seo_description: Review Bruno HTTP Rest Client for API testing, including strengths, limitations, and practical fit so you can choose the right client for your workflow. -showads: true ---- - -# Bruno - -Bruno is an Open Source API client designed for local use, with Git as the main project sharing mechanism. - -## About Bruno - -Bruno is an Open Source API client with paid features. The free features have been good enough for everything that I have had to do with Bruno. - -Bruno can be found at: - -- [usebruno.com](https://usebruno.com) - -Bruno is a fast developing project so to get a full understanding of the tool I read the documentation, the blog and the Github feature request forums. - -- [Bruno Documentation](https://docs.usebruno.com/) -- [Bruno Blog](https://blog.usebruno.com/) -- [Bruno Github Repo](https://github.com/usebruno/bruno) -- [Support and Feature Request forums](https://github.com/usebruno/bruno/issues) -- [Discussions](https://github.com/usebruno/bruno/discussions) - -## Demo Video of Bruno - -{{}} - - -[Patreon ad free video](https://www.patreon.com/posts/127001249) - -## Benefits of Bruno - -I find Bruno to be easy to use. Bruno focuses on making it easy to interact with an API either interactively using the UI or automating using the `Test` tabs. - -I have not used the `Test` capabilities because I automate using HTTP libraries. But I have used the Scripting capabilities to generate random data and log the output of my testing. - -## Quick Review Criteria - -| Feature | Y/N | Comment | -|-----------------------------------------------------|-----|-------------------------------------------------------------------| -| **Essential** | | | -| Send HTTP or HTTPS requests | Y | | -| View the actual requests and responses | Y | Seen in the timeline view | -| Proxy support | Y | | -| Can create invalid requests | Y | | -| **Optional** | | | -| Read Open API files | Y | Also populates Environment host variables | -| Supports global environment variables e.g. for host | Y | With syntax highlighting and linting | -| Different payload body types | Y | | -| Easy to add headers | Y | | -| - custom headers | Y | | -| - auth header support | Y | | -| - override body type headers | Y | It is possible to edit as JSON and set content-type header to XML | -| Variables in headers | Y | | -| Custom Verb support | N | GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD | -| Repeating or cloning earlier requests | Y | But not from the History, only the Request UI | -| View history of requests/responses | Y | But no export capability | -| Exporting requests to cURL format | Y | | -| Sends Diagnostic information to server | N | Does not seem to send diagnostic information | -| **Bonus** | | | -| Data driven requests | Y | Paid plan only | -| Output log of test sessions | N | | -| Scriptable for customisation | Y | At Collection and Request level | -| Importing cURL requests | Y | When creating a request can import cURL | -| Free for commercial use | Y | Additional features are available on paid plan | - -Bruno met all my basic needs. The Data Driven feature is only available on a paid plan. - -For people who like to automate in the REST Client, the `test` suite capability can be used on the free plan. - -## Notes on Usage - -Bruno saves all the collection files in a folder. This makes it easy to review and manage in version control. - -I haven't used the direct Git integration features in Bruno, preferring to manage the Collection versioning using Git directly. - -Bruno makes the version control easy by keeping all data in simple files, and multiple files are used rather than a single database file or large JSON or XML file. - -When I imported an Open API file with environment data the environment variables in Bruno were set correctly and I was able to use the remote server or local server without configuring the collection in the UI. - -The Payload body editor uses the syntax of the payload type selected e.g. JSON payloads are colour coded and syntax checked. - -The editor did not prevent me from sending through invalid payloads. i.e. the payload was shown as invalid in the UI but still sent through to the server which is exactly what I want. - -Headers are easy to add. Code completion on Headers is available for the value, rather than the key. - -Multiple authentication types are supported by the UI making it easy to add Bearer tokens or API keys. These can also be added directly through the Header amendment and can use values in variables. - -Variables can be created at multiple levels: - -- Collection -- Request -- Environment -- Local OS Environment - -I tend to stick to Environment variables. But have experimented with Collection variables for random data. - -Bruno has recently added Dynamic Data so Faker can be used to add random data into a payload. - -e.g. `{{$randomCurrencyCode}}` - -This is documented in the [Dynamic Variables](https://docs.usebruno.com/testing/script/dynamic-variables) section. - -Bruno changes frequently. - -## Summary - Easy Default First Choice for free APi Exploratory Testing Client - -Bruno is my current default REST API tool for Exploratory and Interactive Test Sessions. - -I find it: - -- easy to use. -- plays well with other tools e.g. Proxies -- easy to script and generate random data -- imports Open API files well -- has a small set of features to the UI is uncluttered - -The paid plan as the Data Driven capabilities. But if I want Data Driven I either use a Proxy or code the automated execution. - -## Appendix - Experiments in Scripting Bruno - -I use the Scripting capabilities of tools on an ad-hoc basis to improve my workflow, rather than automating the coverage. - -I found the Scripting in Bruno to be very suitable for my purposes. - -Bruno comes with useful libraries built in, and it is possible to treat your [Bruno collection as a Node project](https://docs.usebruno.com/testing/script/external-libraries) by adding a `package.json` and adding whatever library you want. - -### Random Data - -I first experimented using Random Data in Bruno. The built in Faker `{{$randomCurrencyCode}}` capability is useful but I wanted more custom random data. - -e.g. the [Simple API](https://apichallenges.eviltester.com/practice-modes/simpleapi) requires a unique ISBN number for each item - -I used Bruno to make a random ISBN available to each request, if I wanted to use it. - -In the Script section of my collection I added: - -``` -bru.setVar("isbn13",'xxx-x-xx-xxxxxx-x'. - replaceAll('x', (match, p1, offset) => {return Math.floor(Math.random() * 10)})); -``` - -This means on every Request, there is a variable called `isbn13` which has been set to a random valid ISBN 13 format, which I could use in the request by adding `{{isbn13}}` anywhere in the request. This gave me the ability to test quickly when I didn't care what ISBN to use, and I could easily use a String when I wanted more control. - -I noticed that one of the [supported libraries](https://docs.usebruno.com/testing/script/inbuilt-libraries) when the collection is opened in Developer mode is lodash. so I used lodash to randomly select a 'type' from an Array. I could have used normal JavaScript for this but wanted to experiment with the Scripting capabilities. - -I found I had to `require` lodash as follows: - -``` -var _ = require('lodash'); -bru.setVar("type",_.sample(['cd', 'dvd', 'blu-ray','book'])); -``` - -The code above created a `{{type}}` variable with a random type if I wanted to use it in a payload. - - -e.g. in a POST payload - -``` -{ - "type": "{{type}}", - "isbn13": "{{isbn13}}", - "price": "13", - "numberinstock": "12" -} -``` - -The Scripting approach was well documented and easy to use. - -### Logging Test Sessions - -I started writing a script to run at a Collection level at a post response event level (after every response). - -I wanted to write the request and response object to a file to keep track of my testing. - -I found that I had to use a feature that is documented but not obviously documented. - -Amending the `bruno.json` file in the collection to allow scripts to access the file system. - -``` - "scripts": { - "filesystemAccess": { - "allow": true - } - } -``` - -This is shown in the documentation in some code sections but I found it by searching Github for other questions related to file system access through Scripting. - -- https://github.com/usebruno/bruno/issues/306 - -This is also when I found that there is a section in the Discussions where people have uploaded their own scripts. - -- [Bruno - Scriptmania](https://github.com/usebruno/bruno/discussions/385) - -This is where I found a script that closely matched what I wanted: - -- https://github.com/usebruno/bruno/discussions/385#discussioncomment-7246390 - -The above script: - -- requires creating a `logs` folder in the collection -- writes a JSON representation of the request and response to a time stamp named file - -I amended the script to: - -- create the `logs` folder -- write all the responses to a markdown formatted file -- with one file for each hour -- if a `TestNote` variable is in the Request section then it will be written as a paragraph in the log output - -This more closely matches my Test Approach, where I Test for small chunks of time, and create notes in a text editor about the work I'm doing. - -The script I used is below, and it was pasted into the Collection `post response` Script section: - -``` -// based on https://github.com/usebruno/bruno/discussions/385#discussioncomment-7246390 - -const fs = require('fs'); -const path = require('path'); -const moment = require('moment'); - -const safeStringify = function (obj) { - try { - return JSON.stringify(obj, null, 2); - } catch (err) { - return obj - } -}; - -const request = { - url: req.getUrl(), - headers: req.getHeaders(), - method: req.getMethod(), - body: req.getBody() -}; - -const response = { - status: res.getStatus(), - headers: res.getHeaders(), - body: res.getBody() -}; - -const now = moment(); -const fileNameFormattedDate = now.format('YYYYMMDD-HH'); - -const dir = path.join(bru.cwd(), 'logs') -if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); -} - -const filename = path.join(dir, `${fileNameFormattedDate}.md`); -const content = safeStringify({request, response}, null, 2); - -let title = "\n\n## " + new Date().toLocaleString() + - " - " + res.getStatus() + " " + - req.getMethod() + " " + req.getUrl() + - "\n\n"; - -fs.appendFileSync(filename, title); - -let comment = bru.getRequestVar("TestNote") - -if(comment!=null){ - fs.appendFileSync(filename, "\n\n" + comment + "\n\n"); -} - -fs.appendFileSync(filename, content); -``` - diff --git a/challenger/src/main/resources/content/tools/clients/curl.md b/challenger/src/main/resources/content/tools/clients/curl.md deleted file mode 100644 index 99a0dc0d..00000000 --- a/challenger/src/main/resources/content/tools/clients/curl.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: cURL HTTP Client - Overview and Review -seo_title: cURL HTTP Client - Overview and Review | API Client Review -description: An overview of the cURL HTTP Client. -lastmod: 2026-02-18 -seo_description: Review cURL HTTP Client for API testing, including strengths, limitations, and practical fit so you can choose the right client for your workflow. -showads: true ---- - -# cURL - -cURL is an Open Source API CLI based client for making HTTP requests. - -## cURL Overview - -cURL is a command line tool. Available by default in most Linux and Mac systems. Windows ships with an alias for `curl` and cURL needs to be installed on Windows to use properly. - -- Official Site - [cURL](https://curl.se) - -Because it is a command line tool it can be more difficult to use. But it is an essential tool to have at your disposal. Most other REST Clients will be able to generate a cURL command. - -cURL is useful because when you raise a defect, you don't want to try and attach a Postman of Bruno Collection, but you can easily paste in a `cUrl` command that can trigger the issue, and can reasonably expect that the programmers on the team will have cURL installed. - -~~~~~~~~ -curl {{}}/heartbeat -i -curl -X GET {{}}/todos -curl {{}}/todos -H "accept: application/xml" -~~~~~~~~ - -You might also find cURL useful for data driven testing, if your REST Client does not support feeding in a file of data to a request then you can probably find a tutorial online for the operating system you are using, generate the request from the REST API Client, and feed a file of data into the request. - -[cURL](https://curl.se) Can be complicated but useful for emergencies, scripting, bug reporting. - -> **NOTE**: By default Windows Powershell has a `curl` command but it is not really curl. It is an alias around `Invoke-WebRequest` and does not have all cURL features or use all the command line parameters. e.g. all the cURL examples listed above would fail. But if you change `curl` to `curl.exe` you can start using the non-alias Windows preinstalled `cURL` tool. - - -## cURL Support From Other REST Clients - -cURL came first. Before Postman, Insomnia, Bruno or any other REST Client. If you wanted a tool to make HTTP requests you used cURL. - -This is why, when you use the Swagger UI. For each of the requests you will see the cURL command that can replicate that request. - -Other Rest Clients will also generate cURL requests. - -- In Bruno, if you click on 'Generate Code' then you can choose "Shell-Curl" and it will generate a cURL command matching the request you are currently editing. - -> **HINT**: you can use Postman or Insomnia to generate cURL code but different continuation characters on different operating systems: `^` Windows and `\` on Mac/Linux also `"` and `'` differences. - -Postman and Insomnia can generate a request in the UI by pasting a cURL request into the request URL in the application. diff --git a/challenger/src/main/resources/content/tools/clients/httpie.md b/challenger/src/main/resources/content/tools/clients/httpie.md deleted file mode 100644 index 661bedf1..00000000 --- a/challenger/src/main/resources/content/tools/clients/httpie.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Httpie HTTP API Rest Client - Overview and Review -seo_title: HTTPie API Client Review for Exploratory Testing -description: An overview of the Httpie HTTP REST API Client. -lastmod: 2026-02-18 -seo_description: Review Httpie HTTP API Rest Client for API testing, including strengths, limitations, and practical fit so you can choose the right client for you. -showads: true ---- - -# Httpie - -Httpie is a Desktop Open Source API client. An expansion of the Httpie CLI client. - -Free to use for commercial environments but requires an account for some advanced features. Missing features required for Exploratory Testing. - -## About Httpie - -I've seen HTTPIE listed as a CLI tool. But when I visited the site it is now a Desktop and CLI tool. - -Httpie is an Open Source API client with a login for some advanced features. - -Httpie can be found at: - -- [Httpie](https://httpie.io/) - -I should start with the limitations. These were enough that they prevent me recommending the tool as an Exploratory Testing tool, given the competition in this space. - -- does not support loading Open API files - only Postman or Insomnia, so in theory could convert using those tools but this seems like a limitation -- proxy does not seem to be supported from Desktop App -- account required to choose where to save files locally - -For me, the proxy support is essential. I wouldn't recommend Exploratory Testing without the ability to hook a client up to a Proxy. - -In all other respects it seems very competent with a functional UI. - -- can create environment variables these are shared across all collections in a 'space' -- can create custom methods -- good cURL support, can generate cURL and create requests from cURL -- it is possible to override content type -- body is syntax checked and it is possible to send through invalid payloads -- authentication is easy to add -- code completion on headers -- no scripting capabilities -- can only see the 'last' response, not a history of responses -- can see the request sent. - -I found that the tool is prioritising AI features rather than the competitive basics. So you'll b enable to generate requests from a prompt, but you won't be able to view the history of requests through a proxy or extend the tool with scripting. But if you want AI support then this is the first of the simple clients I've seen support it. - -Creating an account and logging in is optional, doing so allows you to create a space on your disk in a location of your choosing, and access the AI. - -## Quick Review Criteria - -| Feature | Y/N | Comment | -|-----------------------------------------------------|-----|-------------------------------------------------------------------| -| **Essential** | | | -| Send HTTP or HTTPS requests | Y | | -| View the actual requests and responses | Y | Last request and response can be viewed | -| Proxy support | N | | -| Can create invalid requests | Y | | -| **Optional** | | | -| Read Open API files | N | Can read Postman and Insomnia collections | -| Supports global environment variables e.g. for host | Y | | -| Different payload body types | Y | | -| Easy to add headers | Y | | -| - custom headers | Y | | -| - auth header support | Y | | -| - override body type headers | Y | It is possible to edit as JSON and set content-type header to XML | -| Variables in headers | Y | | -| Custom Verb support | Y | | -| Repeating or cloning earlier requests | Y | But not from the History, only the Request UI | -| View history of requests/responses | N | Only last request and response can be viewed | -| Exporting requests to cURL format | Y | | -| Sends Diagnostic information to server | ? | Not possible to configure through proxy | -| **Bonus** | | | -| Data driven requests | N | | -| Output log of test sessions | N | | -| Scriptable for customisation | N | 'Chaining' can be used to 'pull' values from earlier responses. | -| Importing cURL requests | Y | | -| Free for commercial use | Y | | - - -## Summary - Pretty but lacks basic features - -Httpie was pretty but did not meet my basic needs. - -The lack of Open API import would hinder most people using it in production environments. - -The lack of Proxy support means that I wouldn't consider it for exploratory testing. - diff --git a/challenger/src/main/resources/content/tools/clients/insomnia.md b/challenger/src/main/resources/content/tools/clients/insomnia.md deleted file mode 100644 index 40a9c313..00000000 --- a/challenger/src/main/resources/content/tools/clients/insomnia.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Insomnia HTTP Rest Client - Overview and Review -seo_title: Insomnia API Client Review for Exploratory Testing -description: An overview of the Insomnia HTTP REST API Client. -lastmod: 2026-02-18 -seo_description: Review Insomnia HTTP Rest Client for API testing, including strengths, limitations, and practical fit so you can choose the right client for your workflow. -showads: true ---- - -# Insomnia - -Insomnia is a free to use REST API Client that requires a login to use properly. Paid plans are required to have control of where data is stored. - -## About Insomnia - -[Insomnia](https://insomnia.rest/) used to be my first choice of REST Client for API Testing. Then the company required a login and I switched to Bruno. - -- [Insomnia.rest](https://insomnia.rest/) -- [Insomnia on Github](https://github.com/Kong/insomnia) -- [Insomnia Plugins](https://insomnia.rest/plugins) - -It is possible to use without signing in if you click the `use local scratchpad` option on the login screen. This allows you to work with a single 'scratchpad' collection. I'm not sure where it is stored, but it is possible to export to Insomnia or HAR format. - -- Can Import many formats of API definition, including Open API -- A History of requests can be seen, but cannot be exported. -- cURL is well support for import and generation. -- Environment variables are supported in requests and headers. -- Proxy is easy easily configured. -- Requests can be organised into folders. -- Scripting is available in the free version. -- Custom Methods can be used. -- Plugins can be added for additional features - -After logging in you have the ability to create more collections, which are saved in the cloud and 'locally' somewhere. Git Sync feature where you control the location of collections is only available on a paid plan. - -I was surprised to see that Insomnia, although requiring a login for the most part, was still a capable exploratory testing client. - -## Quick Review Criteria - -| Feature | Y/N | Comment | -|-----------------------------------------------------|-----|-----------------------------------------------------------------| -| **Essential** | | | -| Send HTTP or HTTPS requests | Y | | -| View the actual requests and responses | Y | In the console view | -| Proxy support | Y | | -| Can create invalid requests | Y | | -| **Optional** | | | -| Read Open API files | Y | | -| Supports global environment variables e.g. for host | Y | Editable from a JSON object view | -| Different payload body types | Y | | -| Easy to add headers | Y | | -| - custom headers | Y | | -| - auth header support | Y | | -| - override body type headers | Y | It is possible to edit as JSON and set content-type header to XML | -| Variables in headers | Y | | -| Custom Verb support | Y | | -| Repeating or cloning earlier requests | Y | From the hierarchical view | -| View history of requests/responses | Y | A History is stored but not exportable | -| Exporting requests to cURL format | Y | | -| Sends Diagnostic information to server | N | Does not seem to send diagnostic information | -| **Bonus** | | | -| Data driven requests | Y | From the 'run' dialog where a full set of requests is executed | -| Output log of test sessions | N | | -| Scriptable for customisation | Y | | -| Importing cURL requests | Y | | -| Free for commercial use | Y | Additional features are available on paid plan | - -Insomnia has a very clean and uncluttered UI and is still a useful API client. - -## Summary - Easy to use. Scratchpad mode for non-logged in use. - -Requires login to get multiple collections. And a Paid plan to control where collections are saved. - -Most features seem to be available on the free plan. - -I prefer the convenience of saving locally with Bruno, but Insomnia remains a very capable REST API Client and supports Exploratory Testing well. - -## Review Notes - -Importing an Open API file automatically filled in the environment variables. - -History of requests sent is maintained, but cannot be exported. - -It is possible to convert requests to cURL. - -It is also possible to import from curl, you have to use the `(+)` symbol next to the filter. - -It is possible to create variables in the environment editing. This is done by editing a raw json object rather than a table like dialog view. - -Easy to configure a proxy via the preferences dialog. - -It is possible to organise requests into folders. - -Scripting is available in the free version. - -It is possible to create Custom Methods. - -In scratchpad mode, to save your work you would have to export the collection. - -You can export in Insomnia 5 format or HAR format. - -It is possible to run all the requests in a collection and order them prior to sending. - -Body payloads have syntax highlighting and it is possible to send invalid data. It is also possible to override the content type header - -Header names have text completion, the values do not. - -It is possible to use Insomnia in scratchpad mode as a capable free exploratory testing client. - -When logged in it is possible to work on multiple collections. And these can be locally stored. I am not sure where they are locally stored so they are not suitable for sharing. The Git Sync feature where you can choose the folder is part of the Team Plan. It is possible to store your projects in cloud and collaborate, but I think only for 1 project. - -Insomnia is still a capable tool for local exploratory testing and free to use. Can be used without login in scratchpad mode, you just don't get shared collections online. - diff --git a/challenger/src/main/resources/content/tools/clients/katalon.md b/challenger/src/main/resources/content/tools/clients/katalon.md deleted file mode 100644 index 925e24fd..00000000 --- a/challenger/src/main/resources/content/tools/clients/katalon.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Katalon Studio HTTP Rest Client - Overview and Review -seo_title: Katalon Studio HTTP REST Client Review | API Client Review -description: An overview of the Katalon Studio HTTP REST API Client. -lastmod: 2026-02-18 -seo_description: Review Katalon Studio HTTP Rest for API testing, including strengths, limitations, and practical fit so you can choose the right client for your workflow. -showads: true ---- - -# Katalon Studio - -Katalon Studio has a free plan. Free, user-friendly test automation tools to simplify testing for individuals and small teams. It does not support Exploratory Testing well. - -## About Katalon Studio - -[katalon Studio Free](https://katalon.com/download-studio-free) is advertised as a tool for test automation. - -So to be fair to Katalon, I'm probably evaluating it for the wrong use case as it does not show up in searches for "REST API Testing Client". - -However, part of the feature set is working with Open API files and manually issuing REST API calls prior to automating them. - -I chose to evaluate it because it does appear in other recommended lists for API Testing Tools. - -I found it to be too slow at sending requests manually to be usable as an Exploratory Testing tool. - -- Can not import Open API files v3 on free plan without editing the file. -- Slow to send requests. -- Clumsy login and account creation process which is required to use the tool. -- No obvious proxy support. -- Not designed to support Exploratory Testing. - - -## Quick Review Criteria - -I didn't fully review the tool hence the incompleteness of the table. I found it was too slow to send requests and so abandoned the review. - - -| Feature | Y/N | Comment | -|-----------------------------------------------------|-----|-------------------------------------------------------------------| -| **Essential** | | | -| Send HTTP or HTTPS requests | Y | | -| View the actual requests and responses | Y | In the HAR view | -| Proxy support | N | | -| Can create invalid requests | Y | | -| **Optional** | | | -| Read Open API files | Y | Not Open API v3 | -| Supports global environment variables e.g. for host | ? | | -| Different payload body types | Y | | -| Easy to add headers | Y | | -| - custom headers | Y | | -| - auth header support | Y | | -| - override body type headers | Y | It is possible to edit as JSON and set content-type header to XML | -| Variables in headers | ? | | -| Custom Verb support | N | | -| Repeating or cloning earlier requests | Y | But not from the History, only the Request UI | -| View history of requests/responses | N | Only last request and responses can be viewed. | -| Exporting requests to cURL format | N | | -| Sends Diagnostic information to server | ? | Not configurable through proxy to check | -| **Bonus** | | | -| Data driven requests | ? | | -| Output log of test sessions | N | | -| Scriptable for customisation | ? | -| Importing cURL requests | N | | -| Free for commercial use | Y | | - - -## Summary - Not designed for Exploratory Testing, designed for Automated Execution of Requests - -I don't think the tool was designed to support Exploratory Testing. It was frustratingly slow to send requests and I abandoned the review. - -It has so many features related to automating that I should just not consider it an option for supporting exploratory testing. - -## Review Notes - -[katalon.com](https://katalon.com) - -I downloaded Katalon Studio Free - -https://katalon.com/download-studio-free - -And the first thing I have to do is login. Which is a big barrier to entry. - -I created an account because I didn't trust the login with Google and Github dialogs. I would have preferred pop up, normal Oauth login. - -This also meant going through an annoying "I am not a robot" process, clicking on images. - -First thing I tried to do was import an OpenAPI File. - -"You are importing OpenAPI 3, which requires a Studio Enterprise License" - -I managed to import a file using the same approach required for SoapUI Open API json importing. Requests are then found in the Object Repository. - -Requests were very slow to send. I had to open Bruno and issue a request to check that it was the Katalon tool and not the server. It was the Katalon tool. - -It is not possible to view the actual request sent, only one response is shown, there sees to be no history log. It is possible to export the request and response from a HAR view, so this is where you can see the request. - -No headers are added to the request by default according to the HAR output. - -Code completion is available on header names and values via drop down UI elements. - -I tried to create an adhoc Rest API Request, but could not send it for some reason. No errors were reported on screen. I could only use the requests imported from the amended Open API file. - -I could not find a way to change Proxy. - -Since the requests were frustratingly slow to send I gave up. - -This is clearly not a tool designed to support interactive exploratory testing. It has so many features related to automating that I should just not consider it an option for supporting exploratory testing. diff --git a/challenger/src/main/resources/content/tools/clients/kreya.md b/challenger/src/main/resources/content/tools/clients/kreya.md deleted file mode 100644 index bc9fed94..00000000 --- a/challenger/src/main/resources/content/tools/clients/kreya.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Kreya HTTP API Rest Client - Overview and Review -seo_title: Kreya API Client Review for Exploratory Testing -description: An overview of the Kreya HTTP REST API Client. -lastmod: 2026-02-18 -seo_description: Review Kreya HTTP API Rest Client for API testing, including strengths, limitations, and practical fit so you can choose the right client for your workflow. -showads: true ---- - -# Kreya - -Kreya is a Desktop Open Source API client. Free to use for commercial environments with extra features available on a paid plan. Harder to configure proxy for exploratory testing. - -## About Kreya - -Kreya has a free forever plan and I believe can be used in a commercial environment - -[kreya.app](https://kreya.app) - -A very capable API client but the proxy configuration is a little hard to setup as it can only be configured using environment variables, and then all application traffic is visible. - -**Warning: Kreya sends usage diagnostic information back to company e.g. what types of methods and features are used. No Payloads or URLs are sent. If this is an issue for your company then you probably should not evaluate. The diagnostic information is visible when you configure Kreya to work with a Proxy.** - -It can import Open API files, but I had to amend the collection afterwards because the default base url had been configured incorrectly. - -I don't like that I cannot 'see' the actual request that is sent. I was able to view it in proxy but not in the UI. - -There are no script capabilities in the free version. But the templating feature is powerful, and it is possible to use it for basic scripting. - -Environment variables were a little hard to amend, because a JSON view is shown, but it is possible and the variables can be used in request and headers. - -This is only the second tool I've seen with Test Data generation. Faker and Bogus are built in. I have no idea how to use Bogus in this tool as the documentation does not have examples and only `faker.` works in the code completion of the template payloads. [more info](https://kreya.app/docs/templating/#generating-fake-data) - -I was able to use the faker capability to generate random data to feed into the simpleapi - -``` -{ - "type": "cd", - "isbn13": "{{ faker.random.replace_numbers "#############" #}}", - "price": 34.2 -} -``` - -It is possible to send custom methods. - -Two things make this hard to use: - -- inability to see the requests sent -- hard to configure the Proxy - -If it was easy to configure the proxy then it wouldn't really matter if I couldn't see the request sent. But templates are hard to debug if you can't see the data generated in the request. - -But it is easier to configure the proxy for Kreya than build Yaak from source, so Kreya is part of my recommended list (provided you do configure it to use a proxy). - -## Quick Review Criteria - -| Feature | Y/N | Comment | -|-----------------------------------------------------|-----|--------------------------------------------------------------------------| -| **Essential** | | | -| Send HTTP or HTTPS requests | Y | | -| View the actual requests and responses | N | Last Response only. Request can not be viewed. | -| Proxy support | Y | Hard to configure the proxy as it requires setting environment variables | -| Can create invalid requests | Y | | -| **Optional** | | | -| Read Open API files | Y | Minor edits required after loading from a URL | -| Supports global environment variables e.g. for host | Y | | -| Different payload body types | Y | | -| Easy to add headers | Y | | -| - custom headers | Y | | -| - auth header support | Y | | -| - override body type headers | Y | It is possible to edit as JSON and set content-type header to XML | -| Variables in headers | Y | | -| Custom Verb support | Y | | -| Repeating or cloning earlier requests | Y | from the collection list | -| View history of requests/responses | N | Only last response can be viewed | -| Exporting requests to cURL format | N | | -| Sends Diagnostic information to server | Y | Sends diagnostic information. No configuration to stop this. | -| **Bonus** | | | -| Data driven requests | N | | -| Output log of test sessions | N | | -| Scriptable for customisation | N | Scripting and Tests available in Pro plan | -| Importing cURL requests | N | | -| Free for commercial use | Y | | - - -## Summary - Recommended if you use it through a proxy - -If the proxy was easier to use, and the request was visible in the UI then Kreya would be easier to recommend. - -I think it is a perfectly capable in its free configuration if you use it through a proxy. Its just a shame the Proxy isn't configurable from a dialog in the UI. - -But be warned. When used through a Proxy you will see that Kreya is sending usage diagnostic information to the home server. - -## Kreya Proxy Support - -I did manage to get kreya working with Proxy, but unfortunately all requests are sent through the proxy e.g. the messages the app sends to its server as well as the HTTP requests. - -I had to start a command line, create the environment variables and then run the tool from the command line `"C:\Program Files\Kreya\Kreya.exe"` - -``` -set HTTP_PROXY=http://127.0.0.1:8080 -set HTTPS_PROXY=http://127.0.0.1:8080 -``` - -To clear env variables on Windows: - -``` -set HTTP_PROXY= -set HTTPS_PROXY= -``` - -## Open API Support - -I imported an Open API file from a URL, but then each request used the imported Open API file url host was set to the import file i.e. `https://apichallenges.eviltester.com/simpleapi/docs/swagger?permissive` - -So it was making requests to https://apichallenges.eviltester.com/simpleapi/docs/simpleapi/items - -I had to amend the REST settings at the collection level. - -Without proxy support I might not have found this quickly because there is no history of requests made (in the free version). - -The Open API import did not pick up the environments in the file so the base url needs to be set at the collection level. - -But it does support Open API, which is good. - diff --git a/challenger/src/main/resources/content/tools/clients/milkman.md b/challenger/src/main/resources/content/tools/clients/milkman.md deleted file mode 100644 index b45a3ede..00000000 --- a/challenger/src/main/resources/content/tools/clients/milkman.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: MilkMan HTTP API Rest Client - Overview and Review -seo_title: Milkman API Client Review for Exploratory Testing -description: An overview of the MilkMan HTTP REST API Client. -lastmod: 2026-02-18 -seo_description: Review MilkMan HTTP API Rest Client for API testing, including strengths, limitations, and practical fit so you can choose the right client for you. -showads: true ---- - -# Milkman - -Milkman is a Desktop Open Source API client. Completely open source and community driven. - -## About Milkman - -Milkman is one of the few Open Source projects with no commercialisation attached. So it is free to use in a commercial environment, no features are locked away and no licensing issues. - -Because it is a community project it is a little rough around the edges but it is well featured and performed well in my evaluation. - -I installed Milkman by downloading the nightly release zip files. - -The basic concept of Milkman is a Workbench that is highly extensible through community contributed plugins. Milkman is a JavaFX application and should run on windows, mac and linux. - -[Milkman](https://github.com/warmuuh/milkman) - -Since this works, and is completely open source, it is a viable tool for Exploratory Testing. The main concern is the storage of collections in a database in the app folder, so make sure you export the collections regularly to Postman format to version control them. - -- Can import Open API files from disk -- Uses the environment variables to configure environments listed in Open API file -- Json responses can be queried using [JMESPath](https://jmespath.org/) -- Possible to have simple scripting using [plugins](https://github.com/warmuuh/milkman/blob/master/milkman-scripting/readme.md) -- Can configure a proxy -- Can add custom headers. Header names do have auto completion. Header values do not. -- it is very easy to send through invalid requests -- Auth looks like it needs to be handled manually by crafting headers -- It is possible to export a collection as a postman collection -- Possible to create environment variables -- Content type header is not automatically added after choosing a content-type in the editor, so configure header manually -- Can not see the actual request sent -- Custom methods are not supported -- Collections are saved to the database.db in the application folder when the application exits, rather than plain text files - -Shame this doesn't save to local files that are easy to version control like Bruno. But since this can work with a proxy I could consider using this as an emergency API client for exploratory testing. Or a fall-back tool, just in case all the other easy to use tools lock basic features behind a paywall or registration wall. - -## Quick Review Criteria - -| Feature | Y/N | Comment | -|-----------------------------------------------------|-----|----------------------------------------------------| -| **Essential** | | | -| Send HTTP or HTTPS requests | Y | | -| View the actual requests and responses | N | Request can not be viewed. | -| Proxy support | Y | Easy to configure e.g. `http://127.0.0.1:8080` | -| Can create invalid requests | Y | | -| **Optional** | | | -| Read Open API files | Y | from disk | -| Supports global environment variables e.g. for host | Y | | -| Different payload body types | Y | All headers are manually configured | -| Easy to add headers | Y | | -| - custom headers | Y | | -| - auth header support | N | Can send Auth headers. Manually configure only | -| - override body type headers | Y | No header type set by default. Manually configure. | -| Variables in headers | Y | | -| Custom Verb support | N | | -| Repeating or cloning earlier requests | Y | Duplicate option available on tab right click | -| View history of requests/responses | N | Only last response can be viewed | -| Exporting requests to cURL format | Y | Save > export... | -| Sends Diagnostic information to server | N | Does not seem to send diagnostic information | -| **Bonus** | | | -| Data driven requests | N | | -| Output log of test sessions | N | | -| Scriptable for customisation | Y | Not evaluated, but capability is there | -| Importing cURL requests | N | | -| Free for commercial use | Y | | - -## Summary - Recommended as Completely Open Source and Free - -Given that it is completely free and open source, has proxy support and can handle Open API files. Milkman has to be an option if looking for completely free tooling. - - diff --git a/challenger/src/main/resources/content/tools/clients/postman.md b/challenger/src/main/resources/content/tools/clients/postman.md deleted file mode 100644 index 14cca878..00000000 --- a/challenger/src/main/resources/content/tools/clients/postman.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Postman HTTP Rest Client - Overview and Review -seo_title: Postman API Client Review for Exploratory Testing -description: An overview of the Postman HTTP REST API Client. -lastmod: 2026-02-18 -seo_description: Review Postman HTTP Rest Client for API testing, including strengths, limitations, and practical fit so you can choose the right client for your workflow. -showads: true ---- - -# Postman - -Postman is a free to use REST API Client that requires a login to use properly. It has many features and this can make the UI hard to get used to. - -## About Postman - -When [Postman](https://www.postman.com/) was first released, it was a very simple Electron application which could create a collection of requests and make them easy to send. It was perfect for Exploratory Testing. There were few competitors and I remember paying around $10 to get an early version of cloud storage to try and support the project. - -Now it is one of the largest API Suites. This has the side effect that it has more features than you will use, crammed into a UI, which I find makes it a little cluttered and harder to use. I tend to choose a simpler client for Exploratory Testing. - -I have also had to email Postman support in the past to clarify their license agreement. Now the website is clearer and it is obvious that the API Client can be used for free in commercial environments no matter the team size. - -- Import many collection formats -- Export postman formats -- Easy to configure proxy -- Simple variable support -- Most features required for exploratory testing in free plan -- Scripting available in free plan. -- Will probably be the default client for most people. -- Feature Cluttered UI that takes time to get used to. -- Collections stored in cloud, limited sharing on free plan. Be careful what you make public. - - -## Quick Review Criteria - -| Feature | Y/N | Comment | -|-----------------------------------------------------|-----|------------------------------------------------------------------| -| **Essential** | | | -| Send HTTP or HTTPS requests | Y | | -| View the actual requests and responses | N | Can see the last response, but not the full actual raw request. | -| Proxy support | Y | | -| Can create invalid requests | Y | | -| **Optional** | | | -| Read Open API files | Y | | -| Supports global environment variables e.g. for host | Y | Editable from a JSON object view | -| Different payload body types | Y | | -| Easy to add headers | Y | | -| - custom headers | Y | | -| - auth header support | Y | | -| - override body type headers | Y | It is possible to edit as JSON and set content-type header to XML | -| Variables in headers | Y | | -| Custom Verb support | Y | | -| Repeating or cloning earlier requests | Y | From the hierarchical view or history view. | -| View history of requests/responses | Y | A History is stored but not exportable | -| Exporting requests to cURL format | Y | | -| Sends Diagnostic information to server | N | Does not seem to send diagnostic information | -| **Bonus** | | | -| Data driven requests | Y | From the 'run' dialog where a set of requests is executed | -| Output log of test sessions | N | | -| Scriptable for customisation | Y | | -| Importing cURL requests | Y | | -| Free for commercial use | Y | Additional features are available on paid plan | - -Postman is probably the most fully featured of all the clients, but I find the UI cluttered and the login requirement annoying. I am always slightly paranoid that my collection is being saved in the public workspace. I prefer to work locally. But this seems to be the most popular API Client, or perhaps just has the largest marketing department, and the Postman collection format is becoming a standard export and import format. - -## Summary - Free to use. Requires logging in and Practice to get the most of. - -The non-login 'lightweight API Client' is like the Endpoint Explorer of [Soap UI](/tools/clients/soapui) or [Cartero](/tools/clients/summary-reviews). This can be used for very adhoc testing. - -Requires login to get the most of. It is a very capable tool and you get a lot for free, but I find the UI to be cluttered and hard to use. I'm sure you get used to it if you use it as your main API Client, but I don't use all the features so I tend to use a simpler tool. - -## Review Notes - -[getpostman.com](https://getpostman.com) - -Without logging in you get access to the 'lightweight API Client' which allows you to paste in cURL Requests, or create basic requests. - -It is like the Endpoint Explorer from SoapUI but much more usable. - -- custom methods -- import/export cURL -- history of requests and responses sent (but not exportable) -- configure proxy in settings - -This mode is suitable for very adhoc exploratory testing. - -You cannot import or export the session as a collection so it is very adhoc and lightweight. - -Logging in gives access to more features. - -I do find the Postman UI confusing because it tries to cram so much functionality into a tiny space. - -Sometimes the send button says 'try' and then sometimes it is 'send'. - -It is not possible to see the actual request sent. - -The curl snippet does not match the exact request sent from Postman because postman adds additional headers. - -The history view by default does not save the response by default. So you have to configure the save response setting. Also the 'actual' request with the custom postman headers it not saved e.g the postman token. It isn't a raw request history. - -It is possible to re-issue requests from the history view, where it acts like the lightweight API Client but by amending prior to resending you amend the previously sent item and then send it as new one. I found this all a little confusing. - -One thing you have to be careful about is to avoid accidentally adding items to your public workspace. - -It is possible to export collections to disk. - -I'm sure, with time, I could get used to the UI, but I don't need the complexity on offer here. - -Looking through the pricing and licensing page https://www.postman.com/pricing/ it does seem as though there has been an effort to clean this up and make it less ambiguous. The free plan does seem to be completely free, for any sized companies. The free plan limits the number of people you can collaborate with via cloud projects, but since you can export collections and save them locally you can share the collection with as many people as you want. The 'teams of 3' text is just an example, not a license condition. - diff --git a/challenger/src/main/resources/content/tools/clients/soapui.md b/challenger/src/main/resources/content/tools/clients/soapui.md deleted file mode 100644 index 7bf3fca9..00000000 --- a/challenger/src/main/resources/content/tools/clients/soapui.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: Soap UI HTTP Rest Client - Overview and Review -seo_title: SoapUI API Client Review for Exploratory Testing -description: An overview of the Soap UI HTTP REST API Client. -lastmod: 2026-02-18 -seo_description: Review Soap UI HTTP Rest Client for API testing, including strengths, limitations, and practical fit so you can choose the right client for your workflow. -showads: true ---- - -# SoapUI - -SoapUI is an Open Source API client designed for local use. One of the earliest HTTP clients, and is now showing its age. - -## About SoapUI - -[SoapUI](https://www.soapui.org/) is an Open Source API client. The paid version with additional features is now called RapiAPI. - -SoapUI was one of the early HTTP clients and I remember using it to test Soap APIs. SoapUI is written in Java and has a slightly confusing UI with multiple floating windows within the main Desktop UI. - -- [SoapUI official site](https://www.soapui.org/) -- [SoapUI on github](https://github.com/SmartBear/soapui) - -- Can not import Open API files v3 without editing the file to change `"openapi":"3.0.1"` to `"swagger":"2.0"` -- Text feels small on a large monitor with no way to configure. -- A little clumsy to get started with. -- Can view raw requests -- Can view and save a log of historic requests and responses. -- Support for Auth headers -- Other headers add manually to request -- Can not issue custom methods -- Can be configured through a proxy. -- Can create adhoc load tests from requests. -- Sends diagnostic usage data to `mixpanel.com` with no way to switch this off e.g. what verbs are sent, other UI usage. - -## Quick Review Criteria - -| Feature | Y/N | Comment | -|-----------------------------------------------------|-----|-------------------------------------------------------------------| -| **Essential** | | | -| Send HTTP or HTTPS requests | Y | | -| View the actual requests and responses | Y | Raw requests in UI | -| Proxy support | Y | | -| Can create invalid requests | Y | | -| **Optional** | | | -| Read Open API files | Y | Does not support Open API 3 | -| Supports global environment variables e.g. for host | N | | -| Different payload body types | Y | | -| Easy to add headers | Y | | -| - custom headers | Y | | -| - auth header support | Y | | -| - override body type headers | Y | It is possible to edit as JSON and set content-type header to XML | -| Variables in headers | N | | -| Custom Verb support | N | But has more than most cleints | -| Repeating or cloning earlier requests | Y | From the Request UI | -| View history of requests/responses | Y | Shown in HTTP Log, which can be exported to file | -| Exporting requests to cURL format | N | | -| Sends Diagnostic information to server | Y | Diagnostic information sent to mixpanel visible in proxy. | -| **Bonus** | | | -| Data driven requests | N | | -| Output log of test sessions | N | | -| Scriptable for customisation | N | 'tests' can be scripted | -| Importing cURL requests | N | | -| Free for commercial use | Y | The paid version is a different product | - - -## Summary - Works, but sends diagnostic usage information - -Once you get used to the UI it does work, but there are now easier tools to use. This would not be my default tool. - -I don't like the diagnostic information being sent to mixpanel by default, there should be an option to switch this off. - -I can see the adhoc load tests being useful - -## Review Notes - -[SoapUI](https://www.soapui.org/) is now two products. SoapUI is free and open source. ReadyAPI is a paid product with more features: data-driven testing, coverage testing, scripting, etc. - -I'll be reviewing SoapUI, the free open source version. - -[SoapUI on github](https://github.com/SmartBear/soapui) - -Written in Java, can be run from source if you want. Has an easy to use installer. - -When SoapUI starts it jumps straight into an Endpoint Explorer allowing you to make basic API Requests without creating a collection. Unfortunately it is a modal dialog and SoapUI takes over the main screen so I chose not to show this window on launch. - -To import an Open API file I created a new REST Project, then it is possible to import Open API files from the project menu or context menu. - -You can only import from URL if the API is on swagger Hub, but you can import from file, so I downloaded the Open API File and tried to import it. - -But received an error: - -``` -java.lang.NullPointerException: Cannot invoke "com.smartbear.swagger.SwaggerImporter.importSwagger(String)" because "this.val$importer" is null -``` - -This was the same file that I've imported into multiple clients during the review process. - -I found a bug report on the community for 2 years ago: - -https://community.smartbear.com/discussions/soapui_os/problem-with-importing-openapi/242050 - -Suggesting that json files need to be amended to change `"openapi":"3.0.1"` to `"swagger":"2.0"` - -That did work, but this is clearly a long standing bug in the product. And not a good first sign. - -The environments were not picked up from the file so I amended the project in the UI to have a base Path. - -At this point I notice how clumsy the UI feels. The text is small and there are too many floating windows inside the internal desktop window. - -I then realised that I had made a mistake, I should not have added `https://apichallenges.eviltester.com` to the base path, I need to add that as the `Endpoint` in the request view. - -Having done that I was finally able to make a GET request on `/simpleapi/items` - -The raw request is visible in the dialog in the Raw tab. - -And can be viewed in the http log tab. This log can be saved to a file, which is good. The first tool I've encountered in this review process which can do this out of the box. - -Headers are not added by default so I added an `accept` header. There is no code completion on header names or values. - -Adding an auth header is done via a dialog so is easy to configure. - -The payload editing part of the dialog is not available with GET requests so I can't create an 'invalid' GET request with a payload. - -There is no syntax checking in the editor . It is possible to override the Media type in the editor with a header, and the media type can be customised in the editor itself. - -I could not find a way to create project level variables that I could share across requests. - -Can not issue custom methods - -I was able to configure a proxy fairly easily. I had to scroll up in the dialog to see the manual button. But when I did, I also saw that the application was sending usage information to mixpanel.com. The information is base64 encoded and it sends information about the Method used, but not the content of the request. I could see no way of switching this off. It makes me uncomfortable to see this and I don't imagine that it is acceptable in many commercial environments. - -I can see that it is possible to add requests into a TestSuite and create assertions for them. - -For exploratory testing the more useful option is the creation of adhoc load tests. - -The EndPoint explorer is a fairly fast way to issue random requests, feels a lot like Cartero but SoapUI has no cURL support. - -Cons: - -- does not import Open API v3 files in JSON format without amending file -- UI is a bit clunky, -- takes time to get used to -- no shared environment variables -- cannot switch off product usage data sent to mixpanel.com -- free and open source - -Pros: -- can make basic requests -- can work through a proxy -- can create collections -- can create adhoc load tests -- can output the http log for a test session -- no cURL support - diff --git a/challenger/src/main/resources/content/tools/clients/summary-reviews.md b/challenger/src/main/resources/content/tools/clients/summary-reviews.md deleted file mode 100644 index f8b3ec98..00000000 --- a/challenger/src/main/resources/content/tools/clients/summary-reviews.md +++ /dev/null @@ -1,311 +0,0 @@ ---- -title: REST API Client Suitability for Exploratory Testing Summary Reviews -seo_title: Exploratory Testing API Client Reviews and Comparisons -description: A list REST Clients with a summary review of each concerning their support for Exploratory Testing. -lastmod: 2026-02-18 -seo_description: Review REST API Client Suitability for API testing, including strengths, limitations, and practical fit so you can choose the right client for you. -showads: true ---- - -# REST API Client Support for Exploratory Testing Summary Reviews - -Reviews of REST API Clients covering how well the tool supports Exploratory Testing. - -## Introduction to Exploratory Testing Support Reviews - -There are so many REST Clients available I have tried to list all those that I have evaluated here, with a summary of the evaluation results. The evaluation criteria was 'how well does the tool support exploratory testing'. If you also use the REST API Client for 'Test Automation' then you will evaluate and rank these tools differently. - -The main headings are listed in alphabetic order. To see a recommended grouping check the section below. - -For Exploratory Testing I use Bruno. I evaluated the tools below to make sure I have alternative tools and know the limits of each tool. - -*Tools last evaluated in April 2025.* - -A list of Open Source API Clients is available at the [Awesome API Clients Github list](https://github.com/stepci/awesome-api-clients). Many of desktop tools listed are reviewed below. - -## Grouped Recommendations for Exploratory Testing - -If you are evaluating a tool for Exploratory Testing then start with those listed in this section. - -### Recommended Lightweight Free Clients - -- Bruno [review](/tools/clients/bruno) - good all-rounder, free for commercial use. **Recommended** [usebruno.com](https://usebruno.com) -- Milkman [review](/tools/clients/milkman) - a little rough around the edges, but open source and free for commercial use [Milkman on github](https://github.com/warmuuh/milkman) -- Kreya [review](/tools/clients/kreya) - proxy can only be configured via environment variables but capable when configured [Kreya](https://kreya.app) -- Yaak [review](/tools/clients/yaak) - lacks scripting, free for commercial use if run from source. [yaak.app](https://yaak.app/) - -### Recommended Heavyweight Free Clients - -- Insomnia - [review](/tools/clients/insomnia) - easy to use without logging in, login required for multiple collections [Insomnia.rest](https://insomnia.rest/) -- Postman - [review](/tools/clients/postman) - requires logging in, feature packed, UI takes time to learn [GetPostman.com](https://getpostman.com/) - - -## All Evaluated Client Summaries - -All recommended clients have longer reviews. - -I added tools in here that I do not recommend, simply because they are available, if you search the web you will find them. If you do have good experiences with tools that I have not recommend then feel free to [let us know](https://www.linkedin.com/in/eviltester). - -### Advanced Rest Client - -- [Advanced Rest Client on Github](https://github.com/advanced-rest-client) -- Hasn't been updated in a few year -- I tried to open an Open API file and it failed. - -Since there are so many other Open Source and free API clients, I do not recommend. - -### API Dash - -[API Dash on github](https://github.com/foss42/apidash/releases) - -- Free and open source desktop client - last release Nov 2023 -- Super Simple. Create collections, but I have no idea where they are saved. -- Can generate cURL commands -- No proxy support. -- No Open API import support. -- No environment variable support -- Cannot see history of requests and responses -- Cannot see the request sent -- no scripting capabilities -- no custom methods - -Would not recommend given the availability of other tools. Does not seem to be under active development. - -### Bruno - -- Bruno is Open Source. -- Free for commercial use. -- Aids Exploratory Testing well with good Proxy support. -- Free plan also allows scripting. -- Paid plans for CI usage, data driven and automated assertions -- Good cURL integration with both export and import capabilities. -- Provides Open API import, and other import formats supported. -- [usebruno.com](https://usebruno.com) - -Read our [full review of Bruno](/tools/clients/bruno) - -### Cartero - -I'm tempted to view Cartero more like a front end for creating cURL requests rather than a REST API Client. It seems to build cURL requests quite well. - -- Allows you to create single requests, which are stored in files. -- Cannot see a way to set a proxy. -- No concept of collections. -- You can't copy requests, but you could 'save as' new file and then load it back. -- No concept of environment variables. -- It does allow adding headers and parameters but there is no auto complete. -- It does allow exporting to cURL -- [Cartero](https://cartero.danirod.es/) - - -### HTTPIE - -- Free for commercial use -- Requires a login for basic functionality like 'choosing where to save files' -- Does not support HTTP Proxy -- Does not support Open API import - but can import from Postman and Insomnia -- Pretty UI -- Login for experimental Generative AI features -- cURL Support -- Can use Custom Methods -- No Scripting -- No request and response history -- [Httpie](https://httpie.io) - -Requires Open API import and Proxy Support to be viable for supporting Exploratory Testing. - -Read our [full review of Httpie](/tools/clients/httpie) - -### HttpMaster - Windows Only - -- Completely free -- Windows only. -- Lots of features. -- I found the UI Cluttered and hard to use. -- Given the amount of competition available there are other tools I would choose. -- [HttpMaster.net](https://www.httpmaster.net/) - - -### Httpyac - -- VS Code plugin tool -- Write and Issue API requests from text file -- Allows the creation of a literate HTTP testing spec -- Scripting and Request Chaining capabilities -- To get the proxy working I had to configure vs code `preferences > settings` to 'on' and not 'override'. -- I have an example `.http` file in the thingifier repo: [httpyac example](https://github.com/eviltester/thingifier/tree/master/docs/httpyac) -- [Httpyac](https://httpyac.github.io/) - -### Insomnia - -An easy to use client for adhoc exploratory testing using scratchpad mode. Login is required for multiple collections. - -- Can Import many formats of API definition, including Open API -- A History of requests can be seen, but cannot be exported. -- cURL is well support for import and generation. -- Environment variables are supported in requests and headers. -- Proxy is easy easily configured. -- Requests can be organised into folders. -- Scripting is available in the free version. -- Custom Methods can be used. -- [Insomnia.rest](https://insomnia.rest/) - -Read our [full review of Insomnia](/tools/clients/insomnia) - -### Katalon Studio - -Based on my evaluation, I don't think Katalon Studio should be viewed as an Exploratory Testing tool. It seems to be aimed at test automation and should be evaluated in those terms. - -- Requires Login -- Clumsy sign up process -- Free version does not support Open APi 3 files fully -- Slow to send requests -- Recommend evaluating it as a Test Automation tool, not an Exploratory Testing tool. -- [Katalon Studio](https://katalon.com/katalon-studio) - -Read our [full review of Katalon](/tools/clients/katalon) - - -### Kreya - -Kreya would be much easy to recommend with simpler Proxy configuration. But it is easier to configure the proxy for Kreya than build Yaak from source. - -- A free forever plan and I think can be used in a commercial environment -- Can import Open API files -- Can use a proxy but only if set environment variables. -- No script capabilities in the free version. -- A templating feature is avaiable with some ability to script. -- Faker is built in to generate random data -- Can use custom methods. -- **Warning: Kreya sends usage diagnostic information back to company e.g. what types of methods and features are used. No Payloads or URLs are sent.** -- [Kreya](https://kreya.app) - -Read our [full review of Kreya](/tools/clients/kreya) - -### MilkMan - -I installed milkman by downloading the nightly release zip files. A completely free and Open Source REST API Client with collection and Proxy support. - -- MilkMan is a JavaFX application and should run on windows, mac and linux. -- Import Open API files from disk -- Can not see the actual request sent -- Json responses can be queried using JMESPath -- Supports simple scripting using [plugin](https://github.com/warmuuh/milkman/blob/master/milkman-scripting/readme.md) -- Can configure a proxy -- All Headers are manually configured. Can add custom headers. Header names do have auto completion, header values do not -- Not support custom methods -- Collections are saved to the database.db in the application folder when the application exits. Exportable to Postman. -- Environment variable support. -- [Milkman](https://github.com/warmuuh/milkman) - -Read our [full review of Milkman](/tools/clients/milkman) - -### Paw - Mac Only - -- Paw is now called Rapid API -- Free, not open source -- Mac only tool -- Simple to use HTTP API Client -- [paw.cloud](https://paw.cloud/) - -A good choice if you are on Mac. I used this a lot for Exploratory Testing when I was primarily Mac OS based. - -### Postman - -- Import many collection formats -- Export postman formats -- Easy to configure proxy -- Simple variable support -- Most features required for exploratory testing in free plan -- Scripting available in free plan. -- Will probably be the default client for most people. -- Feature Cluttered UI that takes time to get used to. -- Collections stored in cloud, limited sharing on free plan. Be careful what you make public. -- [GetPostman.com](https://getpostman.com/) - -Read our [full review of Postman](/tools/clients/postman) - - -### Pororoca - -- I had huge problems with this application. -- I could import an Open API collection, but then I could not make any requests. -- Most of the time when I clicked on an imported request the UI would go blank. -- When I finally did manage to click Send for a valid URL, the application shut down. -- From what I saw, there was no way to set a proxy. -- And it didn't look like a history of requests was maintained. -- [Pororoca](https://pororoca.io/intro/download) - -Perhaps I downloaded an unluckily buggy version but I can't recommend. - -### REST Client - -Listed here but requires future evaluation. These are initial notes. - -- Rest Client seems to be very similar to Httpyac -- Rest Client seems to support a subset of curl so it might be possible to write cURL directly into the editor. -- Does not get updated very often, last commit (October 2024) -- Seems like a very interesting tool. Evaluate at the same time as HttpYac -- [Rest Client](https://github.com/Huachao/vscode-restclient) - -### SoapUI - -- does not import Open API v3 files in JSON format without amending file -- UI is a bit clunky, -- takes time to get used to -- no shared environment variables -- cannot switch off product usage data sent to mixpanel.com -- free and open source -- can make basic requests -- can work through a proxy -- can create collections -- can create adhoc load tests -- can output the http log for a test session -- no cURL support -- [SoapUI](https://www.soapui.org/) - -Read our [full review of SoapUI](/tools/clients/soapui) - -### Thunder Client VS Code extension - -- The licensing model makes it hard to evaluate. -- A Paid license is required for basic functionality. -- Hard to recommend given the free plan available in Bruno or Yaak -- Evaluate if you want a VS Code based IDE with cheap commercial license -- [thunderclient.com](https://www.thunderclient.com/) - -### Yaak - -- This is a very capable basic client which can be used for exploratory testing quite easily. -- Open Source API client which requires a paid license if pre-built binaries are used in a commercial organisation with more than two people. -- $50 per year per user to be compliant with license. All features available for evaluation period. -- Yaak is completely free for commercial use if run from source. -- Does not support scripting, but does support Request Chaining to populate new requests with data from earlier responses. -- Good cURL integration with both export and import capabilities. -- Provided excellent Open API import, with other import formats supported. -- To get the most out of Yaak for exploratory testing I found that I needed to configure it to use a Proxy because the Requests are not shown in the Client, only the responses. -- Supports custom methods. -- [Yaak](https://yaak.app/) - -Read our [full review of Yaak](/tools/clients/yaak) - - - - - - - - - - - - - - - - - - - - - diff --git a/challenger/src/main/resources/content/tools/clients/yaak.md b/challenger/src/main/resources/content/tools/clients/yaak.md deleted file mode 100644 index 72de2d47..00000000 --- a/challenger/src/main/resources/content/tools/clients/yaak.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Yaak HTTP Rest Client - Overview and Review -seo_title: Yaak API Client Review for Exploratory Testing -description: An overview of the Yaak HTTP REST API Client. -lastmod: 2026-02-18 -seo_description: Review Yaak HTTP Rest Client for API testing, including strengths, limitations, and practical fit so you can choose the right client for your workflow. -showads: true ---- - -# Yaak - -Yaak is an Open Source API client designed for local use. Projects are stored as files so can be shared via Git. - -Free to use for commercial environments if the application is built and run from source. Using the pre-built binaries requires a paid licence. - -## About Yaak - -Yaak is an Open Source API client which requires a paid license if used in a commercial organisation with more than two people, and you want to use the pre-built binaries. $50 per year per user to be compliant with license. - -Yaak is completely free if run from source. - -Yaak can be found at: - -- [Yaak](https://yaak.app/) - -To get the most out of Yaak for exploratory testing I found that I needed to configure it to use a Proxy. This was because the Requests are not shown in the Client, only the responses. - -This is a very capable basic client which can be used for exploratory testing quite easily. - -Does not support scripting, but does support Request Chaining to populate new requests with data from earlier responses. - -Good cURL integration with both export and import capabilities. - -Provided excellent Open API import, with other import formats supported. - -## Quick Review Criteria - -| Feature | Y/N | Comment | -|-----------------------------------------------------|-----|--------------------------------------------------------------------------------------------------------------------------| -| **Essential** | | | -| Send HTTP or HTTPS requests | Y | | -| View the actual requests and responses | Y | Only the Response can be viewed | -| Proxy support | Y | | -| Can create invalid requests | Y | | -| **Optional** | | | -| Read Open API files | Y | Also populates Environment host variables and param variables | -| Supports global environment variables e.g. for host | Y | | -| Different payload body types | Y | | -| Easy to add headers | Y | | -| - custom headers | Y | | -| - auth header support | Y | | -| - override body type headers | Y | It is possible to edit as JSON and set content-type header to XML | -| Variables in headers | Y | | -| Custom Verb support | Y | | -| Repeating or cloning earlier requests | Y | But not from the History, only the Request UI | -| View history of requests/responses | Y | Only responses can be viewed. | -| Exporting requests to cURL format | Y | | -| Sends Diagnostic information to server | N | Does not seem to send diagnostic information | -| **Bonus** | | | -| Data driven requests | N | | -| Output log of test sessions | N | | -| Scriptable for customisation | N | 'Chaining' can be used to 'pull' values from earlier responses. | -| Importing cURL requests | Y | Auto detects when a cUrl command is in the clipboard. | -| Free for commercial use | Y | When run from source MIT License applies. Prebuilt binary requires commercial licence for companies bigger than 2 people | - -## Summary - Recommended but be careful of the licensing - -Yaak met all my basic needs when fed through a Proxy. I tried both the pre-built binary and running from source. - -Be careful to remain in compliance with the licensing because it uses an Honour System. All features are available, but if you use the pre-built download in a commercial environment then you should buy a licence. - -If you are running from source then you are using the MIT License and can use it without payment. - -## Review Notes - -It will import Open API files and during import, setup Global variables for the cloud server. URL params identified in the Open API file were identified and added to the request automatically e.g. `:id` - -Easy to configure routing through a proxy. I configured both the HTTP and HTTPS settings to `127.0.0.1:8080` to fee traffic through BurpSuite. - -There is a history of each response which is viewable, responses can be saved out individually. It is not possible to view the actual request, but hooking it up to a proxy solves that. - -It is possible to send custom methods. - -XML and JSON are pretty printed in the response. It is possible to filter and query XML responses using XPATH. JSONPath can be used to filter JSON responses. - -Code completion is available for header names and values - -Auth can be setup easily. You have to remember to unset it if you use a header because the Auth tab value takes precedence. - -The body header can be overridden. There is no syntax highlighting for the body request. - -It is possible to send invalid requests. - -There are no automated test capabilities. - -The requests and config are stored as yaml files, and the directory to store them can be configured. It is not obvious from the files what the request is e.g. `yaak.rq_cPn7eng93Q.yaml` but at least they can be saved in github easily and yaml makes them easy to review. - -It is unfortunate that a folder can not be configured for the history, and then the history saved or exported, possibly as a HAR file. - -Yaak offers cURL support. Requests can be copied to cURL by right-clicking on the request in the collection. The app detects when a curl command is on the clipboard and automatically prompts you to 'import from cURL'. - -Support for plugins seems to be on the way. - -There is no scripting. But it is possible to chain requests. Chain request does not mean, issue Request 1 then automatically issue Request 2. Chaining Requests means that if Request 1 has been issued then it is possible to use information from the response of Request 1 in a new request. By referencing some path extract function from a response received by an earlier request. It is also possible to use the same mechanism to create global variables which are updated from request responses automatically. e.g. pull out a header value and use in future requests. This is very similar to the approaches used the IDE Rest Clients RestClient and Httpyac). - -This is a very capable basic client which can be used for exploratory testing quite easily. - -If you use the pre-build application in a commercial environment then you are in breach of the license. Be aware of this it is only 'completely free' if you run from source. - -### Running Yaak from Source - -Running from source is a little painful only because of the development dependencies: - -- install node - https://nodejs.org/en - - on windows this will also install the Visual Studio C++ development tools required by Rust -- install rust - https://www.rust-lang.org/tools/install - -Then to be able to run Yaak locally: - -- download the repo zip from https://github.com/mountain-loop/yaak -- unzip into a folder -- in the folder run `npm install` -- `npm run bootstrap` - -The above two processes will take about 15 minutes, but you'll have to check the development requirements when you upgrade to new versions. - -From then on, running `npm start` in the directory will run `Yaak` - -**Note: closing the application the VITE server was still running on port 1420, to allow me to re-run Yaak and stop the VITE server I issued the command `npx kill-port 1420`** - -So it isn't quite as convenient as running the pre-built binary, but if you are using this in a commercial environment with more than two people and want to use it as a free tool. This is what you do. - diff --git a/challenger/src/main/resources/content/tools/proxies.md b/challenger/src/main/resources/content/tools/proxies.md deleted file mode 100644 index f62aff01..00000000 --- a/challenger/src/main/resources/content/tools/proxies.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: HTTP Proxies - Introduction -seo_title: HTTP Proxies - Introduction | API Testing Tools -description: A list of essential HTTP Proxies tools and how you can use HTTP Proxies to improve your REST API Testing. -lastmod: 2026-02-18 -seo_description: Learn how HTTP proxies improve API testing by exposing raw traffic, speeding up debugging, and revealing request details your client may hide. -showads: true ---- - -# HTTP Proxies - -- What is an HTTP Proxy? -- Example HTTP Proxies? -- Why use an HTTP Proxy? -- How to direct REST Client through Proxy? - - Inspect Traffic - - Filter Traffic (System Proxies) - - Port Config - - Replay Request -- Fuzzing - -Exercises: in browser - GET, viewing traffic, Ajax requests - ---- - -## What is an HTTP Proxy? - -- HTTP Proxy captures HTTP Traffic -- Allows replay of requests -- Allows manipulation of responses - ---- - -## Which proxies? - - -- [BurpSuite](https://portswigger.net/burp) - - Free edition more than good enough for API Testing -- [Owasp ZAP](https://www.zaproxy.org/) - - Open Source -- [Fiddler](http://www.telerik.com/fiddler) - - Windows (Beta: Linux, Mac) -- [Charles](https://www.charlesproxy.com/) - - Commercial but allows 30 mins in 'shareware' mode - -Fiddler & Charles act as System Proxies making them easy to use with Postman. - -I tend to rely on BurpSuite. - ---- - -## Why use when Testing API? - -- Record requests -- Create evidence of your testing -- Replay requests outside of client tool -- Fuzzing - -For every HTTP REST Client I try, I make sure that I know how to send all HTTP requests through an HTTP Proxy. - -This allows me to view every Request I send to the server and response I receive. - -Why? Would you want to do this when most REST Clients have a debug view or history view to see the requests? - -Well, these views often don't show you the exact request sent. - -For example, many REST API tools add additional headers into the request, which they filter out from the debug or history view. - -It is very important to know exactly what you have sent to the API Server, and exactly what has been received. - -The HTTP Proxies will capture the requests and you can see exactly what the HTTP message is, including all the additional headers the REST Client has added. - -Additionally, you can see the full and uninterpreted request from the API. - -## Review all the HTTP Traffic - -HTTP Proxies are designed to make it easy to see all the requests and responses sent in a session. - -Most Proxies will allow you to: - -- search the requests and responses -- replay the requests with minor edits -- review historic requests easily to check if you have covered a condition you want to test for -- show a hierarchy of urls to check which end points you have tested - -## Save the Evidence - -You can also use them as an accurate historical record of the testing you conduct by saving the session as a HAR file. - -A HAR file is an HTTP Archive File. - -- [wikipedia description of HAR file format](https://en.wikipedia.org/wiki/HAR_(file_format)) - -This can be easily stored because it is a text file, and later read back into a proxy tool or HAR Viewer to check your testing coverage or review evidence when investigating defects. - - - ---- - -## Recommended Trust Exercise - -Don't trust your REST Tool History, review what the REST Tool sends and receives against the Proxy - -Try this with 3 or 4 different REST API Tools through a Proxy - -- GET a request on an endpoint -- where the request has multiple accept headers -- e.g. - -``` -accept: application/xml -accept: text/html -accept: application/json -``` - -Why? - -HTTP standard says you can't send multiple accept headers and they should be combined like: - -``` -accept: application/xml, text/html, application/json -``` - -Check: - -- What request does the REST Client send? - - Did it combine headers? Did it send one? Did it choose the one you expect? Does it complain? -- What response did you receive? -- What did the REST Client say it sent and received? - - Are the proxy and client in agreement? - -Some tools don't let you create extreme circumstances and send invalid requests (which you might want to do when testing). - -Some tools choose on your behalf without telling you. diff --git a/challenger/src/main/resources/content/tutorials/http-basics.md b/challenger/src/main/resources/content/tutorials/http-basics.md deleted file mode 100644 index 8099b88b..00000000 --- a/challenger/src/main/resources/content/tutorials/http-basics.md +++ /dev/null @@ -1,343 +0,0 @@ ---- -title: HTTP Basics - Tutorial -seo_title: Tutorial: HTTP Basics Tutorial | API Challenges -description: Basic HTTP tutorial and overview of key HTTP terminology and status codes. -lastmod: 2026-02-18 -seo_description: Learn HTTP Basics with practical examples and clear guidance you can apply immediately when creating requests, analyzing responses, and testing APIs. -showads: true ---- - -# Introduction to HTTP Requests AND Responses - ---- - -## Overview of Section - HTTP Requests and Responses - -- HTTP Verbs - GET, POST, DELETE -- Headers -- Responses - - Status Codes - e.g. 200, 404, 500 -- This is the foundation for most web, HTTP, REST testing and automating. - ---- - -## HTTP GET Request sent from cURL - -Command: - -~~~~~~~~ -curl {{}}/heartbeat ^ --H "accept: application/xml" ^ ---proxy 127.0.0.1:8888 -~~~~~~~~ - -Sends a GET Request: - -~~~~~~~~ -GET {{}}/heartbeat HTTP/1.1 -User-Agent: curl/7.39.0 -Host: {{}} -Connection: Keep-Alive -accept: application/xml -~~~~~~~~ - -- GET requests retrieve information from the Web Application -- important stuff: - - Verb (GET), - - Http version (1.1), - - User-Agent, - - Accept, - - Host, - - endpoint - ---- - -## HTTP Response to GET /heartbeat request - -~~~~~~~~ -HTTP/1.1 204 OK -Date: Thu, 17 Aug 2017 10:34:32 GMT -Content-Type: application/json -Transfer-Encoding: chunked -Server: Jetty(9.4.4.v20170414) -~~~~~~~~ - -- cURL response was same but content-type was `application/xml` -- important stuff: Status Code (204 OK), Http version (1.1), Date, Content-Type - ---- - -## Raw HTTP Requests and Responses - -- we need to be able to read them -- we will rarely have to create them by hand -- lookup headers you don't know - - https://en.wikipedia.org/wiki/List_of_HTTP_header_fields -- some fields are for the server some are for the application some are documentation - ---- - -## Basic HTTP Verbs - -- [GET](https://tools.ietf.org/html/rfc7231#section-4.3.1) - retrieve data -- [POST](https://tools.ietf.org/html/rfc7231#section-4.3.3) amend/create from partial information -- [PUT](https://tools.ietf.org/html/rfc7231#section-4.3.4) - create or replace from full information -- [DELETE](https://tools.ietf.org/html/rfc7231#section-4.3.5) - delete items -- [OPTIONS](https://tools.ietf.org/html/rfc7231#section-4.3.7) - verbs available on this url - ---- - -## References - - -- [W3c Standard](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) -- [IETF standard](https://tools.ietf.org/html/rfc7231) -- [httpstatuses.com](https://httpstatuses.com) -- http://www.restapitutorial.com/lessons/httpmethods.html - ---- - -## HTTP Status Codes - -- 1xx Informational - - 100 Continue -- 2xx Success - - e.g. 200 OK -- 3xx Redirection - - e.g. 301 Moved Permanently -- 4xx Client Error - - e.g. 404 Not Found -- 5xx Server Error - - e.g. 500 Internal Server Error - - ---- - -## Common HTTP Status Codes - -| **Status Code** | **Status Code** | -|----------------------------|-----------------| -| 200 OK | 405 Method Not Allowed | -| 201 Created | 409 Conflict | -| 301 Moved Permanently | 500 Internal Server Error | -| 307 Temporary Redirect | 501 Not Implemented | -| 400 Bad Request | 502 Bad Gateway | -| 401 Unauthorized| 503 Service Unavailable | -| 403 Forbidden | 504 Gateway Timeout | -| 404 Not Found | | - ---- - -## HTTP Status code references - -- https://httpstatuses.com/ -- https://moz.com/blog/response-codes-explained-with-pictures -- https://http.cat/ -- https://httpstatusdogs.com/ - ---- - -## HTTP Message Body Format - JSON - -- JSON - JavaScript Object Notation -- an actual Object in JavaScript -- common data transfer and marshalling format for other languages -- https://en.wikipedia.org/wiki/JSON -- http://json.org -- http://countwordsfree.com/jsonviewer -- schema exists for JSON http://json-schema.org/ - ---- - -### JSON Example Explained - -~~~~~~~~ -{ - "lists": - [ - { - "guid":"f8134dd6-a573-4cf5-a6c6-9d556118ed0b", - "title":"a list title", - "description":"", - "createdDate":"2017-08-17-13-11-12", - "amendedDate":"2017-08-17-13-11-12" - } - ] -} -~~~~~~~~ - -- An object, which has an array called "lists". -- the lists array contains an object with fields: `guid`, `title`, `description`, `createdDate`, `amendedDate` - all String fields. - ---- - -## HTTP Message Body Format - XML - -- XML - eXtended Markup Language -- HTML is often XML -- another common marshalling format -- can be validated against XML schema -- http://countwordsfree.com/xmlviewer - ---- - -## XML Example Explained - -~~~~~~~~ - - - - f8134dd6-a573-4cf5-a6c6-9d556118ed0b - a list title - - 2017-08-17-13-11-12 - 2017-08-17-13-11-12 - - -~~~~~~~~ - -- elements, nested elements -- tags, values - ---- - -## URI - Universal Resource Identifier - -`scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]` - -- `http://compendiumdev.co.uk/apps/api/mock/reflect` - - scheme = `http` - - host = `compendiumdev.co.uk` - - path = `apps/api/mock/reflect` - -[wikipedia.org/wiki/Uniform_Resource_Identifier](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) - -A URL is a URI - ---- - -### URI vs URL vs URN - -- URI - Universal Resource Identifier - - 'generic' representation - might not include the 'scheme' - - `http://compendiumdev.co.uk/apps/api/mock/reflect` - - `compendiumdev.co.uk/apps/api/mock/reflect` - - `/apps/api/mock/reflect` -- URL - Universal Resource Locator - - `http://compendiumdev.co.uk/apps/api/mock/reflect` - - defines how to locate the identified resource -- URN - [Universal Resource Name](https://en.wikipedia.org/wiki/Uniform_Resource_Name) - - not often used - uses scheme `urn` - ---- - -### Scheme(s) - -- http -- https -- ftp -- mailto -- file - ---- - -### Query Strings - -~~~~~~~~ -GET /lists/{guid}?without=title,description -GET {{}}/lists/f13?without=title,description -~~~~~~~~ - -Query String: - -~~~~~~~~ -?without=title,description -~~~~~~~~ - -- starts with `?` -- params separated with `&` - ---- - -### More About Query Strings - -~~~~~~~~ -GET /lists/{guid}?without=title,description -~~~~~~~~ - -- usually `name=value` pairs separate by '&' - - convention since anything after the `?` is the Query string - - app then parses as required -- can be used with any verb -- `GET` request - all params are send as query strings - -https://en.wikipedia.org/wiki/Query_string - ---- - -## HTTP Standards? - -- rfc7231 [(HTTP/1.1): Semantics and Content](https://tools.ietf.org/html/rfc7231) -- rfc7230 [(HTTP/1.1): Message Syntax and Routing](https://tools.ietf.org/html/rfc7230) - ---- - -## How to test with this information - -- Read the standards for the verbs and the status codes. -- Projects often argue about interpretations. -- Some of the standards are exact enough that it is possible to say "I observed X" it does not match the standard - include links and quotes to the standards. - - -## HTTP Headers - -- Headers are `Key: value` pair attributes in the request -- Headers are not counted as the payload -- Headers help configure the response from the server and tell the server how to process this request -- e.g. a `GET` request with an `accept: application/xml` header is asking the server to respond with the information in XML format - - -### User-Agent Header - -- Often not sent when accessing an API -- Marks request as coming from a browser - -~~~~~~~~ -User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) -AppleWebKit/537.36 (KHTML, like Gecko) -Chrome/60.0.3112.90 Safari/537.36 -~~~~~~~~ - ---- - -### Accept Header - -- Defines the payload types that the receiver will accept -- If this was an API call it would likely return XML - -~~~~~~~~ -Accept: text/html,application/xhtml+xml,application/xml; -q=0.9,image/webp,image/apng,*/*;q=0.8 -~~~~~~~~ - -Common values: - -- `text/html` -- `application/json` -- `application/xml` - ---- - -### Basic Auth Header - -- An application might use Basic Auth Authentication to control access to the API -- `Authorization` Header - -e.g. `Authorization: Basic dXNlcjpwYXNzd29yZA==` - -`dXNlcjpwYXNzd29yZA==` is base64 encoded "user:password" - -see [base64decode.org](https://www.base64decode.org) - -- cURL you need to add the header -- Postman & Insomnia use the Authorization and Auth tabs diff --git a/challenger/src/main/resources/content/tutorials/http-verbs.md b/challenger/src/main/resources/content/tutorials/http-verbs.md deleted file mode 100644 index 8b83a688..00000000 --- a/challenger/src/main/resources/content/tutorials/http-verbs.md +++ /dev/null @@ -1,304 +0,0 @@ ---- -title: HTTP Verbs - Tutorial -seo_title: Tutorial: HTTP Verbs Tutorial | API Challenges -description: Basic HTTP Verbs and Methods tutorial what they do and how to use them. -lastmod: 2026-02-18 -seo_description: Learn HTTP Verbs with practical examples and clear guidance you can apply immediately when creating requests, analyzing responses, and testing APIs. -showads: true ---- - -# HTTP Verbs Overview - ---- - -## HTTP GET Verb - -- [GET](https://tools.ietf.org/html/rfc7231#section-4.3.1) - retrieve data -- GET verbs can be issued by a browser - - click on link - - visit a site -- GET `http://compendiumdev.co.uk/apps/api/mock/reflect` -- Important Headers - - User-Agent - tells server app type - - Accept - what format response you prefer - - ---- - -### HTTP GET Verb Example - -~~~~~~~~ -curl {{}}/heartbeat ^ --H "accept: application/xml" ^ ---proxy 127.0.0.1:8888 -~~~~~~~~ - -~~~~~~~~ -GET {{}}/heartbeat HTTP/1.1 -User-Agent: curl/7.39.0 -Host: localhost:4567 -Connection: Keep-Alive -accept: application/xml -~~~~~~~~ - ---- - -### Common HTTP Status codes in response to a GET - -- **200** - OK, found the url, returned contents -- **301, 307, 308** - content has moved, new url in `location` header -- **404** - url not found -- **401** - you need to give me authorisation details see `WWW-Authenticate` header -- **403** - url probably exists but you are not allowed to access it - - ---- - -## HTTP POST Verb - -- [POST](https://tools.ietf.org/html/rfc7231#section-4.3.3) amend/create from partial information - -- send a 'body' format of content in the 'content-type' header -- usually used to create or amend data -- browser will usually send a POST request when submitting a form - ---- - -### HTTP POST Verb Send Example - -~~~~~~~~ -curl -X POST {{}}/lists ^ --H "accept: application/xml" ^ --H content-type:application/json ^ --H "Authorization: Basic dXNlcjpwYXNzd29yZA==" ^ --d "{title:'a list title'}" ^ ---proxy 127.0.0.1:8888 -~~~~~~~~ - ---- - -### HTTP POST Verb Request Example - -~~~~~~~~ -POST {{}}/lists HTTP/1.1 -User-Agent: curl/7.39.0 -Host: localhost:4567 -Connection: Keep-Alive -accept: application/json -content-type: application/json -Authorization: Basic dXNlcjpwYXNzd29yZA== -Content-Length: 22 - -{title:'a list title'} - -~~~~~~~~ - ---- - -### HTTP POST Verb Response Example - -~~~~~~~~ -HTTP/1.1 201 Created -Date: Thu, 17 Aug 2017 12:11:12 GMT -Content-Type: application/json -Location: /lists/f8134dd6-a573-4cf5-a6c6-9d556118ed0b -Server: Jetty(9.4.4.v20170414) -Content-Length: 171 - -{"lists":[{ -"guid":"f8134dd6-a573-4cf5-a6c6-9d556118ed0b", -"title":"a list title", -"description":"", -"createdDate":"2017-08-17-13-11-12", -"amendedDate":"2017-08-17-13-11-12"}]} -~~~~~~~~ - ---- - -### Common HTTP Status codes in response to a POST - -- **200** - OK, did whatever I was supposed to -- **201** - OK created new items -- **202** - OK, I'll do that later -- **204** - OK, I have no more information to give you -- **400** - what? that request made no sense -- **404** - I can't post to that url it is not found -- **401** - need authorisation see `WWW-Authenticate` header -- **403** - url probably exists but you are not allowed to access it -- **409** - can't do that, already exists -- **500** - your request made me crash - - ---- - -## HTTP PUT Verb - -- [PUT](https://tools.ietf.org/html/rfc7231#section-4.3.4) - create or replace from full information - -Full information means it should be idempotent - send it again and get exactly the same request - -Demo - ---- - -### HTTP PUT Send Example - -~~~~~~~~ -curl -X PUT {{}}/lists ^ --H "Authorization: Basic dXNlcjpwYXNzd29yZA==" ^ ---proxy 127.0.0.1:8888 ^ --d @createlistwithput.txt -~~~~~~~~ - -where `createlistwithput.txt` file contains - -~~~~~~~~ -{"title":"title added with put", -"description":"list description", -"guid": "guidcreatedwithput201708171440", -"createdDate": "2017-08-17-14-40-34", -"amendedDate": "2017-08-17-14-40-34"} -~~~~~~~~ - ---- - -### HTTP PUT Request Example - -~~~~~~~~ -PUT {{}}/lists HTTP/1.1 -User-Agent: curl/7.39.0 -Host: localhost:4567 -Accept: */* -Connection: Keep-Alive -Authorization: Basic dXNlcjpwYXNzd29yZA== -Content-Length: 180 -Content-Type: application/json - -{"title":"title added with put", -"description":"list description", -"guid": "guidcreatedwithput201708171440", -"createdDate": "2017-08-17-14-40-34", -"amendedDate": "2017-08-17-14-40-34"} -~~~~~~~~ - ---- - -### HTTP PUT Response Example - -~~~~~~~~ -HTTP/1.1 201 Created -Date: Thu, 17 Aug 2017 13:41:46 GMT -Content-Type: application/json -Server: Jetty(9.4.4.v20170414) -Content-Length: 0 -~~~~~~~~ - ---- - -## HTTP DELETE Verb - -- [DELETE](https://tools.ietf.org/html/rfc7231#section-4.3.5) - delete items - -Demo - ---- - -### HTTP DELETE Send Example - -~~~~~~~~ -curl -X DELETE {{}}/lists/{guid} ^ --H "Authorization: Basic YWRtaW46cGFzc3dvcmQ=" ^ ---proxy 127.0.0.1:8888 -~~~~~~~~ - ---- - -### HTTP DELETE Request Example - -~~~~~~~~ -DELETE {{}}/lists/{guid} HTTP/1.1 -User-Agent: curl/7.39.0 -Host: localhost:4567 -Accept: */* -Connection: Keep-Alive -Authorization: Basic YWRtaW46cGFzc3dvcmQ= -~~~~~~~~ - ---- - -### HTTP DELETE Response Example - -~~~~~~~~ -HTTP/1.1 204 No Content -Date: Thu, 17 Aug 2017 12:20:35 GMT -Content-Type: application/json -Server: Jetty(9.4.4.v20170414) -~~~~~~~~ - - ---- - -### Common HTTP Status codes in response to a DELETE - -- **200** - OK, did whatever I was supposed to -- **202** - OK, I'll do that later -- **204** - OK, I have no more information to give you -- **404** - I can't post to that url it is not found -- **401** - you need to give me authorisation details see `WWW-Authenticate` header -- **403** - url probably exists but you are not allowed to access it -- **500** - your request made me crash - - ---- - -## HTTP OPTIONS Verb - -- [OPTIONS](https://tools.ietf.org/html/rfc7231#section-4.3.7) - shows the verbs available on this url -- returns an `Allow` header describing the allowed HTTP Verbs - ---- - -### HTTP OPTIONS Send Example - -~~~~~~~~ -curl -X OPTIONS {{}}/lists ^ ---proxy 127.0.0.1:8888 -~~~~~~~~ - -Demo - ---- - -### HTTP OPTIONS Request Example - -~~~~~~~~ -OPTIONS {{}}/lists HTTP/1.1 -User-Agent: curl/7.39.0 -Host: localhost:4567 -Accept: */* -Connection: Keep-Alive -~~~~~~~~ - ---- - -### HTTP OPTIONS Response Example - -~~~~~~~~ -HTTP/1.1 200 OK -Date: Thu, 17 Aug 2017 12:24:39 GMT -Allow: GET, POST, PUT -Content-Type: text/html;charset=utf-8 -Server: Jetty(9.4.4.v20170414) -Content-Length: 0 -~~~~~~~~ - ---- - -### Common HTTP Status codes in response to a OPTIONS - -- **200** - OK, did whatever I was supposed to -- **404** - I can't post to that url it is not found - - - - diff --git a/challenger/src/main/resources/content/tutorials/openapi-swagger.md b/challenger/src/main/resources/content/tutorials/openapi-swagger.md deleted file mode 100644 index a379b2f3..00000000 --- a/challenger/src/main/resources/content/tutorials/openapi-swagger.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -title: OpenAPI / Swagger- Tutorial -seo_title: Tutorial: OpenAPI / Swagger- Tutorial | API Challenges -description: An introduction to the OpenAPI (Swagger) API Specification Format. -lastmod: 2026-02-18 -seo_description: Learn OpenAPI / Swagger- Tutorial with practical examples and clear guidance you can apply immediately when creating requests, and analyzing responses. -showads: true ---- - -# Introduction to OpenAPI - -## What is the OpenAPI Specification Format? - -[OpenAPI](https://www.openapis.org/) is a specification format for documenting APIs. It primarily concentrates on REST and HTTP APIs. Because it was created as part of the Swagger project it is sometimes referred to as the "Swagger API Format". - -- [openapis.org](https://www.openapis.org/) -- [swagger.io/specification](https://swagger.io/specification/) - -The specification can be written using YAML or JSON, and can be read by tools to: - -- automatically create API call templates in REST API Clients -- automatically create HTTP Requests which compare the API response with the specification -- automatically create an interface to allow humans to interact with the API -- automatically generate code to create an SDK to access the API - -## How to View an OpenAPI Specification - -It is possible to view an OpenAPI specification in any text editor. - -But if you are not familiar with the OpenAPI specification then it can be hard to read. - -The easiest way to view an OpenAPI specification is to load it into the Swagger Open API Editor. - -- [swagger.io/tools/swagger-editor](https://swagger.io/tools/swagger-editor/) - -The editor can be used online at [editor.swagger.io](https://editor.swagger.io/). - -You can import the OpenAPI file either from a URL or from a file that you have downloaded. - -> Exercise: try the following URLs in the swagger editor -> -> - https://apichallenges.eviltester.com/simpleapi/docs/swagger -> - https://apichallenges.eviltester.com/docs/swagger -> - https://api.practicesoftwaretesting.com/docs/api-docs.json - -The Editor has the benefit that you can see the YAML version of the specification on the left of the screen, and a rendered UI on the right of the screen that allows you to issue requests to the API. - -The more detailed the OpenAPI specification file, the more features the UI will have and the more benefit you will gain from the tools that consume the API. - -## Running the Swagger Editor locally - -I prefer to run the Swagger Editor locally and do so using Docker. - -Initially: - -``` -docker run -p 1235:8080 --rm swaggerapi/swagger-editor -``` - -The above command will run the Swagger Editor UI, via Docker, on port 1235, and delete the container when I exit the command line. - -I can then access the editor by visiting `http://localhost:1235`. - -> Exercise: Run the editor locally using Docker -> -> - Load the URL into the editor https://apichallenges.eviltester.com/simpleapi/docs/swagger -> - Try some requests -> - Edit some of the YAML and see what difference it makes to the Swagger UI - -## Disadvantages of Using the Swagger UI and Editor - -The Swagger UI and Editor are very convenient tools, but they are designed for using an API and have disadvantages if you want to rely on them for Testing. - -### CORS Security - -The Swagger UI and Editor make requests to the API using JavaScript calls. - -Browsers have security measures to prevent JavaScript making `POST`, `PATCH`, `PUT`, `DELETE` and other requests to a server. - -This security is known as [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing - -- [mdn CORS documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - -Most Web Applications are strict about CORS and do not send the Response headers that ease up on the Security, so you may find that you cannot issue all the requests you want, to any API, using the Swagger Tools. - -The API Challenges APIs have relaxed CORS headers to allow most tools to run against them. - -> Exercise: Load the SimpleAPI OpenAPI Spec into Swagger and view CORS Headers -> -> The SimpleAPI has very permissive CORS headers to allow most API tooling to access it. -> -> - Load the OpenAPI Spec into a Swagger UI -> - Access UI [petstore.swagger.io](https://petstore.swagger.io/) -> - Amend the URL input at the top to be "https://apichallenges.eviltester.com/simpleapi/docs/swagger" -> - Click [Explore] -> - Use the API requests -> - View the Response Headers to see all the CORS headers - -APIs that use Swagger to allow people to use the API tend to have the Swagger UI served from the same domain as the API. These allow most requests without having to relax security and set all the CORS response headers. - -> Exercise: Try Swagger UIs designed to be used to support all requests -> -> Try and use the ToolShop Swagger API and Petstore API -> -> - [api.practicesoftwaretesting.com/api/documentation](https://api.practicesoftwaretesting.com/api/documentation) -> - [petstore.swagger.io/](https://petstore.swagger.io/) - -### Designed for Usage Not Testing - -The Swagger tools are useful for testing out an API. They tend to have validation on the requests so that fields are enforced as required, or the types are enforced like Integer or Enum etc. - -Additionally only valid HTTP verbs will be shown, if a URL does not support a `DELETE` verb then there won't be a `DELETE` option for that URL in the Swagger UI. - -When we are testing an API we want to go beyond the normal usage scenarios and want to test all the extreme scenarios, like `DELETE /todos` to delete all the todos, and make sure that the API does not perform the action. - -In order to use Swagger UI to do this we would have to amend the OpenAPI specification file. This is why the Swagger Editor can be useful, but only if the API supports CORS requests to allow these actions to occur. - -> Exercise: SimpleAPI has a usage OpenAPI spec and a Testing API Spec -> -> Compare the two OpenAPI specifications provided by the SimpleAPI and see how the restrictions are enforced in the normal OpenAPI spec and how they are relaxed in the permissive OpenAPI spec. -> -> - [normal SimpleAPI spec for usage](http://localhost:4567/simpleapi/docs/swagger) -> - [Permissive SimpleAPI Spec for Testing](http://localhost:4567/simpleapi/docs/swagger?permissive) - - -## Using OpenAPI in a REST Client - -Most Rest Clients support loading in an OpenAPI file and generating a skeleton collection of requests to allow you to then test the API by editing and issuing the skeleton requests. - -- Using Bruno - Import a Collection, from a downloaded OpenAPI file - -This is typically the approach that most people testing APIs will use because the REST API client will not be subject to the same CORS restrictions that the Swagger browser based tools encounter. - - -## Example Open Source Tools for Automated Execution of API from OpenAPI - -- [Dredd — HTTP API Testing Framework](https://dredd.org/en/latest/) -- [Postman Contract Test Generator](https://github.com/allenheltondev/postman-contract-test-generator) -- [K6 load generation from OpenAPI](https://k6.io/blog/load-testing-your-api-with-swagger-openapi-and-k6/) -- [Tcases](https://github.com/Cornutum/tcases/blob/master/tcases-openapi/README.md#tcases-for-openapi-from-rest-ful-to-test-ful) -- [Humlix](https://www.humlix.com/) -- [OpenAPI Testing Tools List](https://openapi.tools/#testing) - - -## Tools Using Open API - -Rather than try and build a large list of Open API Tooling, I will defer to [OpenAPI.Tools](https://openapi.tools/) a list of tools curated by [APIsYouWontHate.com](https://apisyouwonthate.com/). - -The list is split into sections e.g. - -- Code Generators -- Convertors -- Documentation -- Mocking -- Validators - -The list of tools includes Open Source and commercial tools. - -- [OpenAPI.Tools](https://openapi.tools/) diff --git a/challenger/src/main/resources/content/tutorials/rest-api-basics.md b/challenger/src/main/resources/content/tutorials/rest-api-basics.md deleted file mode 100644 index b0b98c45..00000000 --- a/challenger/src/main/resources/content/tutorials/rest-api-basics.md +++ /dev/null @@ -1,203 +0,0 @@ ---- -title: REST API - Tutorial -seo_title: Tutorial: REST API Tutorial for API Testing | API Challenges -description: Basic REST API tutorial to learn what is a REST API and how they work. -lastmod: 2026-02-18 -seo_description: Learn REST API with practical examples and clear guidance you can apply immediately when creating requests, analyzing responses, and testing APIs. -showads: true ---- - -# REST API BASICS - -- What is a REST API? -- CRUD and REST -- HTTP Verbs - HEAD, PATCH -- Authentication and Authorisation - ---- - -## What is a REST API? - -- HTTP API - generic, anything goes -- REST API - - the HTTP Verbs mean something specific e.g. should not Delete with a POST request - - URI are 'nouns' and describe entities - -A REST API is an API that conforms to the REST standards. - -Many HTTP API's describe themselves as REST APIs but are inconsistent in how they interpret the standard. - -When working with APIs, having an understanding of the REST API standards can provide you with a base level of expectation of how the API might work. - -Often, REST API is simply used to mean HTTP API. - - ---- - -## REST Standards? - -[Representational State Transfer](https://en.wikipedia.org/wiki/Representational_state_transfer) - -- Loose standards -- Lots of disagreement on teams and online -- DISSERTATION: "Architectural Styles and the Design of Network-based Software Architectures" by [Roy Fielding](http://www.ics.uci.edu/~fielding/) - - [ics.uci.edu/~fielding/pubs/dissertation/top.htm](https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm) - ---- - -## Guidance - -- Idempotent - same request, same result (on server, not necessarily in response) -- Stateless - server does not need to maintain state of client requests between requests e.g. - - request 1: select these files, - - request 2: delete files selected in previous request -- Cacheable - on the server side e.g. GET can be cacheable until entities in GET are updated -- Does it comply with HTTP Standard Guidance? - ---- - -## CRUD - -- Verbs are not as simple as Create, Read, Update, Delete - -| CRUD Action | Verb | -|------|--------| -| Create | POST, PUT | -| Read | GET | -| Update | POST, PUT, PATCH | -| Delete | DELETE | - ---- - -## Endpoints vs URL - -Very often when discussing REST APIs we talk about 'endpoints'. - -Basically the 'path' part of the URL. - -The following are the same Endpoint - -- `/lists` -- `/lists?title="title"` - ---- - -## Payloads vs Body - -A Payload is the content of the body of the HTTP request. - -- XML and JSON -- Tends not to be Form encoded -- Request defined by `content-type` header -- Response requested in `accept` header -- usually unmarshalled into an object in the application - ---- - -## Requesting Formats - -| Header | Means | -|---------------|-------| -|Accept: application/json | Please return JSON | -|Accept: application/xml | Please return XML | -|Content-Type: application/json | This payload is JSON | -|Content-Type: application/xml | This payload is XML | - -- XML might also be : `text/xml` -- The server might not support a particular format it might default to JSON or XML and ignore the header - ---- - -## Authentication - -If you make a request to a server and receive a 401 then you are not authenticated. - -`WWW-Authenticate` header should challenge you with the authentication required. - -- Generally avoid header sending by known authenticating information in request. -- Common bug is `WWW-Authenticate` not sent back in response. - ---- - -## Common Authentication Approaches - -- [Basic Auth Header](http://tools.ietf.org/html/7617) - - `Authorization: Basic Ym9iOmRvYmJz` - - base 64 encoded `username:password` -- Cookies - - when 'login' server sends back a 'session cookie' - - send 'session cookie' in future requests -- Custom Headers - - API `secret codes` - - e.g. `X-API-AUTH: thisismysecretapicode` - ---- - -## Common Authentication Approaches - -- URL authentication - - `https://username:password@www.example.com/` - - deprecated - - used to be very common when automating web GUIs - -Recommended reading [developer.mozilla.org/en-US/docs/Web/HTTP/Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) - ---- - -## Authentication vs Authorization - -Authentication - -- Are you authenticated? -- Does the system know who you are? -- Are your auth details correct? - -Authorization - -- you are authenticated -- do you have permission to access this endpoint? - ---- - -## Real World vs Standards - -Teams debate this all the time. - -- Login? [stackoverflow.com/questions/13916620](https://stackoverflow.com/questions/13916620/rest-api-login-pattern) -- Put vs Post [stackoverflow.com/questions/630453](https://stackoverflow.com/questions/630453/put-vs-post-in-rest) -- see discussions on [restcookbook.com](http://restcookbook.com) - -As a Tester: - -- Refer to HTTP standards - - headers, idempotency, response recommendations - -Expect 'discussions' and 'debates' on a team. - ---- - -### Verb - Head - -- [HEAD](https://tools.ietf.org/html/rfc7231#section-4.3.2) -- same as GET but does not return a 'body' -- can be useful for checking 'existence' of an endpoint or entity - ---- - -### Verb - Patch - -- [PATCH](https://tools.ietf.org/html/rfc5789) - An 'Update' method which provides a set of changes -- Contentious [see](http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/) -- Proposed standard for [JSON Merge Patch format](https://tools.ietf.org/html/rfc7396) -- Promosed standard for [XML Patch Using XPath](https://tools.ietf.org/html/rfc5261) - -Most web services just use `POST` or `PUT` - - -## Recommended Reading - -Reading: - -- read the REST Dissertation [ics.uci.edu/~fielding/pubs/dissertation/top.htm](https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm) -- Read the docs on authentication [developer.mozilla.org/en-US/docs/Web/HTTP/Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) -- for real world 'discussions' see [restcookbook.com](http://restcookbook.com) diff --git a/challenger/src/main/resources/content/tutorials/summary.md b/challenger/src/main/resources/content/tutorials/summary.md deleted file mode 100644 index 6871ff33..00000000 --- a/challenger/src/main/resources/content/tutorials/summary.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: Summary - Tutorial -seo_title: Tutorial: Summary Tutorial for API Testing | API Challenges -description: Basic Summary of REST API tutorial content. -lastmod: 2026-02-18 -seo_description: Learn Summary with practical examples and clear guidance you can apply immediately when creating requests, analyzing responses, and testing APIs. -showads: true ---- - -# Summary Overview - -This API testing tutorial summary gives you a fast, structured review of the core topics behind reliable REST API testing. It connects HTTP basics, request/response behavior, tooling, and test strategy so you can design stronger checks, investigate failures faster, and automate with confidence. Instead of treating API work as isolated commands, this page shows how standards, payload design, headers, and workflow decisions combine in real testing. - -Use this summary when you need to refresh fundamentals before starting a new project, challenge set, or automation effort. The sections below emphasize the highest-value concepts: HTTP message structure, verb selection, documentation analysis, and tool-assisted validation of what was actually sent over the wire. You will also see practical automation themes, including reuse, abstraction, and evidence capture for maintainable API test suites. - -For best results, pair this REST API testing tutorial summary with the detailed pages in this section, then apply each concept in a live API environment. That learn-execute-review loop is one of the quickest ways to improve practical API testing and API automation skills. - -## Technology - -- Learn HTTP Standards -- You can base your 'bugs' on Standards - - HTTP Message Syntax and Routing [RFC 7230](https://tools.ietf.org/html/rfc7230) -- Learn the common VERBS: GET, POST, DELETE, PUT -- Read the REST Dissertation - - ---- - -## Testing - -- Add as much variation as you can -- Use tooling to help you -- Go beyond the outcome -- Use headers -- Read the Docs -- Read the Swagger Open API output -- Combine everything you learned -- Use a Client, send in requests as easily as possible -- Use a Proxy, trust the proxy output rather than the tool output -- Track your testing -- Save HAR files to document your results - ---- - -## Tools - Clients - -- Different tools have different capabilites -- Experiment with multiple tools -- Postman: Collections for Data Creation, Console -- Insomnia: Import, Timeline, Proxies -- Import/Export between Tools - ---- - -## Tools - Proxies - -- Often used for Security Testing -- Fuzzers create data -- Automatically keep a record of your testing -- View actual requests and responses -- Replay requests - ---- - -## Tools - -- Clients - - Bruno - - Hoppscotch.io - - Postman - - Insomnia - - cURL -- Proxies - - System - - Fiddler - - Charles - - Other - - BurpSuite - - Owasp Zap - ---- - -## Automating - -- HTTP libraries -- REST libraries -- Domain Abstractions -- Reuse for performance testing - ---- - -## Testing Summarised - -- Requirements - domain, documentation, sdk -- Standards - HTTP, REST, Auth -- Security -- Capacity -- Interfacing Systems - - - - - - diff --git a/challenger/src/main/resources/content/tutorials/testing-apis.md b/challenger/src/main/resources/content/tutorials/testing-apis.md deleted file mode 100644 index 5b4a78c9..00000000 --- a/challenger/src/main/resources/content/tutorials/testing-apis.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Testing APIs - Tutorial -seo_title: Tutorial: Testing APIs Tutorial | API Challenges -description: Basic Testing REST API tutorial and how to think when thinking about testing an API. -lastmod: 2026-02-18 -seo_description: Learn Testing APIs with practical examples and clear guidance you can apply immediately when creating requests, analyzing responses, and testing APIs. -showads: true ---- - -# Testing a REST API - -- how to model an API -- testing ideas - ---- - -## Testing different from Technology and Tooling - -- at this point we have discussed technology and tooling -- time to discuss testing - ---- - -## What would we test? - -- Risk -- Coverage -- Functionality and Outcomes - ---- - -## Coverage Driven Testing - -REST APIs are a very 'pure' system. - -- Input -> Process -> Output -- Request -> Process -> Response - -Most of the variation comes from: - -- Input -- Current System State - -With a UI we have to worry about variation like: - -- which browser? -- exactly how I interact e.g. time between click and release of mouse, did I hold a key at the same time? etc. - ---- - -## Coverage of What? - -- Verbs - have you used every verb with every end point? -- Endpoints - have you tried them all? -- Swagger - have you used the Swagger API document? -- Documentation - have you read the docs? -- Query Params - have you tried combinations? -- formats (content and accept) - have you varied XML, JSON, Text and others? -- State - Get when missing, Create when exists? etc. - ---- - -# What are the architecture risks? - -- Client -> Web Server -> App Server -> App -- Do we understand the architecture? - ---- - -# What are the capacity risks? - -- Performance? -- Load Testing? - ---- - -# What are the security risks? - -- Authentication -- Authorisation -- Injection -- What headers are accepted? X-HTTP-Method-Override? - ---- - -# Data Risks - -- minimum data in requests - missing fields, headers -- not enough data in requests -- wrong format data: json, xml, length, null, empty -- malformed data -- consistency? query params across requests? -- are defaults correct? -- duplicate data in payloads? -- headers: missing, malformed, too many, duplicate - ---- - -# Document your testing - -- How can you document your testing? -- Mindmaps? -- Text files -- Record all requests through an HTTP Proxy and store as a HAR file - ---- - -# Other Risks or Common Issues? - ---- - -# Exercise: Think through testing - -- Read the requirements -- Create some test ideas -- Look at the existing testing conducted -- Any ideas from that? -- Test -- Document and Track your Testing in a lightweight fashion -- Try different tools -- Run all your requests and responses through a Proxy and review the results - you might be surprised to see differences that your REST Client Tool did not reveal. - diff --git a/challenger/src/main/resources/content/tutorials/web-basics.md b/challenger/src/main/resources/content/tutorials/web-basics.md deleted file mode 100644 index 511f0c71..00000000 --- a/challenger/src/main/resources/content/tutorials/web-basics.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Web Basics - Tutorial -seo_title: Tutorial: Web Basics Tutorial | API Challenges -description: Overview of how Web applications work and an introductory HTTP tutorial. -lastmod: 2026-02-18 -seo_description: Learn Web Basics with practical examples and clear guidance you can apply immediately when creating requests, analyzing responses, and testing APIs. -showads: true ---- - -# Web Basics and HTTP Basics - ---- - -## Overview of Section - HTTP Basics - -- What is a Web Application? -- What is an HTTP Request? -- Viewing/Making HTTP Requests in Browser -- Using Browser Dev Tools - - ---- - -## What is a Web Application? - -- A web hosted HTTP accessed application with a GUI and possibly an API - ---- - -## Examples of Web Applications - -- Google -- Twitter -- etc. - -This API Challenges application is a Web Application. - -All the pages are generated by a server side application and shown in the Web Browser. - ---- - -## Overview - Browser - Web Application - -![Browser to Web Application](/images/tutorials/basicwebapp_orig.png) - ---- - -## Example - A Web Application - -- Diagram showing browser making GET, POST requests to Server - -![A Web Application](/images/tutorials/googlesearch_orig.png) - ---- - -## What is HTTP? - -- Verbs (or Methods) - GET, POST, DELETE, PUT, HEAD, OPTIONS, PATCH -- URL (URI) -- Headers - cookies, accept formats, user agent, content of message, authentication, etc. -- Data contained in message body - Form, JSON, XML - ---- - -## Example HTTP Request - -use browser to GET {{}}/mirror/raw - -Formatted for readability - headers are normally on one line. - -~~~~~~~~ -GET {{}}/mirror/raw HTTP/1.1 - -Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122" -Connect-Time: 0 -Accept: text/html, - application/xhtml+xml, - application/xml; - q=0.9,image/avif, - image/webp,image/apng, - */*;q=0.8,application/signed-exchange; - v=b3;q=0.7 -X-Request-Id: 018de744-6d5a-4c80-a696-dfb6e2ab73dd -Connection: close -User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; - x64) AppleWebKit/537.36 (KHTML, like Gecko) - Chrome/122.0.0.0 Safari/537.36 -X-Forwarded-Proto: https -X-Request-Start: 1710798423231 -Host: {{}} -Accept-Encoding: gzip, deflate, br, zstd -X-Forwarded-Port: 443 -~~~~~~~~ - -It is worth looking up the headers to understand them e.g. - -- [Accept:](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) -- [Connection:](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection) -- [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) -- [Accept-Encoding:](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) - -Headers with an "X-" are often custom headers added by the REST Client or Browser or Web Server. - -Learn more about: - -- [Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) - ---- - -## HTTP Requests - Human or System - -- Human Initiated - - user types URL into browser search bar (GET) - - user submits form (POST) - -- System Initiated - -System automatically polls server for new content via JavaScript - - [AJAX](https://en.wikipedia.org/wiki/Ajax_(programming)) (Asynchronous JavaScript and XML) - - [XHR](https://en.wikipedia.org/wiki/XMLHttpRequest) (XML HTTP Request) - - GET / POST - - often returns JSON - -Humans 'do things' to a web site e.g. click a button, type in information. - -Humans are often unaware that an HTTP Request has been triggered. - -Humans often can't tell if a new piece of information on the screen for a web application was retrieved from the server, or if it was generated by JavaScript on the page. - -The more we learn about web applications, the more we can understand how the systems that we use on a day to day basis function. This helps us spot risks, and helps us get out of trouble when the systems we use fail. - -e.g. - -- when you know about Cookies - - you'll often clear the cookies on a troublesome web application that isn't working - - you'll often use incognito mode to create a clean session -- when you know about HTTP headers - - you'll often change the USER-AGENT to see a different web page or interact with a web application differently - diff --git a/challenger/src/main/resources/partials/author-bio-snippet.html b/challenger/src/main/resources/partials/author-bio-snippet.html deleted file mode 100644 index 80033ab6..00000000 --- a/challenger/src/main/resources/partials/author-bio-snippet.html +++ /dev/null @@ -1,5 +0,0 @@ -
- -
diff --git a/challenger/src/main/resources/partials/content-index.md b/challenger/src/main/resources/partials/content-index.md deleted file mode 100644 index 0ce49fbc..00000000 --- a/challenger/src/main/resources/partials/content-index.md +++ /dev/null @@ -1,63 +0,0 @@ -- [Challenge Solutions](/apichallenges/solutions) -- [How to learn APIs](/learning) -- Reference: - - [Web Applications](/tutorials/web-basics) - - [HTTP Basics](/tutorials/http-basics) - - [HTTP Verbs](/tutorials/http-verbs) - - [REST API Basics](/tutorials/rest-api-basics) - - [Testing APIs](/tutorials/testing-apis) - - [OpenAPI](/tutorials/openapi-swagger) - - [Summary](/tutorials/summary) -- Practice Modes - - [Simple API](/practice-modes/simpleapi) - - [API Challenges](/apichallenges) - - [Mirror Mode](/practice-modes/mirror) - - [Simulation Mode](/practice-modes/simulation) -- Tools - - [REST/HTTP Clients](/tools/clients) - - [Summary Reviews](/tools/clients/summary-reviews) - - Detailed Reviews: - - [Bruno](/tools/clients/bruno) - - [cURL](/tools/clients/curl) - - [Httpie](/tools/clients/httpie) - - [Insomnia](/tools/clients/insomnia) - - [Katalon](/tools/clients/katalon) - - [Kreya](/tools/clients/kreya) - - [Milkman](/tools/clients/milkman) - - [Postman](/tools/clients/postman) - - [SoapUI](/tools/clients/soapui) - - [Yaak](/tools/clients/yaak) - - [Proxies](/tools/proxies) -- [Practice Sites](/practice-sites) - - Simulators - - [API Simulator](/practice-modes/simulation) - - [Fake Rest API](/practice-sites/fakerestapi) - - [JSON Placeholder](/practice-sites/jsonplaceholder) - - [HttpBin](/practice-sites/httpbin) - - Basic GET - - [Random User](/practice-sites/randomuser) - - [Swapi](/practice-sites/swapi) - - Basic - - [Dummy JSON](/practice-sites/dummyjson) - - Full - - [Simple API](/practice-sites/simpleapi) - - Apps - - [Best Buy](/practice-sites/apps/bestbuy) - - [Device Registry](/practice-sites/apps/deviceregistry) - - [Fx Trade Hub](/practice-sites/apps/fxtradehub) - - [RestListicator](/practice-sites/apps/restlisticator) - - [Toolshop](/practice-sites/apps/toolshop) - - [Tracks](/practice-sites/apps/tracks) - -- [Sponsors](/sponsors) -- [Change Log](/changes) - ---- - -You can [support this site](https://www.patreon.com/eviltester) from as little as $1 a month on Patreon. - -The support pays for site hosting and the expansion of this content. - -In addition, Patreon Supporters gain access to exclusive online video training courses and ebooks. - -[Join Now](https://www.patreon.com/eviltester) \ No newline at end of file diff --git a/challenger/src/main/resources/partials/generate-random-isbn.html b/challenger/src/main/resources/partials/generate-random-isbn.html deleted file mode 100644 index 7c368251..00000000 --- a/challenger/src/main/resources/partials/generate-random-isbn.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
- - -
-
\ No newline at end of file diff --git a/challenger/src/main/resources/partials/next-challenge-cta.html b/challenger/src/main/resources/partials/next-challenge-cta.html deleted file mode 100644 index a9ff5b4a..00000000 --- a/challenger/src/main/resources/partials/next-challenge-cta.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/challenger/src/main/resources/public/css/content.css b/challenger/src/main/resources/public/css/content.css deleted file mode 100644 index e57225b2..00000000 --- a/challenger/src/main/resources/public/css/content.css +++ /dev/null @@ -1,196 +0,0 @@ -@media (min-width: 1000px) { -.doc-columns{ - display: grid; - grid-template-columns: 20% 70%; - grid-auto-flow: column - } -.left-column{ - font-size: smaller; - } -} - -.statustrue{ - background:palegreen; -} - -.youtube-video { - aspect-ratio: 16 / 9; - width: 100%; -} - -/* for youtube player facade https://github.com/justinribeiro/lite-youtube */ -.lite-youtube-fallback { - aspect-ratio: 16 / 9; /* matches YouTube player */ - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - gap: 1em; - padding: 1em; - background-color: #000; - color: #fff; - text-decoration: none; -} - -/* right-facing triangle "Play" icon */ -.lite-youtube-fallback::before { - display: block; - content: ''; - border: solid transparent; - border-width: 2em 0 2em 3em; - border-left-color: red; -} - -.lite-youtube-fallback:hover::before { - border-left-color: #fff; -} - -.lite-youtube-fallback:focus { - outline: 2px solid red; -} - -/* - CSS Menu - */ - -.css-menu { - width:100%; - overflow:hidden; -} - -.sub-menu { - position: relative; - margin-bottom: 2em; - background:#e4e5eb; -} - -.sub-menu a { - display: block; - color: #000000; - text-decoration: none; - padding: 0.5em; -} - -.sub-menu a:hover { - background-color: #d7d6d5; -} - -/* remember to adjust both hover and dropped */ -.sub-menu li:hover a{ - color: #000000; - background: #efefef; -} -.sub-menu li.dropped a { - color: #000000; - background: #efefef; -} - -.sub-menu li { display: inline-block; } - - -.sub-menu li ul { display: none; } - -/* remember to adjust both hover and dropped */ -@media (min-width: 1000px) { - .sub-menu li:hover ul { - display: block; - position: absolute; - left: 0; - width: 100%; - background: #efefef; - } - .sub-menu li.dropped ul { - display: block; - position: absolute; - left: 0; - width: 100%; - background: #efefef; - } -} - -.sub-menu li ul li:hover a { color: #000; } - -/* minimal screensize based responsiveness */ -@media (max-width: 1000px) { - .sub-menu li { display: grid; } - .sub-menu li ul {display: grid;} - .sub-menu li:hover ul { - display: grid; - position: relative; - } -} - -/* Left Side Menu */ - -div.side-toc ul { - padding-inline-start: 1em; -} - -div.left-column { - padding-right: 2em; -} - -div.side-toc > ul > li > ul li { - padding-top: 0.2em; - padding-bottom: 0.2em; - font-weight: normal; -} - -div.side-toc > ul > li { - list-style-type: none; - font-weight: bold; -} - -/* breadcrumb */ -div.breadcrumb blockquote { -} - -/* main-text-content formatting */ -div.main-text-content ul li { margin-bottom: 0.5rem;} - -div.main-text-content blockquote { - background: #f5f5f5; - padding: 0.5em; -} - -div.sponsor-top { - background-color:#403f4c; - color: #ffffff; - text-align: center; - font-size: .875em; - padding-top: .5rem; - padding-bottom: .5rem; -} - -div.sponsor-top a { - color: #c0e0de; -} -aside.author-bio-snippet { - margin-top: 1.5rem; - padding: 1rem; - border-left: 4px solid #1f4e79; - background: #f2f6fb; -} - -aside.author-bio-snippet p { - margin: 0.25rem 0; -} - -aside.next-challenge-cta { - margin-top: 1.25rem; - margin-bottom: 1rem; -} - -a.next-challenge-cta-link { - display: inline-block; - background: #1f4e79; - color: #ffffff; - text-decoration: none; - padding: 0.6rem 0.9rem; - border-radius: 4px; - font-weight: 600; -} - -a.next-challenge-cta-link:hover { - background: #163858; -} diff --git a/challenger/src/main/resources/public/css/toc.css b/challenger/src/main/resources/public/css/toc.css deleted file mode 100644 index 04c32e25..00000000 --- a/challenger/src/main/resources/public/css/toc.css +++ /dev/null @@ -1,19 +0,0 @@ -div#toc div.h2{ - margin-left: 2em; -} -div#toc div.h3{ - margin-left: 3em; -} -div#toc div.h4{ - margin-left: 4em; -} -div#toc div.h5{ - margin-left: 5em; -} -div#toc div.h6{ - margin-left: 6em; -} - -div#toc div{ - margin-bottom: 0.3rem; -} \ No newline at end of file diff --git a/challenger/src/main/resources/public/favicon/about.txt b/challenger/src/main/resources/public/favicon/about.txt deleted file mode 100644 index 1aef7c43..00000000 --- a/challenger/src/main/resources/public/favicon/about.txt +++ /dev/null @@ -1,6 +0,0 @@ -This favicon was generated using the following font: - -- Font Title: Galindo -- Font Author: Copyright (c) 2012 by Brian J. Bonislawsky DBA Astigmatic (AOETI) (astigma@astigmatic.com), with Reserved Font Name "Galindo" -- Font Source: https://fonts.gstatic.com/s/galindo/v24/HI_KiYMeLqVKqwyuQ5HiRp-dhpQ.ttf -- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL)) diff --git a/challenger/src/main/resources/public/favicon/android-chrome-192x192.png b/challenger/src/main/resources/public/favicon/android-chrome-192x192.png deleted file mode 100644 index 0ca265f2..00000000 Binary files a/challenger/src/main/resources/public/favicon/android-chrome-192x192.png and /dev/null differ diff --git a/challenger/src/main/resources/public/favicon/android-chrome-512x512.png b/challenger/src/main/resources/public/favicon/android-chrome-512x512.png deleted file mode 100644 index 8e08b952..00000000 Binary files a/challenger/src/main/resources/public/favicon/android-chrome-512x512.png and /dev/null differ diff --git a/challenger/src/main/resources/public/favicon/apple-touch-icon.png b/challenger/src/main/resources/public/favicon/apple-touch-icon.png deleted file mode 100644 index dd00fe3c..00000000 Binary files a/challenger/src/main/resources/public/favicon/apple-touch-icon.png and /dev/null differ diff --git a/challenger/src/main/resources/public/favicon/favicon-16x16.png b/challenger/src/main/resources/public/favicon/favicon-16x16.png deleted file mode 100644 index cdbad726..00000000 Binary files a/challenger/src/main/resources/public/favicon/favicon-16x16.png and /dev/null differ diff --git a/challenger/src/main/resources/public/favicon/favicon-32x32.png b/challenger/src/main/resources/public/favicon/favicon-32x32.png deleted file mode 100644 index 477c0cfb..00000000 Binary files a/challenger/src/main/resources/public/favicon/favicon-32x32.png and /dev/null differ diff --git a/challenger/src/main/resources/public/favicon/favicon.ico b/challenger/src/main/resources/public/favicon/favicon.ico deleted file mode 100644 index 3fd9aa77..00000000 Binary files a/challenger/src/main/resources/public/favicon/favicon.ico and /dev/null differ diff --git a/challenger/src/main/resources/public/favicon/site.webmanifest b/challenger/src/main/resources/public/favicon/site.webmanifest deleted file mode 100644 index 45dc8a20..00000000 --- a/challenger/src/main/resources/public/favicon/site.webmanifest +++ /dev/null @@ -1 +0,0 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/challenger/src/main/resources/public/images/social/apichallenges-og-1200x630.png b/challenger/src/main/resources/public/images/social/apichallenges-og-1200x630.png deleted file mode 100644 index b278ab6a..00000000 Binary files a/challenger/src/main/resources/public/images/social/apichallenges-og-1200x630.png and /dev/null differ diff --git a/challenger/src/main/resources/public/images/tutorials/basicwebapp_orig.png b/challenger/src/main/resources/public/images/tutorials/basicwebapp_orig.png deleted file mode 100644 index 0f68ab25..00000000 Binary files a/challenger/src/main/resources/public/images/tutorials/basicwebapp_orig.png and /dev/null differ diff --git a/challenger/src/main/resources/public/images/tutorials/googlesearch_orig.png b/challenger/src/main/resources/public/images/tutorials/googlesearch_orig.png deleted file mode 100644 index 3ac75980..00000000 Binary files a/challenger/src/main/resources/public/images/tutorials/googlesearch_orig.png and /dev/null differ diff --git a/challenger/src/main/resources/public/js/challengerui.js b/challenger/src/main/resources/public/js/challengerui.js deleted file mode 100644 index ddd04d5c..00000000 --- a/challenger/src/main/resources/public/js/challengerui.js +++ /dev/null @@ -1,235 +0,0 @@ -function setCookie(cname,cvalue,exdays) { - let expires=""; - if(exdays!=undefined){ // use undefined to create a session cookie - const d = new Date(); - d.setTime(d.getTime() + (exdays*24*60*60*1000)); - expires = 'expires=' + d.toUTCString() + ";"; - } - document.cookie = cname + '=' + cvalue + ';' + expires + 'path=/'; -} - -function getCookie(cname) { - let name = cname + '='; - let decodedCookie = decodeURIComponent(document.cookie); - let ca = decodedCookie.split(';'); - for(let i = 0; i < ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1); - } - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); - } - } - return ''; -} - -function inputChallengeGuid(){ - let guid = prompt('Input a Challenger GUID to use'); - if(guid){location.href=`/gui/challenges/`+encodeURIComponent(guid);}; -} - -function forgetGuid(aguid){ - var guids = localStorage.getItem('challenges-guids'); - guids = guids.replace(`|${aguid}|`, ''); - localStorage.setItem('challenges-guids', guids); - localStorage.removeItem(`${aguid}.data`); - localStorage.removeItem(`${aguid}.progress`); - document.getElementById('p'+aguid).remove(); - if(getCookie('X-THINGIFIER-DATABASE-NAME')== aguid){ - setCookie('X-THINGIFIER-DATABASE-NAME','',0); - } -} - -function displayLocalGuids(){ - var guids = localStorage.getItem('challenges-guids') || ''; - var guidsArray = guids.match(/\|([^|]*)\|/g); - currGuid = getCookie('X-THINGIFIER-DATABASE-NAME'); - if(currGuid && !guidsArray){ - guidsArray=[]; - } - if(currGuid && guidsArray && currGuid!='' && !guidsArray.includes(`|${currGuid}|`)){ - guidsArray.push(`|${currGuid}|`) - } - if(guidsArray!=null && guidsArray.length>0){ - document.writeln('
Previously Used Challenger GUIDs') - - for(guidItem in guidsArray){ - var myguid = guidsArray[guidItem].replace(/\|/g,''); - document.writeln("

"); - document.writeln(""+myguid+""); - document.writeln(" "); - document.writeln("

"); - } - document.writeln('
'); - } -} - -function setCookieSaveLocally(){ - setCookie("auto-save-x-challenger-locally","true"); - const challengerData = document.challengerData; - if(challengerData && challengerData.xChallenger){ - currentChallengerGuid = challengerData.xChallenger; - setCookie("last-auto-saved-x-challenger", currentChallengerGuid); - } -} - -function deleteCookieSaveLocally(){ - setCookie("auto-save-x-challenger-locally","") -} - -function showCurrentStatus(){ - const challengerData = document.challengerData; - const databaseData = document.databaseData; - - document.writeln("
"); - - document.writeln(`

`); - - // issue if immediately autosave then it will wipe out any saved - // use a session cookie to force setting for everyone - // and have a last-auto-saved-guid and only switch on auto save if current guid matches last autosaved - var lastAutoSavedGuid = getCookie("last-auto-saved-x-challenger") - var currentChallengerGuid = ""; - var canAutoSaveChallenger = false; - if(challengerData && challengerData.xChallenger){ - currentChallengerGuid = challengerData.xChallenger; - if(currentChallengerGuid!="" && currentChallengerGuid===lastAutoSavedGuid){ - canAutoSaveChallenger=true; - } - } - - var autoSave = false; - if(getCookie("auto-save-x-challenger-locally")!='' && canAutoSaveChallenger){ - autoSave=true; - } - - var autoSaveOn = autoSave ? "checked" : "" - var autoSaveOnChange = autoSave ? "'deleteCookieSaveLocally();location.reload();'" : "'setCookieSaveLocally();location.reload()'" - var autoSaveLocallyHtml = ` ` - document.writeln(autoSaveLocallyHtml); - - document.writeln("

"); - - - if(challengerData && challengerData.xChallenger){ - var xChallengerGuid = challengerData.xChallenger; - - - if(challengerData.challengeStatus){ - var status = challengerData.challengeStatus; - var doneCount = Object.values(challengerData.challengeStatus).filter(x=>x).length; - var totalCount = Object.values(challengerData.challengeStatus).length; - var leftCount = totalCount - doneCount; - document.writeln(`

${totalCount} Challenges: ${doneCount} complete, ${leftCount} remain.
`); - - // only allow save if not in localStorage or is different from local storage - if(localStorage.getItem(`${xChallengerGuid}.progress`)!==JSON.stringify(challengerData)){ - if(!autoSaveOn){ - document.writeln(``); - }else{ - saveChallengerProgressToLocalStorage(challengerData); - } - }else{ - document.writeln(``); - } - if(localStorage.getItem(`${xChallengerGuid}.progress`)){ - document.writeln(``); - } - document.writeln(`

`); - } - - if(databaseData && databaseData.todos){ - - document.writeln(`

${databaseData.todos.length} todos in database. `); - document.writeln(`View Todos
`) - - // only allow save button if not in localStorage or is different from local storage - document.databaseData.todos.sort((a,b)=>a.id-b.id); - if(localStorage.getItem(`${xChallengerGuid}.data`)!==JSON.stringify(document.databaseData)){ - if(!autoSaveOn){ - document.writeln(``); - }else{ - saveChallengerTodosToLocalStorage(databaseData,challengerData); - } - }else{ - document.writeln(``); - } - if(localStorage.getItem(`${xChallengerGuid}.data`)){ - document.writeln(``); - } - document.writeln(`

`); - }else{ - if(localStorage.getItem(`${xChallengerGuid}.data`)){ - document.writeln(``); - } - } - }else{ - // if we have a guid in the url then allow restoring - var parts = location.pathname.split("/"); - var possibleUuid = parts[parts.length-1]; - - if(localStorage.getItem(`${possibleUuid}.progress`) || localStorage.getItem(`${possibleUuid}.data`)){ - - document.writeln(`

Restore Data for ${possibleUuid}

`); - if(localStorage.getItem(`${possibleUuid}.progress`)){ - document.writeln(``); - } - - if(localStorage.getItem(`${possibleUuid}.data`)){ - document.writeln(``); - } - } - - } - document.writeln("
"); - // if we haven't managed to create the challenger yet -} - - // get challenger progress and save to local storage - // get challenger todos and save to local storage - -function saveChallengerProgressToLocalStorage(aChallenger){ - if(aChallenger && aChallenger.xChallenger){ - localStorage.setItem(aChallenger.xChallenger + ".progress", JSON.stringify(aChallenger)); - } -} - -function saveChallengerTodosToLocalStorage(data, aChallenger){ - if(data && aChallenger && aChallenger.xChallenger){ - data.todos.sort((a,b)=>a.id-b.id); - localStorage.setItem(aChallenger.xChallenger + ".data", JSON.stringify(data)); - } -} - -function restoreChallengerProgressInSystem(xchallengeruuid){ - - data = localStorage.getItem(`${xchallengeruuid}.progress`); - if(data==null) return; - - fetch(`/challenger/${xchallengeruuid}`, { - method: "PUT", - body: data, - headers: { - "Content-type": "application/json", - }, - }) - .then((response) => console.log(response)); - -} - -function restoreTodosInSystem(xchallengeruuid){ - - data = localStorage.getItem(`${xchallengeruuid}.data`); - if(data==null) return; - - fetch(`/challenger/database/${xchallengeruuid}`, { - method: "PUT", - body: data, - headers: { - "Content-type": "application/json", - }, - }) - .then((response) => console.log(response)); - -} \ No newline at end of file diff --git a/challenger/src/main/resources/public/js/externalize-links.js b/challenger/src/main/resources/public/js/externalize-links.js deleted file mode 100644 index 505447ec..00000000 --- a/challenger/src/main/resources/public/js/externalize-links.js +++ /dev/null @@ -1,15 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - externalizeLinks(); -} ); - -function externalizeLinks(){ - // open external links in new window - var links = document.links; - - for (var i = 0, linksLength = links.length; i < linksLength; i++) { - if (links[i].hostname != window.location.hostname) { - links[i].target = '_blank'; - links[i].setAttribute("rel","noopener noreferrer"); - } - } -} \ No newline at end of file diff --git a/challenger/src/main/resources/public/js/toc.js b/challenger/src/main/resources/public/js/toc.js deleted file mode 100644 index db82f3e1..00000000 --- a/challenger/src/main/resources/public/js/toc.js +++ /dev/null @@ -1,28 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - htmlTableOfContents(); -} ); - -// based on https://stackoverflow.com/a/41085566 -function htmlTableOfContents( documentRef ) { - var documentRef = documentRef || document; - var toc = documentRef.getElementById("toc"); - if(!toc) - return; - var headings = [].slice.call(documentRef.body.querySelectorAll('h2, h3, h4, h5, h6')); - headings.forEach(function (heading, index) { - var ref = "toc" + index; - if ( heading.hasAttribute( "id" ) ) - ref = heading.getAttribute( "id" ); - else - heading.setAttribute( "id", ref ); - - var link = documentRef.createElement( "a" ); - link.setAttribute( "href", "#"+ ref ); - link.textContent = heading.textContent; - - var div = documentRef.createElement( "div" ); - div.setAttribute( "class", heading.tagName.toLowerCase() ); - div.appendChild( link ); - toc.appendChild( div ); - }); -} \ No newline at end of file diff --git a/challenger/src/main/resources/public/robots.txt b/challenger/src/main/resources/public/robots.txt deleted file mode 100644 index 65d00e94..00000000 --- a/challenger/src/main/resources/public/robots.txt +++ /dev/null @@ -1,21 +0,0 @@ -User-agent: * -# use no index meta data instead -# Disallow: /gui/entities -# Disallow: /gui/entities/* -# Disallow: /gui/instances -# Disallow: /gui/instances/* -# Disallow: /gui/instance/* -Disallow: /gui/challenges/* -Disallow: /mirror/raw -Disallow: /mirror/request -Disallow: /mirror/raw/* -Disallow: /mirror/request/* -Disallow: /sim/* -Disallow: /challenger -Disallow: /challenger/* -Disallow: /secret/* -# use noindex http header instead -# Disallow: /todos/* -# Disallow: /todos -# Disallow: /challenges -# Disallow: /heartbeat diff --git a/challenger/src/main/resources/public/sitemap.bak b/challenger/src/main/resources/public/sitemap.bak deleted file mode 100644 index c90bab5e..00000000 --- a/challenger/src/main/resources/public/sitemap.bak +++ /dev/null @@ -1,333 +0,0 @@ - - - - https://apichallenges.eviltester.com/ - 2024-03-22 - weekly - 1.0 - - - https://apichallenges.eviltester.com/gui/multiuser - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/docs - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/learning - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/practice-modes/simulation - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/gui/challenges - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/practice-modes/mirror - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/tutorials/summary - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/tutorials/testing-apis - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/authorization/post-secret-note-401-403 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/authorization/get-post-secret-note-bearer - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/practice-sites - 2024-03-24 - weekly - 0.5 - - - https://apichallenges.eviltester.com/practice-sites/swapi - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/content-type-header/post-todos-xml - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/get/get-todo-404 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/get/get-todos-id-200 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/content-type-header/post-todos-json - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/content-type-header/post-todos-415 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/mix-accept-content/post-xml-accept-json - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/tutorials/web-basics - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/get/get-todos-id-404 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/mix-accept-content/post-json-accept-xml - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/authorization/post-secret-note-200 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/authorization/get-secret-note-200 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/get/get-todos-200-filter - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/status-codes/status-codes-405-500-501-204 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/authorization/get-secret-note-401 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/authorization/get-secret-note-403 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/authentication/post-secret-401 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/sponsors - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/authentication/post-secret-201 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/tutorials/http-basics - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/head/head-todos-200 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/post-create/post-todos-201 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/tutorials/http-verbs - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/post-create/post-todos-400 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/post-update/post-todos-id-200 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/accept-header/get-todos-406 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/accept-header/get-todos-200-no-accept - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/accept-header/get-todos-200-xml-pref - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/accept-header/get-todos-200-any - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/accept-header/get-todos-200-json - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/accept-header/get-todos-200-xml - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/options/options-todos-200 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/delete/delete-todos-id-200 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/tools/proxies - 2024-03-23 - weekly - 0.5 - - - https://apichallenges.eviltester.com/tools/clients - 2024-03-23 - weekly - 0.5 - - - https://apichallenges.eviltester.com/tutorials/rest-api-basics - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/create-session/post-challenger-201 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/first-challenge/get-challenges-200 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/get/get-todos-200 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/authentication/post-secret-201 - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges - 2024-03-22 - weekly - 0.5 - - - https://apichallenges.eviltester.com/apichallenges/solutions/post-create/post-todos-201 - 2024-03-22 - weekly - 0.5 - - \ No newline at end of file diff --git a/challenger/src/main/resources/seo/schema-author.properties b/challenger/src/main/resources/seo/schema-author.properties deleted file mode 100644 index dafac3b6..00000000 --- a/challenger/src/main/resources/seo/schema-author.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=Alan Richardson -url=https://www.eviltester.com/page/about/ -jobTitle=Software Testing and Development Consultant -sameAs=https://www.linkedin.com/in/eviltester/,https://github.com/eviltester \ No newline at end of file diff --git a/challenger/src/main/resources/seo/schema-publisher.properties b/challenger/src/main/resources/seo/schema-publisher.properties deleted file mode 100644 index 9eda4343..00000000 --- a/challenger/src/main/resources/seo/schema-publisher.properties +++ /dev/null @@ -1,6 +0,0 @@ -name=eviltester.com -url=https://eviltester.com/ -legalName=Compendium Developments Ltd -contactType=customer support -email=contact@eviltester.com -sameAs=https://www.youtube.com/@EvilTesterVideos,https://www.linkedin.com/in/eviltester/,https://github.com/eviltester,https://www.compendiumdev.co.uk \ No newline at end of file diff --git a/challenger/src/main/resources/site/index.md b/challenger/src/main/resources/site/index.md deleted file mode 100644 index 436d3422..00000000 --- a/challenger/src/main/resources/site/index.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: The API Challenges - API Tutorials and API Testing Practice Exercises -seo_title: API Challenges Tutorials and Testing Practice -description: A practice API application with tutorials for HTTP and REST APIs. Guided exercises and gamification hands on learning path. -lastmod: 2026-02-18 -template: index -canonical: https://apichallenges.eviltester.com ---- - -# The API Challenges - -## A learning environment to experiment, explore, and play with API Testing and Tooling - -Welcome to The API Challenges, a site dedicated to helping you learn API Testing. -With all the tutorials, practice APIs and information you need to practice and improve your API Testing skills. - -We provide: - -- A simple functioning API for you to practice API Testing -- A learning experience based on a full API, with a set of challenges to help you learn API Testing -- A multi-user playground to experiment within -- API documentation to read and learn from -- An API Simulator to practice your tooling -- An HTTP Mirror API tool to 'see' the full details of your API Tool requests. - -## How To Learn API Testing - -- Start by Reading through our Learning Zone content - - [What is an API?](/tutorials/rest-api-basics) - - [How to Learn API Testing?](/learning) -- Download an API Client - - [Recommended API Clients](/tools/clients) -- Use the API Simulator - - [follow our step by step guide](/practice-modes/simulation) -- Experiment with a Real API - - our [Simple API](/practice-modes/simpleapi) does not require any authentication and supports all HTTP Requests -- Work through the API Challenges - - [solve each of the API Challenges](/gui/challenges) to gain practical experience of Testing APIs -- Experiment with other APIs - - we maintain a list of [Recommended Practice API Sites](/practice-sites) - -## Video Welcome - -{{}} - -[Learn how to Test APIs with our online tutorials and guides](/learning) - -## The API Simulator - -If you want a follow along guide to learn basic API interaction then you can use our API Simulator. - -It is a real API so you can experiment with different content types, but no data is saved to the server and all responses use pre-defined data for the simulated scenario. - -This is the safest and easiest way to start learning API interaction before moving on to a live API. - -Start working through the [API Simulator Scenarios](/practice-modes/simulation). - -## The Simple API - -When reviewing all the practice APIs available we noticed a lack of APIs that allow you to experiment with all HTTP verbs without authentication and with no risk of releasing private information. - -Our Simple API has a domain model that does not allow any unwanted data exposure. Additionally there is a limit on the number of records that can be created, and test data is automatically added when there are a low number of records in the system. - -Use our [Simple API](/practice-modes/simpleapi) to experiment with all basic HTTP Verbs for creation, updating and retrieval. - -## The API Challenges Application - -The API Challenges is a simple and fully functional API for managing a To-Do list. - -You need to authenticate to create a session in the system. This will create a unique API repository with basic test data. -As you interact with the API there are a variety of challenges to complete e.g. Get all TODOs, Delete a Todo, etc. - -Each challenge is designed to teach you some aspect of how APIs work, and how to test them. - -[Get started with the API Challenges](/apichallenges) - -## Tooling - -We recommend that you use an [API GUI Client](/tools/clients) to interact with the API. - -API Clients make it easy to send requests to the API and view the responses. - -Most API Clients will also import Open API format files which document the API. -By importing an Open API file into your client you will see all the requests that -you can use on the API and make it easy to get started. - -All the APIs on our site have Open API files that you can import into your favourite tool. - -If you don't know what API Client to use then you can find a list of recommended clients in our Learning Zone. - -Learn more about: - -- [HTTP/REST Clients](/tools/clients) -- [HTTP Proxies](/tools/proxies) - - -## Books - -Recommended books for learning about API Testing are: - -- [Automating and Testing a REST API](https://compendiumdev.co.uk/page.php?title=tracksrestapibook) - -## More Information - -- created by [Alan Richardson](https://www.linkedin.com/in/eviltester/) -- [@EvilTester](https://twitter.com/eviltester) -- [EvilTester.com](https://eviltester.com) -- [github.com/eviltester](https://github.com/eviltester) - -## Sponsors - -This application is sponsored by [EvilTester.com](https://eviltester.com) - a blog and free online training and books for Software Testing and Development. - -For a full list of corporate sponsors and how you can sponsor and support this application check out our [sponsorship page](/sponsors) - -Individuals can support this project through [Patreon](https://patreon.com/eviltester), or Github Sponsors - - diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/ChallengeAuthDataTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/ChallengeAuthDataTest.java deleted file mode 100644 index 12a836c3..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/ChallengeAuthDataTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package uk.co.compendiumdev.challenge; - -import java.util.Arrays; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class ChallengeAuthDataTest { - - @Test - void canCreateAuthData() { - - final long timeNow = System.currentTimeMillis(); - - ChallengerAuthData authData = new ChallengerAuthData(Arrays.asList(CHALLENGE.values())); - - Assertions.assertTrue(authData.getLastAccessed() >= timeNow); - Assertions.assertTrue(authData.expiresAt() >= timeNow + 30000); - Assertions.assertNotNull(authData.getXChallenger()); - Assertions.assertNotNull(authData.getXAuthToken()); - Assertions.assertNotNull(authData.getNote()); - Assertions.assertEquals("", authData.getNote()); - Assertions.assertFalse(authData.statusOfChallenge(CHALLENGE.GET_CHALLENGES)); - } - - @Test - void canOverrideGuid() { - ChallengerAuthData authData = new ChallengerAuthData(Arrays.asList(CHALLENGE.values())); - authData.setXChallengerGUID("bob"); - Assertions.assertEquals("bob", authData.getXChallenger()); - } - - @Test - void canCreatePassAChallenge() { - - ChallengerAuthData authData = new ChallengerAuthData(Arrays.asList(CHALLENGE.values())); - - authData.pass(CHALLENGE.GET_CHALLENGES); - - Assertions.assertTrue(authData.statusOfChallenge(CHALLENGE.GET_CHALLENGES)); - } - - @Test - void canPassAllChallenges() { - - ChallengerAuthData authData = new ChallengerAuthData(Arrays.asList(CHALLENGE.values())); - - for (CHALLENGE challenge : CHALLENGE.values()) { - authData.pass(challenge); - } - - Assertions.assertTrue(authData.statusOfChallenge(CHALLENGE.GET_CHALLENGES)); - Assertions.assertTrue(authData.statusOfChallenge(CHALLENGE.GET_HEARTBEAT_204)); - - for (CHALLENGE challenge : CHALLENGE.values()) { - Assertions.assertTrue(authData.statusOfChallenge(challenge)); - } - } - - @Test - void canNotSetNullNote() { - ChallengerAuthData authData = new ChallengerAuthData(Arrays.asList(CHALLENGE.values())); - - Assertions.assertThrows(RuntimeException.class, () -> authData.setNote(null)); - } - - @Test - void canSetEmptyNote() { - ChallengerAuthData authData = new ChallengerAuthData(Arrays.asList(CHALLENGE.values())); - - authData.setNote(""); - Assertions.assertEquals("", authData.getNote()); - } - - @Test - void noteTruncatedTo100Chars() { - ChallengerAuthData authData = new ChallengerAuthData(Arrays.asList(CHALLENGE.values())); - - String morethanonehundred = stringOfLength(101); - Assertions.assertEquals(101, morethanonehundred.length()); - - authData.setNote(morethanonehundred); - Assertions.assertEquals(100, authData.getNote().length()); - - authData.setNote(stringOfLength(200)); - Assertions.assertEquals(100, authData.getNote().length()); - } - - private String stringOfLength(final int desiredLength) { - String ofLength = ""; - while (ofLength.length() < desiredLength) { - ofLength = ofLength + "a"; - } - - return ofLength; - } - - @Test - void noteAcceptedAt100CharsOrLess() { - ChallengerAuthData authData = new ChallengerAuthData(Arrays.asList(CHALLENGE.values())); - - authData.setNote(stringOfLength(100)); - Assertions.assertEquals(100, authData.getNote().length()); - - authData.setNote(stringOfLength(99)); - Assertions.assertEquals(99, authData.getNote().length()); - - authData.setNote(stringOfLength(1)); - Assertions.assertEquals(1, authData.getNote().length()); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/ChallengerConfigRepositoryTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/ChallengerConfigRepositoryTest.java deleted file mode 100644 index 8d792be0..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/ChallengerConfigRepositoryTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package uk.co.compendiumdev.challenge; - -import java.nio.file.Path; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.thingifier.core.repository.ThingStoreProvider; -import uk.co.compendiumdev.thingifier.core.repository.inmemory.InMemoryThingStoreProvider; -import uk.co.compendiumdev.thingifier.core.repository.sqlite.SqliteThingStoreProvider; - -public class ChallengerConfigRepositoryTest { - - @Test - void defaultSimulationRepositoryIsMemory() { - ChallengerConfig config = new ChallengerConfig(); - config.setSimulationRepositoryFromArgs(new String[] {}); - - Assertions.assertEquals( - "memory", config.getSimulationRepositoryConfig().getRepositoryMode()); - - try (ThingStoreProvider provider = - config.getSimulationRepositoryConfig().createProvider()) { - Assertions.assertTrue(provider instanceof InMemoryThingStoreProvider); - } - } - - @Test - void simRepositoryFlagCanUseSqliteMemory() { - ChallengerConfig config = new ChallengerConfig(); - config.setSimulationRepositoryFromArgs(new String[] {"-sim-repository=sqlite-memory"}); - - Assertions.assertEquals( - "sqlite-memory", config.getSimulationRepositoryConfig().getRepositoryMode()); - - try (ThingStoreProvider provider = - config.getSimulationRepositoryConfig().createProvider()) { - Assertions.assertTrue(provider instanceof SqliteThingStoreProvider); - } - } - - @Test - void simSqliteMemoryAliasCanUseSqliteMemory() { - ChallengerConfig config = new ChallengerConfig(); - config.setSimulationRepositoryFromArgs(new String[] {"-sim-sqlite-memory"}); - - Assertions.assertEquals( - "sqlite-memory", config.getSimulationRepositoryConfig().getRepositoryMode()); - - try (ThingStoreProvider provider = - config.getSimulationRepositoryConfig().createProvider()) { - Assertions.assertTrue(provider instanceof SqliteThingStoreProvider); - } - } - - @Test - void mainRepositorySqliteMemoryFlagDoesNotConfigureSimulationRepository() { - ChallengerConfig config = new ChallengerConfig(); - config.setSimulationRepositoryFromArgs(new String[] {"-sqlite-memory"}); - - Assertions.assertEquals( - "memory", config.getSimulationRepositoryConfig().getRepositoryMode()); - } - - @Test - void simSqliteFileCanUseSeparateDirectory() { - ChallengerConfig config = new ChallengerConfig(); - config.setSimulationRepositoryFromArgs( - new String[] { - "-sim-repository=sqlite-file", - "-sim-sqlite-directory=target/simulation-repository-test" - }); - - Assertions.assertEquals( - "sqlite-file", config.getSimulationRepositoryConfig().getRepositoryMode()); - Assertions.assertEquals( - Path.of("target/simulation-repository-test"), - config.getSimulationRepositoryConfig().getSqliteDirectory()); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/IndexNowRouteHandlerTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/IndexNowRouteHandlerTest.java deleted file mode 100644 index d8075f02..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/IndexNowRouteHandlerTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package uk.co.compendiumdev.challenge; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.thingifier.adapter.httpserver.HttpRouteHandler; - -public class IndexNowRouteHandlerTest { - - @Test - public void routeCanBeConfiguredWhenValuesAreValid() { - - AtomicReference capturedPath = new AtomicReference<>(null); - AtomicReference capturedRoute = new AtomicReference<>(null); - - BiConsumer registrar = - (path, HttpRouteHandler) -> { - capturedPath.set(path); - capturedRoute.set(HttpRouteHandler); - }; - - IndexNowRouteHandler handler = - new IndexNowRouteHandler("/myIndexNowKey63638.txt", "abc123", registrar); - - Assertions.assertTrue(handler.configureRoutes()); - Assertions.assertEquals("/myIndexNowKey63638.txt", capturedPath.get()); - Assertions.assertNotNull(capturedRoute.get()); - } - - @Test - public void routeReturnsConfiguredKey() throws Exception { - - AtomicReference capturedRoute = new AtomicReference<>(null); - - IndexNowRouteHandler handler = - new IndexNowRouteHandler( - "/myIndexNowKey63638.txt", - "abc123", - (path, HttpRouteHandler) -> capturedRoute.set(HttpRouteHandler)); - - handler.configureRoutes(); - - Assertions.assertNotNull(capturedRoute.get()); - Assertions.assertEquals("abc123", capturedRoute.get().handle(null, null)); - } - - @Test - public void doesNotConfigureRouteWhenKeyMissing() { - - AtomicReference capturedRoute = new AtomicReference<>(null); - - IndexNowRouteHandler handler = - new IndexNowRouteHandler( - "/myIndexNowKey63638.txt", - " ", - (path, HttpRouteHandler) -> capturedRoute.set(HttpRouteHandler)); - - Assertions.assertFalse(handler.configureRoutes()); - Assertions.assertNull(capturedRoute.get()); - } - - @Test - public void doesNotConfigureRouteWhenLocationMissing() { - - AtomicReference capturedRoute = new AtomicReference<>(null); - - IndexNowRouteHandler handler = - new IndexNowRouteHandler( - " ", - "abc123", - (path, HttpRouteHandler) -> capturedRoute.set(HttpRouteHandler)); - - Assertions.assertFalse(handler.configureRoutes()); - Assertions.assertNull(capturedRoute.get()); - } - - @Test - public void locationMustStartWithSlashAndEndWithTxt() { - Assertions.assertTrue(IndexNowRouteHandler.isValidLocation("/myIndexNowKey63638.txt")); - Assertions.assertFalse(IndexNowRouteHandler.isValidLocation("myIndexNowKey63638.txt")); - Assertions.assertFalse(IndexNowRouteHandler.isValidLocation("/myIndexNowKey63638")); - } - - @Test - public void locationMustNotContainUnsafeCharacters() { - Assertions.assertFalse(IndexNowRouteHandler.isValidLocation("/my*IndexNowKey63638.txt")); - Assertions.assertFalse(IndexNowRouteHandler.isValidLocation("/myIndexNowKey63638.txt?x=1")); - Assertions.assertFalse( - IndexNowRouteHandler.isValidLocation("/myIndexNowKey63638.txt#hash")); - Assertions.assertFalse(IndexNowRouteHandler.isValidLocation("/my IndexNowKey63638.txt")); - Assertions.assertFalse(IndexNowRouteHandler.isValidLocation("/my\\IndexNowKey63638.txt")); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModelRepositoryTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModelRepositoryTest.java deleted file mode 100644 index cb705e53..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModelRepositoryTest.java +++ /dev/null @@ -1,262 +0,0 @@ -package uk.co.compendiumdev.challenge.apimodel; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; -import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; -import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; -import uk.co.compendiumdev.thingifier.core.repository.sqlite.SqliteThingStore; -import uk.co.compendiumdev.thingifier.core.repository.sqlite.SqliteThingStoreProvider; - -public class ChallengeApiModelRepositoryTest { - - @Test - public void challengeApiCanUseSqliteInMemoryRepository() { - try (SqliteThingStoreProvider provider = SqliteThingStoreProvider.inMemory()) { - Thingifier thingifier = new ChallengeApiModel().get(provider); - - Assertions.assertTrue( - thingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME) - instanceof SqliteThingStore); - Assertions.assertEquals( - 10, - thingifier - .getStore(EntityRelModel.DEFAULT_DATABASE_NAME) - .entityQueries() - .count( - thingifier - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"))); - } - } - - @Test - public void challengeApiGetUsesSqliteRepositoryReadsForSimpleTodoQueries() { - try (SqliteThingStoreProvider provider = SqliteThingStoreProvider.inMemory()) { - Thingifier thingifier = new ChallengeApiModel().get(provider); - ThingStore repository = thingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - EntityDefinition todo = thingifier.getDefinitionNamed("todo"); - - EntityInstance firstTodo = repository.entityQueries().findByQueryIdentifier(todo, "1"); - repository - .entities() - .patch( - firstTodo, - EntityInstanceDraft.forEntity(todo).withField("doneStatus", "true")); - - ApiResponse allTodos = - thingifier.api().get("/todos", new QueryFilterParams(), new HttpHeadersBlock()); - Assertions.assertEquals(200, allTodos.getStatusCode()); - Assertions.assertEquals(10, allTodos.getReturnedInstanceCollection().size()); - - ApiResponse todoById = - thingifier - .api() - .get("/todos/1", new QueryFilterParams(), new HttpHeadersBlock()); - Assertions.assertEquals(200, todoById.getStatusCode()); - Assertions.assertEquals(1, todoById.getReturnedInstanceCollection().size()); - Assertions.assertEquals( - "1", todoById.getReturnedInstanceCollection().get(0).getPrimaryKeyValue()); - - QueryFilterParams doneStatusFilter = new QueryFilterParams(); - doneStatusFilter.put("doneStatus", "false"); - ApiResponse notDoneTodos = - thingifier.api().get("/todos", doneStatusFilter, new HttpHeadersBlock()); - Assertions.assertEquals(200, notDoneTodos.getStatusCode()); - Assertions.assertEquals(9, notDoneTodos.getReturnedInstanceCollection().size()); - - QueryFilterParams idRangeFilter = new QueryFilterParams(); - idRangeFilter.put("id", ">=2"); - ApiResponse idRangeTodos = - thingifier.api().get("/todos", idRangeFilter, new HttpHeadersBlock()); - Assertions.assertEquals(200, idRangeTodos.getStatusCode()); - Assertions.assertEquals(9, idRangeTodos.getReturnedInstanceCollection().size()); - - QueryFilterParams sortByDescendingId = new QueryFilterParams(); - sortByDescendingId.put("sortBy", "-id"); - ApiResponse sortedTodos = - thingifier.api().get("/todos", sortByDescendingId, new HttpHeadersBlock()); - Assertions.assertEquals(200, sortedTodos.getStatusCode()); - Assertions.assertEquals( - "10", sortedTodos.getReturnedInstanceCollection().get(0).getPrimaryKeyValue()); - } - } - - @Test - public void relationshipGetRoutesStillFallBackToLegacyQueryTraversal() { - try (SqliteThingStoreProvider provider = SqliteThingStoreProvider.inMemory()) { - Thingifier thingifier = new Thingifier(new EntityRelModel(provider)); - - EntityDefinition project = thingifier.defineThing("project", "projects"); - project.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - project.addField(Field.is("title", FieldType.STRING)); - - EntityDefinition task = thingifier.defineThing("task", "tasks"); - task.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - task.addField(Field.is("title", FieldType.STRING)); - - thingifier - .defineRelationship(project, task, "tasks", Cardinality.ONE_TO_MANY()) - .whenReversed(Cardinality.ONE_TO_ONE(), "task-of"); - - ThingStore repository = thingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - EntityInstance projectInstance = - repository - .entities() - .create( - EntityInstanceDraft.forEntity(project) - .withField("title", "Office")); - EntityInstance taskInstance = - repository - .entities() - .create( - EntityInstanceDraft.forEntity(task) - .withField("title", "File paperwork")); - repository.relationships().connect(projectInstance, "tasks", taskInstance); - - ApiResponse apiResponse = - thingifier - .api() - .get( - "/project/1/tasks", - new QueryFilterParams(), - new HttpHeadersBlock()); - - Assertions.assertEquals(200, apiResponse.getStatusCode()); - Assertions.assertEquals(1, apiResponse.getReturnedInstanceCollection().size()); - Assertions.assertEquals( - "File paperwork", - apiResponse - .getReturnedInstanceCollection() - .get(0) - .getFieldValue("title") - .asString()); - } - } - - @Test - public void simpleEntityWritesUseSqliteRepositoryWithoutLoadingCompatibilitySnapshot() { - try (SqliteThingStoreProvider provider = SqliteThingStoreProvider.inMemory()) { - Thingifier thingifier = new Thingifier(new EntityRelModel(provider)); - - EntityDefinition note = thingifier.defineThing("note", "notes"); - note.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - note.addField(Field.is("title", FieldType.STRING)); - - ThingStore repository = thingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - - ApiResponse create = - thingifier - .api() - .post( - "/notes", - bodyParser(thingifier, "{\"title\":\"created\"}"), - new HttpHeadersBlock()); - - Assertions.assertEquals(201, create.getStatusCode()); - Assertions.assertEquals(1, repository.entityQueries().list(note).size()); - - ApiResponse postAmend = - thingifier - .api() - .post( - "/notes/1", - bodyParser(thingifier, "{\"title\":\"posted\"}"), - new HttpHeadersBlock()); - - Assertions.assertEquals(200, postAmend.getStatusCode()); - Assertions.assertEquals( - "posted", - repository - .entityQueries() - .findByQueryIdentifier(note, "1") - .getFieldValue("title") - .asString()); - - ApiResponse putAmend = - thingifier - .api() - .put( - "/notes/1", - bodyParser(thingifier, "{\"title\":\"put\"}"), - new HttpHeadersBlock()); - - Assertions.assertEquals(200, putAmend.getStatusCode()); - Assertions.assertEquals( - "put", - repository - .entityQueries() - .findByQueryIdentifier(note, "1") - .getFieldValue("title") - .asString()); - - ApiResponse delete = thingifier.api().delete("/notes/1", new HttpHeadersBlock()); - - Assertions.assertEquals(200, delete.getStatusCode()); - Assertions.assertEquals(0, repository.entityQueries().list(note).size()); - } - } - - @Test - public void sqliteMemoryProvidersHaveSeparateNamedDatabaseLifetimes() { - EntityRelModel firstModel = null; - EntityRelModel secondModel = null; - try (SqliteThingStoreProvider firstProvider = SqliteThingStoreProvider.inMemory(); - SqliteThingStoreProvider secondProvider = SqliteThingStoreProvider.inMemory()) { - firstModel = new EntityRelModel(firstProvider); - secondModel = new EntityRelModel(secondProvider); - - EntityDefinition firstNote = firstModel.createEntityDefinition("note", "notes"); - firstNote.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - firstNote.addField(Field.is("title", FieldType.STRING)); - - EntityDefinition secondNote = secondModel.createEntityDefinition("note", "notes"); - secondNote.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - secondNote.addField(Field.is("title", FieldType.STRING)); - - firstModel - .getStore(EntityRelModel.DEFAULT_DATABASE_NAME) - .entities() - .create(EntityInstanceDraft.forEntity(firstNote).withField("title", "first")); - - Assertions.assertEquals( - 1, - firstModel - .getStore(EntityRelModel.DEFAULT_DATABASE_NAME) - .entityQueries() - .list(firstNote) - .size()); - Assertions.assertEquals( - 0, - secondModel - .getStore(EntityRelModel.DEFAULT_DATABASE_NAME) - .entityQueries() - .list(secondNote) - .size()); - } finally { - if (firstModel != null) { - firstModel.close(); - } - if (secondModel != null) { - secondModel.close(); - } - } - } - - private BodyParser bodyParser(final Thingifier thingifier, final String json) { - return new BodyParser( - new HttpApiRequest("/path").setBody(json), thingifier.getThingNames()); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerApiResponseHookTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerApiResponseHookTest.java deleted file mode 100644 index 0799f493..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerApiResponseHookTest.java +++ /dev/null @@ -1,215 +0,0 @@ -package uk.co.compendiumdev.challenge.challengehooks; - -import static uk.co.compendiumdev.thingifier.api.http.HttpApiRequest.VERB.DELETE; -import static uk.co.compendiumdev.thingifier.api.http.HttpApiRequest.VERB.GET; -import static uk.co.compendiumdev.thingifier.api.http.HttpApiRequest.VERB.POST; - -import java.util.Arrays; -import java.util.Map; -import java.util.function.Supplier; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.api.ermodelconversion.JsonThing; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; -import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; -import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; -import uk.co.compendiumdev.thingifier.core.repository.ThingStoreProvider; -import uk.co.compendiumdev.thingifier.core.repository.inmemory.InMemoryThingStoreProvider; -import uk.co.compendiumdev.thingifier.core.repository.sqlite.SqliteThingStoreProvider; - -public class ChallengerApiResponseHookTest { - - @ParameterizedTest(name = "{0}") - @MethodSource("repositoryProviders") - public void filteredTodosChallengeCompletesWhenDoneAndNotDoneTodosExist( - final String repositoryName, final Supplier providerFactory) { - - try (HookFixture fixture = new HookFixture(providerFactory.get())) { - fixture.addTodo("done", "true"); - fixture.addTodo("not done", "false"); - - fixture.hook.run( - fixture.request("todos", GET).setQueryParams(Map.of("doneStatus", "true")), - fixture.apiResponse(200), - fixture.thingifier.apiConfig()); - - Assertions.assertTrue( - fixture.challenger.statusOfChallenge(CHALLENGE.GET_TODOS_FILTERED)); - } - } - - @ParameterizedTest(name = "{0}") - @MethodSource("repositoryProviders") - public void filteredTodosChallengeDoesNotCompleteWithoutMixedDoneStatusTodos( - final String repositoryName, final Supplier providerFactory) { - - try (HookFixture fixture = new HookFixture(providerFactory.get())) { - fixture.addTodo("done", "true"); - - fixture.hook.run( - fixture.request("todos", GET).setQueryParams(Map.of("doneStatus", "true")), - fixture.apiResponse(200), - fixture.thingifier.apiConfig()); - - Assertions.assertFalse( - fixture.challenger.statusOfChallenge(CHALLENGE.GET_TODOS_FILTERED)); - } - } - - @ParameterizedTest(name = "{0}") - @MethodSource("repositoryProviders") - public void postMaxContentChallengeReadsCreatedTodoThroughRepository( - final String repositoryName, final Supplier providerFactory) { - - try (HookFixture fixture = new HookFixture(providerFactory.get())) { - EntityInstance todo = - fixture.addTodo( - "2*4*6*8*11*14*17*20*23*26*29*32*35*38*41*44*47*50*", - "true", - "*3*5*7*9*12*15*18*21*24*27*30*33*36*39*42*45*48*51*" - + "54*57*60*63*66*69*72*75*78*81*84*87*90*93*96*100*" - + "104*108*112*116*120*124*128*132*136*140*144*148*" - + "152*156*160*164*168*172*176*180*184*188*192*196*200*"); - - ApiResponse created = - new ApiResponse(201).setLocationHeader("/todos/" + todo.getPrimaryKeyValue()); - - fixture.hook.run( - fixture.request("todos", POST), - fixture.apiResponse(created), - fixture.thingifier.apiConfig()); - - Assertions.assertTrue( - fixture.challenger.statusOfChallenge( - CHALLENGE.POST_MAX_OUT_TITLE_DESCRIPTION_LENGTH)); - } - } - - @ParameterizedTest(name = "{0}") - @MethodSource("repositoryProviders") - public void deleteAllTodosChallengeCompletesWhenRepositoryIsEmpty( - final String repositoryName, final Supplier providerFactory) { - - try (HookFixture fixture = new HookFixture(providerFactory.get())) { - EntityInstance todo = fixture.addTodo("delete me", "false"); - fixture.repository.entities().delete(todo); - - fixture.hook.run( - fixture.request("todos/" + todo.getPrimaryKeyValue(), DELETE), - fixture.apiResponse(200), - fixture.thingifier.apiConfig()); - - Assertions.assertTrue(fixture.challenger.statusOfChallenge(CHALLENGE.DELETE_ALL_TODOS)); - } - } - - @ParameterizedTest(name = "{0}") - @MethodSource("repositoryProviders") - public void deleteAllTodosChallengeDoesNotCompleteWhenRepositoryStillHasTodos( - final String repositoryName, final Supplier providerFactory) { - - try (HookFixture fixture = new HookFixture(providerFactory.get())) { - EntityInstance deletedTodo = fixture.addTodo("delete me", "false"); - fixture.addTodo("keep me", "false"); - fixture.repository.entities().delete(deletedTodo); - - fixture.hook.run( - fixture.request("todos/" + deletedTodo.getPrimaryKeyValue(), DELETE), - fixture.apiResponse(200), - fixture.thingifier.apiConfig()); - - Assertions.assertFalse( - fixture.challenger.statusOfChallenge(CHALLENGE.DELETE_ALL_TODOS)); - } - } - - private static Stream repositoryProviders() { - return Stream.of( - Arguments.of( - "in-memory", - (Supplier) InMemoryThingStoreProvider::new), - Arguments.of( - "sqlite-memory", - (Supplier) SqliteThingStoreProvider::inMemory)); - } - - private static class HookFixture implements AutoCloseable { - private final Thingifier thingifier; - private final Challengers challengers; - private final ChallengerAuthData challenger; - private final ChallengerApiResponseHook hook; - private final EntityDefinition todo; - private final ThingStore repository; - - HookFixture(final ThingStoreProvider provider) { - thingifier = new Thingifier(new EntityRelModel(provider)); - todo = thingifier.defineThing("todo", "todos"); - todo.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); - todo.addFields( - Field.is("title", FieldType.STRING), - Field.is("doneStatus", FieldType.BOOLEAN).withDefaultValue("false"), - Field.is("description", FieldType.STRING)); - - challengers = - new Challengers(thingifier.getERmodel(), Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - challenger = challengers.createNewChallenger(); - thingifier.ensureCreatedAndPopulatedInstanceDatabaseNamed(challenger.getXChallenger()); - repository = thingifier.getStore(challenger.getXChallenger()); - hook = new ChallengerApiResponseHook(challengers, thingifier); - } - - EntityInstance addTodo(final String title, final String doneStatus) { - return addTodo(title, doneStatus, ""); - } - - EntityInstance addTodo( - final String title, final String doneStatus, final String description) { - return repository - .entities() - .create( - EntityInstanceDraft.forEntity(todo) - .withField("title", title) - .withField("doneStatus", doneStatus) - .withField("description", description)); - } - - HttpApiRequest request(final String path, final HttpApiRequest.VERB verb) { - return new HttpApiRequest(path) - .setVerb(verb) - .addHeader("X-CHALLENGER", challenger.getXChallenger()); - } - - HttpApiResponse apiResponse(final int statusCode) { - return apiResponse(new ApiResponse(statusCode)); - } - - HttpApiResponse apiResponse(final ApiResponse apiResponse) { - return new HttpApiResponse( - new HttpHeadersBlock(), - apiResponse, - new JsonThing(thingifier.apiConfig().jsonOutput()), - thingifier.apiConfig()); - } - - @Override - public void close() { - thingifier.close(); - } - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHttpRequestHookTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHttpRequestHookTest.java deleted file mode 100644 index 610720f2..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHttpRequestHookTest.java +++ /dev/null @@ -1,186 +0,0 @@ -package uk.co.compendiumdev.challenge.challengehooks; - -import java.lang.reflect.Field; -import java.util.Arrays; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpMethod; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; - -public class ChallengerInternalHttpRequestHookTest { - - @Test - public void inMultUserModeWeNeedAnXChallengerHeaderToTrackChallenges() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - final ChallengerInternalHTTPRequestHook hook = - new ChallengerInternalHTTPRequestHook(challengers); - - final InternalHttpRequest request = - new InternalHttpRequest("/challenges").setVerb(InternalHttpMethod.GET); - - hook.run(request); - - Assertions.assertFalse( - challengers.SINGLE_PLAYER.statusOfChallenge(CHALLENGE.GET_CHALLENGES)); - } - - @Test - public void inMultUserModeAddingAnXChallengerHeaderWillTouchTheChallenger() - throws NoSuchFieldException, IllegalAccessException { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - // risk that this test is intermittent if it all happens in the same millisecond - // if so could 'wait' in the test, or 'hack the object to have a different last accessed - // time' - // set private variable that test will 'touch' - // when using mock at HTTP server level, did not need to 'hack' the object - Field lastAccessedField = ChallengerAuthData.class.getDeclaredField("lastAccessed"); - lastAccessedField.setAccessible(true); - lastAccessedField.set(challenger, 0L); - Assertions.assertEquals(0, challenger.getLastAccessed()); - - long initialTouchTime = challenger.getLastAccessed(); - - challengers.setMultiPlayerMode(); - - long preTouchTime = System.currentTimeMillis(); - - final ChallengerInternalHTTPRequestHook hook = - new ChallengerInternalHTTPRequestHook(challengers); - - final InternalHttpRequest request = - new InternalHttpRequest("/challenges").setVerb(InternalHttpMethod.GET); - request.addHeader("X-CHALLENGER", challenger.getXChallenger()); - - hook.run(request); - - // we passed the GET /challenges for default user - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_CHALLENGES)); - - Assertions.assertNotEquals(0, challenger.getLastAccessed()); - Assertions.assertNotEquals(initialTouchTime, challenger.getLastAccessed()); - Assertions.assertTrue(challenger.getLastAccessed() >= preTouchTime); - } - - @Test - public void inMultUserModeWeNeedAnXChallengerHeaderThatExistsToTrackChallenges() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - final ChallengerInternalHTTPRequestHook hook = - new ChallengerInternalHTTPRequestHook(challengers); - - final InternalHttpRequest request = - new InternalHttpRequest("/challenges") - .setVerb(InternalHttpMethod.GET) - .addHeader("X-CHALLENGER", "bobobobobobobobobobo"); - - hook.run(request); - - // nothing changes - assert on this - // we passed the GET /challenges for default user - Assertions.assertFalse( - challengers.SINGLE_PLAYER.statusOfChallenge(CHALLENGE.GET_CHALLENGES)); - } - - @Test - public void inSinglePlayerModeNoHeaderMeansUseDefaultChallenger() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - - final ChallengerInternalHTTPRequestHook hook = - new ChallengerInternalHTTPRequestHook(challengers); - - final InternalHttpRequest request = - new InternalHttpRequest("/challenges").setVerb(InternalHttpMethod.GET); - - hook.run(request); - - // we passed the GET /challenges for default user - Assertions.assertTrue( - challengers.SINGLE_PLAYER.statusOfChallenge(CHALLENGE.GET_CHALLENGES)); - } - - @Test - public void canGetHeartbeatInMultiPlayer() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - ChallengerInternalHTTPRequestHook hook = new ChallengerInternalHTTPRequestHook(challengers); - - InternalHttpRequest request = new InternalHttpRequest("/heartbeat").setVerb("GET"); - request.addHeader("X-CHALLENGER", challenger.getXChallenger()); - hook.run(request); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_HEARTBEAT_204)); - } - - @Test - public void canTraceHeartbeatInMultiPlayer() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - ChallengerInternalHTTPRequestHook hook = new ChallengerInternalHTTPRequestHook(challengers); - - Assertions.assertFalse(challenger.statusOfChallenge(CHALLENGE.TRACE_HEARTBEAT_501)); - - InternalHttpRequest request = new InternalHttpRequest("/heartbeat").setVerb("TRACE"); - request.addHeader("X-CHALLENGER", challenger.getXChallenger()); - hook.run(request); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.TRACE_HEARTBEAT_501)); - } - - @Test - public void canDeleteHeartbeatInMultiPlayer() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - ChallengerInternalHTTPRequestHook hook = new ChallengerInternalHTTPRequestHook(challengers); - - Assertions.assertFalse(challenger.statusOfChallenge(CHALLENGE.DELETE_HEARTBEAT_405)); - - InternalHttpRequest request = new InternalHttpRequest("/heartbeat").setVerb("DELETE"); - request.addHeader("X-CHALLENGER", challenger.getXChallenger()); - hook.run(request); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.DELETE_HEARTBEAT_405)); - } - - @Test - public void canPatchHeartbeatInMultiPlayer() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - ChallengerInternalHTTPRequestHook hook = new ChallengerInternalHTTPRequestHook(challengers); - - Assertions.assertFalse(challenger.statusOfChallenge(CHALLENGE.PATCH_HEARTBEAT_500)); - - InternalHttpRequest request = new InternalHttpRequest("/heartbeat").setVerb("PATCH"); - request.addHeader("X-CHALLENGER", challenger.getXChallenger()); - hook.run(request); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.PATCH_HEARTBEAT_500)); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHttpResponseHookTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHttpResponseHookTest.java deleted file mode 100644 index 448cec9e..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengehooks/ChallengerInternalHttpResponseHookTest.java +++ /dev/null @@ -1,349 +0,0 @@ -package uk.co.compendiumdev.challenge.challengehooks; - -import java.util.Arrays; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpMethod; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpResponse; - -public class ChallengerInternalHttpResponseHookTest { - - @Test - public void inMultUserModeWeNeedAnNonNullXChallengerHeaderOrSeeUnknownChallengerHeader() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - final ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final InternalHttpRequest request = - new InternalHttpRequest("/challenges").setVerb(InternalHttpMethod.GET); - - final InternalHttpResponse response = new InternalHttpResponse(); - - hook.run(request, response); - - Assertions.assertTrue(response.getHeader("X-CHALLENGER").startsWith("UNKNOWN CHALLENGER")); - } - - @Test - public void inMultUserModeWeNeedAValidXChallengerHeaderOrSeeUnknownChallengerHeader() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - final ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final InternalHttpRequest request = - new InternalHttpRequest("/challenges") - .setVerb(InternalHttpMethod.GET) - .addHeader("X-CHALLENGER", "bob"); - - final InternalHttpResponse response = new InternalHttpResponse(); - - hook.run(request, response); - - Assertions.assertTrue(response.getHeader("X-CHALLENGER").startsWith("UNKNOWN CHALLENGER")); - } - - @Test - public void canPostChallengerToCreateAChallenger() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - InternalHttpRequest request = new InternalHttpRequest("/challenger").setVerb("POST"); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpResponse response = - new InternalHttpResponse() - .setStatus(201) - .setHeader("X-CHALLENGER", challenger.getXChallenger()); - - hook.run(request, response); - - Assertions.assertNotNull(response.getHeader("X-CHALLENGER")); - - String guid = response.getHeader("X-CHALLENGER"); - final ChallengerAuthData achallenger = challengers.getChallenger(guid); - - Assertions.assertTrue(achallenger.statusOfChallenge(CHALLENGE.CREATE_NEW_CHALLENGER)); - } - - @Test - public void givenAValidChallengerAddTheGuidIntoTheResponse() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/challenger") - .setVerb("GET") - .addHeader("X-CHALLENGER", challenger.getXChallenger()); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(200); - - hook.run(request, response); - - Assertions.assertNotNull(response.getHeader("X-CHALLENGER")); - - String guid = response.getHeader("X-CHALLENGER"); - final ChallengerAuthData achallenger = challengers.getChallenger(guid); - - Assertions.assertEquals(achallenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - public void getTodoWhichReturns404PassesChallenge() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/todo") - .setVerb("GET") - .addHeader("X-CHALLENGER", challenger.getXChallenger()); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(404); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_TODOS_NOT_PLURAL_404)); - } - - @Test - public void optionsOnTodosPassesChallenge() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/todos") - .setVerb("OPTIONS") - .addHeader("X-CHALLENGER", challenger.getXChallenger()); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(204); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.OPTIONS_TODOS)); - } - - @Test - public void postSecretTokenWrongAuthorizationChallenge() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/secret/token") - .setVerb("POST") - .addHeader("Authorization", "ididntcheck") - .addHeader("X-CHALLENGER", challenger.getXChallenger()); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(401); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.CREATE_SECRET_TOKEN_401)); - } - - @Test - public void postSecretTokenCorrectAuthorizationChallenge() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/secret/token") - .setVerb("POST") - .addHeader("Authorization", "ididntcheck") - .addHeader("X-CHALLENGER", challenger.getXChallenger()); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(201); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.CREATE_SECRET_TOKEN_201)); - } - - @Test - public void getNoteWithValidXAuthToken() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/secret/note") - .setVerb("GET") - .addHeader("X-AUTH-TOKEN", "nocheck") - .addHeader("X-CHALLENGER", challenger.getXChallenger()); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(403); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_SECRET_NOTE_403)); - } - - @Test - public void getNoteWithNoAuthTokenFailChallenge() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/secret/note") - .setVerb("GET") - .addHeader("X-CHALLENGER", challenger.getXChallenger()); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(401); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_SECRET_NOTE_401)); - } - - @Test - public void cannotAmendNoteWithInvalidAuthTokenFailChallenge() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/secret/note") - .setVerb("POST") - .addHeader("X-AUTH-TOKEN", "nocheck") - .addHeader("X-CHALLENGER", challenger.getXChallenger()) - .setBody("{\"note\":\"bob\""); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(403); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_SECRET_NOTE_403)); - } - - @Test - public void forbiddenAmendNOAuthTokenForNote() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/secret/note") - .setVerb("POST") - .addHeader("X-CHALLENGER", challenger.getXChallenger()) - .setBody("{\"note\":\"bob\""); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(401); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_SECRET_NOTE_401)); - } - - @Test - public void canAmendNoteUsingAuthTokenPass() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/secret/note") - .setVerb("POST") - .addHeader("X-AUTH-TOKEN", "validtoken") - .addHeader("X-CHALLENGER", challenger.getXChallenger()) - .setBody("{\"note\":\"bob\""); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(200); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_SECRET_NOTE_200)); - } - - @Test - public void canGetNoteUsingAuthTokenPass() { - - Challengers challengers = new Challengers(null, Arrays.asList(CHALLENGE.values())); - challengers.setMultiPlayerMode(); - - ChallengerInternalHTTPResponseHook hook = - new ChallengerInternalHTTPResponseHook(challengers); - - final ChallengerAuthData challenger = challengers.createNewChallenger(); - - InternalHttpRequest request = - new InternalHttpRequest("/secret/note") - .setVerb("GET") - .addHeader("X-AUTH-TOKEN", "validtoken") - .addHeader("X-CHALLENGER", challenger.getXChallenger()); - - InternalHttpResponse response = new InternalHttpResponse().setStatus(200); - - hook.run(request, response); - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_SECRET_NOTE_200)); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengers/ChallengersTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/challengers/ChallengersTest.java deleted file mode 100644 index ad5dd986..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengers/ChallengersTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package uk.co.compendiumdev.challenge.challengers; - -import java.lang.reflect.Field; -import java.util.Arrays; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; - -public class ChallengersTest { - - @Test - void canPurgeUserData() throws NoSuchFieldException, IllegalAccessException { - - EntityRelModel erModel = new EntityRelModel(); - erModel.createInstanceDatabaseIfNotExisting("an-active-user"); - - int originalNumberOfDatabases = erModel.getDatabaseNames().size(); - - Challengers challengers = new Challengers(erModel, Arrays.asList(CHALLENGE.values())); - - challengers.setMultiPlayerMode(); - - ChallengerAuthData challenger = challengers.createNewChallenger(); - String guid = challenger.getXChallenger(); - erModel.createInstanceDatabaseIfNotExisting(guid); - - // hack last accessed time so that we can purge it - Field lastAccessedField = ChallengerAuthData.class.getDeclaredField("lastAccessed"); - lastAccessedField.setAccessible(true); - lastAccessedField.set(challenger, 0L); - Assertions.assertEquals(0, challenger.getLastAccessed()); - - Assertions.assertTrue(erModel.getDatabaseNames().contains(guid)); - - challengers.purgeOldAuthData(); - - ChallengerAuthData purgedChallenger = challengers.getChallenger(guid); - Assertions.assertNull(purgedChallenger); - - Assertions.assertEquals(originalNumberOfDatabases, erModel.getDatabaseNames().size()); - Assertions.assertTrue(erModel.getDatabaseNames().contains("an-active-user")); - Assertions.assertFalse(erModel.getDatabaseNames().contains(guid)); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutesTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutesTest.java deleted file mode 100644 index a5662e3c..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutesTest.java +++ /dev/null @@ -1,624 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.co.compendiumdev.challenge.ChallengeMain; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; - -public class AuthRoutesTest { - private static HttpMessageSender http; - private static ChallengerAuthData challenger; - - @BeforeAll - static void createHttp() { - // this uses the Environment to startup the server app to - // issue http tests and test the server routing - http = new HttpMessageSender(Environment.getBaseUri()); - challenger = ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - } - - static Stream simpleRoutingStatus() { - List args = new ArrayList<>(); - - // get - args.add(Arguments.of(401, "head", "/secret/note")); - args.add(Arguments.of(204, "options", "/secret/note")); - // post - args.add(Arguments.of(405, "put", "/secret/note")); - args.add(Arguments.of(405, "delete", "/secret/note")); - args.add(Arguments.of(405, "patch", "/secret/note")); - args.add(Arguments.of(405, "trace", "/secret/note")); - - args.add(Arguments.of(405, "get", "/secret/token")); - args.add(Arguments.of(405, "head", "/secret/token")); - args.add(Arguments.of(204, "options", "/secret/token")); - // post - args.add(Arguments.of(405, "put", "/secret/token")); - args.add(Arguments.of(405, "delete", "/secret/token")); - args.add(Arguments.of(405, "patch", "/secret/token")); - args.add(Arguments.of(405, "trace", "/secret/token")); - return args.stream(); - } - - @ParameterizedTest(name = "simple status routing expected {0} for {1} {2}") - @MethodSource("simpleRoutingStatus") - void simpleRoutingTest(int statusCode, String verb, String url) { - final HttpResponseDetails response = http.send(url, verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } - - static Stream simpleRoutingStatusForToken() { - List args = new ArrayList<>(); - - args.add(Arguments.of(405, "get", "/secret/token")); - args.add(Arguments.of(405, "head", "/secret/token")); - args.add(Arguments.of(204, "options", "/secret/token")); - // post - args.add(Arguments.of(405, "put", "/secret/token")); - args.add(Arguments.of(405, "delete", "/secret/token")); - args.add(Arguments.of(405, "patch", "/secret/token")); - args.add(Arguments.of(405, "trace", "/secret/token")); - return args.stream(); - } - - @ParameterizedTest(name = "simple status routing expected {0} for {1} {2}") - @MethodSource("simpleRoutingStatusForToken") - void simpleRoutingTestAuthenticatedForToken(int statusCode, String verb, String url) { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - // admin:password YWRtaW46cGFzc3dvcmQ= - http.setHeader("Authentication", "basic YWRtaW46cGFzc3dvcmQ="); - - final HttpResponseDetails response = http.send(url, verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } - - @Test - void noProcessingWhenNoBasicAuth() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - // admin:password YWRtaW46cGFzc3dvcmQ= - http.setHeader("Authentication", "basic " + base64("wrong:wrong")); - - final HttpResponseDetails response = http.send("/secret/token", "post"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertEquals( - "Basic realm=\"User Visible Realm\"", response.getHeader("WWW-Authenticate")); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void noProcessingWhenPassBasicAuthButNoChallenger() { - - http.clearHeaders(); - // http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - // admin:password YWRtaW46cGFzc3dvcmQ= - http.setHeader("Authorization", "basic " + base64("admin:password")); - - final HttpResponseDetails response = http.send("/secret/token", "post"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertEquals( - XChallengerHeader.NOT_FOUND_ERROR_MESSAGE, response.getHeader("X-CHALLENGER")); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - } - - @Test - void noProcessingWhenPassBasicAuthButUnknownChallenger() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", "bobobobobobobobob"); - // admin:password YWRtaW46cGFzc3dvcmQ= - http.setHeader("Authorization", "basic " + base64("admin:password")); - - final HttpResponseDetails response = http.send("/secret/token", "post"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertEquals( - XChallengerHeader.NOT_FOUND_ERROR_MESSAGE, response.getHeader("X-CHALLENGER")); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - } - - @Test - void whenChallengerIsValidThenRevealTheAuthToken() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - // admin:password YWRtaW46cGFzc3dvcmQ= - http.setHeader("Authorization", "basic " + base64("admin:password")); - - final HttpResponseDetails response = http.send("/secret/token", "post"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertEquals(challenger.getXAuthToken(), response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - static String base64(String convertMe) { - final Base64.Encoder base64 = Base64.getEncoder(); - return base64.encodeToString(convertMe.getBytes()); - } - - @Test - void cannotGetSecretNoteWhenNoAuthToken() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void cannotGetSecretNoteWhenNoValueForAuthToken() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", ""); - http.setHeader("Authorization", "basic " + base64("admin:password")); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void cannotGetSecretNoteWhenChallengerNotRecognised() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger() + "wrong"); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals( - XChallengerHeader.NOT_FOUND_ERROR_MESSAGE, response.getHeader("X-CHALLENGER")); - } - - @Test - void cannotGetSecretNoteWrongAuthTokenForChallenger() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken() + "wrong"); - http.setHeader("Authorization", "basic " + base64("admin:password")); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(403, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void canGetSecretNoteDefaultJSON() { - - final ChallengerAuthData aNewChallenger = - ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", aNewChallenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", aNewChallenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals( - aNewChallenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - // {"note":""} - Assertions.assertEquals("{\"note\":\"\"}", response.body); - } - - @Test - void canGetSecretNoteAsXML() { - - final ChallengerAuthData aNewChallenger = - ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", aNewChallenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", aNewChallenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Accept", "application/xml"); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/xml", response.getHeader("Content-Type")); - Assertions.assertEquals( - aNewChallenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - // {"note":""} - Assertions.assertEquals("", response.body); - } - - @Test - void cannoteGetSecretNoteWrongTypeRequested() { - - final ChallengerAuthData aNewChallenger = - ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", aNewChallenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", aNewChallenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Accept", "bob"); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(406, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals( - aNewChallenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - // {"note":""} - Assertions.assertEquals("", response.body); - } - - @Test - void cannotPostSecretNoteWhenNoAuthToken() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/json"); - - final HttpResponseDetails response = http.send("/secret/note", "post"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void cannotPostSecretNoteWhenNoValueForAuthToken() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", ""); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/json"); - - final HttpResponseDetails response = http.send("/secret/note", "post"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void cannotPostSecretNoteWhenChallengerNotRecognised() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger() + "wrong"); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/json"); - - final HttpResponseDetails response = http.send("/secret/note", "post"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals( - XChallengerHeader.NOT_FOUND_ERROR_MESSAGE, response.getHeader("X-CHALLENGER")); - } - - @Test - void cannotPostSecretNoteWrongAuthTokenForChallenger() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken() + "wrong"); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/json"); - - final HttpResponseDetails response = http.send("/secret/note", "post"); - - Assertions.assertEquals(403, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void canPostSecretNoteUsingJSON() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/json"); - - // {"note":"hello"} - final HttpResponseDetails response = http.post("/secret/note", "{\"note\":\"hello\"}"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - - Assertions.assertEquals("{\"note\":\"hello\"}", response.body); - - // and get to check - - final HttpResponseDetails getresponse = http.get("/secret/note"); - Assertions.assertEquals("{\"note\":\"hello\"}", getresponse.body); - } - - @Test - void canPostSecretNoteUsingXML() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/xml"); - http.setHeader("Accept", "application/xml"); - - // {"note":"hello"} - final HttpResponseDetails response = - http.post("/secret/note", "hello"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - Assertions.assertEquals("application/xml", response.getHeader("Content-Type")); - - Assertions.assertEquals("hello", response.body); - - // and get to check - - final HttpResponseDetails getresponse = http.get("/secret/note"); - Assertions.assertEquals("hello", getresponse.body); - } - - @Test - void cannotPostSecretNoteIfMalformedJson() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/json"); - - // {"note":"hello"} - final HttpResponseDetails response = http.post("/secret/note", "{\"note\":\"hello\""); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - - Assertions.assertTrue(response.body.contains("errorMessages")); - } - - @Test - void cannotPostSecretNoteIfNoNote() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/json"); - - final HttpResponseDetails response = http.post("/secret/note", "{}"); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - } - - @Test - void cannotPostSecretNoteIfNoBody() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/json"); - - final HttpResponseDetails response = http.post("/secret/note", ""); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - } - - @Test - void cannotPostSecretNoteIfBodyIsNotNote() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "basic " + base64("admin:password")); - http.setHeader("Content-Type", "application/json"); - - // {"name":"bob"} - final HttpResponseDetails response = http.post("/secret/note", "{\"name\":\"bob\"}"); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - } - - // can get secret note if we have a valid bearer token - @Test - void canGetSecretNoteIfValidBearerToken() { - - final ChallengerAuthData aNewChallenger = - ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", aNewChallenger.getXChallenger()); - // http.setHeader("X-AUTH-TOKEN", aNewChallenger.getXAuthToken()); - http.setHeader("Authorization", "bearer " + aNewChallenger.getXAuthToken()); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals( - aNewChallenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - // {"note":""} - Assertions.assertEquals("{\"note\":\"\"}", response.body); - } - - // can not get secret note if given bearer token is not valid - @Test - void canNotGetSecretNoteWhenInValidBearerToken() { - - final ChallengerAuthData aNewChallenger = - ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", aNewChallenger.getXChallenger()); - // http.setHeader("X-AUTH-TOKEN", aNewChallenger.getXAuthToken()); - http.setHeader("Authorization", "bearer not" + aNewChallenger.getXAuthToken()); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(403, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals( - aNewChallenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - // get secret note bearer token takes precedence over x-auth-token - @Test - void getSecretNoteTakesPrecendenceOverAuthToken() { - - final ChallengerAuthData aNewChallenger = - ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", aNewChallenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", aNewChallenger.getXAuthToken() + "bob"); - http.setHeader("Authorization", "bearer " + aNewChallenger.getXAuthToken()); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals( - aNewChallenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - // {"note":""} - Assertions.assertEquals("{\"note\":\"\"}", response.body); - } - - // get secret note bearer token takes precedence over x-auth-token even if it is invalid - @Test - void getSecretNoteBearerTakesPrecendenceOverAuthTokenEvenIfInvalid() { - - final ChallengerAuthData aNewChallenger = - ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", aNewChallenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", aNewChallenger.getXAuthToken()); - http.setHeader("Authorization", "bearer not" + aNewChallenger.getXAuthToken()); - - final HttpResponseDetails response = http.send("/secret/note", "get"); - - Assertions.assertEquals(403, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals( - aNewChallenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void canPostSecretNoteUsingBearerAuth() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - // http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "bearer " + challenger.getXAuthToken()); - http.setHeader("Content-Type", "application/json"); - - // {"note":"hello"} - final HttpResponseDetails response = http.post("/secret/note", "{\"note\":\"hello\"}"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - - Assertions.assertEquals("{\"note\":\"hello\"}", response.body); - - // and get to check - - final HttpResponseDetails getresponse = http.get("/secret/note"); - Assertions.assertEquals("{\"note\":\"hello\"}", getresponse.body); - } - - @Test - void canPostSecretNoteUsingBearerAuthTakingPrecedence() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken() + "bob"); - http.setHeader("Authorization", "bearer " + challenger.getXAuthToken()); - http.setHeader("Content-Type", "application/json"); - - // {"note":"hello"} - final HttpResponseDetails response = http.post("/secret/note", "{\"note\":\"hello\"}"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - - Assertions.assertEquals("{\"note\":\"hello\"}", response.body); - - // and get to check - - final HttpResponseDetails getresponse = http.get("/secret/note"); - Assertions.assertEquals("{\"note\":\"hello\"}", getresponse.body); - } - - @Test - void cannotPostSecretNoteUsingIncorrectBearerAuthTakingPrecedence() { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - http.setHeader("X-AUTH-TOKEN", challenger.getXAuthToken()); - http.setHeader("Authorization", "bearer not" + challenger.getXAuthToken()); - http.setHeader("Content-Type", "application/json"); - - // {"note":"hello"} - final HttpResponseDetails response = http.post("/secret/note", "{\"note\":\"hello\"}"); - - Assertions.assertEquals(403, response.statusCode); - Assertions.assertNull(response.getHeader("X-AUTH-TOKEN")); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals("", response.body); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerIpAddressTrackerTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerIpAddressTrackerTest.java deleted file mode 100644 index c0426add..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerIpAddressTrackerTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import java.util.HashSet; -import java.util.UUID; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class ChallengerIpAddressTrackerTest { - - @Test - public void canPurgeOldIps() { - - ChallengerIpAddressTracker tracker = new ChallengerIpAddressTracker(2, true); - - tracker.trackAgainstThisIp("123.123.123.123", UUID.randomUUID().toString()); - tracker.trackAgainstThisIp("123.123.123.123", UUID.randomUUID().toString()); - tracker.trackAgainstThisIp("123.123.123.124", UUID.randomUUID().toString()); - tracker.trackAgainstThisIp("123.123.123.124", UUID.randomUUID().toString()); - - tracker.purgeEmptyIpAddresses(new HashSet<>()); - - Assertions.assertFalse(tracker.isTrackingIp("123.123.123.123")); - Assertions.assertFalse(tracker.isTrackingIp("123.123.123.124")); - } - - @Test - public void canPurgeOldChallengers() { - - ChallengerIpAddressTracker tracker = new ChallengerIpAddressTracker(2, true); - - String keepChallenger = UUID.randomUUID().toString(); - tracker.trackAgainstThisIp("123.123.123.123", keepChallenger); - tracker.trackAgainstThisIp("123.123.123.123", UUID.randomUUID().toString()); - - HashSet challengers = new HashSet<>(); - challengers.add(keepChallenger); - - tracker.purgeEmptyIpAddresses(challengers); - - Assertions.assertTrue(tracker.isTrackingIp("123.123.123.123")); - Assertions.assertEquals(1, tracker.countFor("123.123.123.123")); - } - - @Test - public void willOnlyTrackACertainNumber() { - - ChallengerIpAddressTracker tracker = new ChallengerIpAddressTracker(2, true); - - tracker.trackAgainstThisIp("123.123.123.123", UUID.randomUUID().toString()); - tracker.trackAgainstThisIp("123.123.123.123", UUID.randomUUID().toString()); - boolean added = tracker.trackAgainstThisIp("123.123.123.123", UUID.randomUUID().toString()); - - Assertions.assertTrue(tracker.isTrackingIp("123.123.123.123")); - Assertions.assertEquals(2, tracker.countFor("123.123.123.123")); - Assertions.assertFalse(added); - } - - @Test - public void canSwitchOffIPAddressTracking() { - - ChallengerIpAddressTracker tracker = new ChallengerIpAddressTracker(2, false); - - tracker.trackAgainstThisIp("123.123.123.123", UUID.randomUUID().toString()); - tracker.trackAgainstThisIp("123.123.123.123", UUID.randomUUID().toString()); - boolean added = tracker.trackAgainstThisIp("123.123.123.123", UUID.randomUUID().toString()); - - HashSet challengers = new HashSet<>(); - challengers.add(UUID.randomUUID().toString()); - challengers.add(UUID.randomUUID().toString()); - tracker.purgeEmptyIpAddresses(challengers); - - Assertions.assertFalse(tracker.isTrackingIp("123.123.123.123")); - Assertions.assertEquals(0, tracker.countFor("123.123.123.123")); - Assertions.assertFalse(added); - Assertions.assertFalse(tracker.hasLimitBeenReachedFor("123.123.123.123")); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutesTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutesTest.java deleted file mode 100644 index c1bfe3a4..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutesTest.java +++ /dev/null @@ -1,215 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import java.util.*; -import java.util.stream.Stream; -import org.junit.jupiter.api.*; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.co.compendiumdev.challenge.ChallengeMain; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; - -public class ChallengerTrackingRoutesTest { - - private static HttpMessageSender http; - private static ChallengerAuthData challenger; - - @BeforeAll - static void createHttp() { - Environment.stop(); - - // this uses the Environment to startup the server app to - // issue http tests and test the server routing - // test this in multi user mode - http = new HttpMessageSender(Environment.getBaseUri(false)); - // challenger = Environment.getNewChallenger(); - challenger = ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - } - - @AfterAll - static void tearDownEnv() { - Environment.stop(); - } - - @Test - void canGetAnExistingChallenger() { - http.clearHeaders(); - - final HttpResponseDetails response = - http.send("/challenger/" + challenger.getXChallenger(), "get"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void challengerHeaderShouldBeIgnored() { - http.clearHeaders(); - final ChallengerAuthData newchallenger = - ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - http.setHeader("X-CHALLENGER", newchallenger.getXChallenger()); - - final HttpResponseDetails response = - http.send("/challenger/" + challenger.getXChallenger(), "get"); - - Assertions.assertEquals(200, response.statusCode); // now returns the status details - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void will404WhenGivenChallengerNotExists() { - http.clearHeaders(); - - final HttpResponseDetails response = http.send("/challenger/" + "bob", "get"); - - Assertions.assertEquals(404, response.statusCode); - Assertions.assertEquals( - XChallengerHeader.NOT_FOUND_ERROR_MESSAGE, response.getHeader("X-CHALLENGER")); - } - - @Test - void will404WhenNoGivenChallenger() { - http.clearHeaders(); - - final HttpResponseDetails response = http.send("/challenger/", "get"); - - Assertions.assertEquals(404, response.statusCode); - Assertions.assertEquals( - XChallengerHeader.NOT_FOUND_ERROR_MESSAGE, response.getHeader("X-CHALLENGER")); - } - - @Test - void postWithChallengerHeaderShouldReturnExistingChallengerIfAvail() { - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - - final HttpResponseDetails response = http.send("/challenger", "post"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - } - - @Test - void postWithChallengerHeaderShouldReturn404IfNotAvail() { - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger() + "bob"); - - final HttpResponseDetails response = http.send("/challenger", "post"); - - Assertions.assertEquals(404, response.statusCode); - Assertions.assertEquals( - XChallengerHeader.NOT_FOUND_ERROR_MESSAGE, response.getHeader("X-CHALLENGER")); - } - - @Test - void postWithoutChallengerHeaderShouldCreateChallenger() { - http.clearHeaders(); - - final HttpResponseDetails response = http.send("/challenger", "post"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertNotEquals( - XChallengerHeader.NOT_FOUND_ERROR_MESSAGE, response.getHeader("X-CHALLENGER")); - - final String guid = response.getHeader("X-CHALLENGER"); - - Assertions.assertNotNull( - ChallengeMain.getChallenger().getChallengers().getChallenger(guid)); - } - - static Stream simpleRoutingStatus() { - List args = new ArrayList<>(); - - args.add(Arguments.of(405, "head", "/challenger")); - args.add(Arguments.of(204, "options", "/challenger")); - args.add(Arguments.of(405, "put", "/challenger")); - args.add(Arguments.of(405, "delete", "/challenger")); - args.add(Arguments.of(405, "patch", "/challenger")); - args.add(Arguments.of(405, "trace", "/challenger")); - return args.stream(); - } - - @ParameterizedTest( - name = "simple status routing expected {0} for {1} {2} with challenger as header") - @MethodSource("simpleRoutingStatus") - void simpleRoutingTestChallengerRouting(int statusCode, String verb, String url) { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - - final HttpResponseDetails response = http.send(url, verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } - - static Stream simpleRoutingStatusSpecific() { - List args = new ArrayList<>(); - - args.add(Arguments.of(200, "head", "/challenger")); - args.add(Arguments.of(204, "options", "/challenger")); - args.add(Arguments.of(400, "put", "/challenger")); // because we don't pass in the details - args.add(Arguments.of(405, "post", "/challenger")); - args.add(Arguments.of(405, "delete", "/challenger")); - args.add(Arguments.of(405, "patch", "/challenger")); - args.add(Arguments.of(405, "trace", "/challenger")); - return args.stream(); - } - - @ParameterizedTest( - name = "simple status routing expected for specific challenger {0} for {1} {2}/id") - @MethodSource("simpleRoutingStatusSpecific") - void simpleRoutingTestSpecificChallengerRouting(int statusCode, String verb, String url) { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send(url + "/" + challenger.getXChallenger(), verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } - - // todo: allow configuration of the ip address limiting to enable this test - @Disabled("ip address limiting it not enabled") - @Test - void canOnlyCreateACertainNumberOfChallengersPerIp() { - http.clearHeaders(); - - // invalidate any existing challengers by making them out of date - Challengers challengers = ChallengeMain.getChallenger().getChallengers(); - Set challengersGuids = challengers.getChallengerGuids(); - for (String aGuid : challengersGuids) { - ChallengerAuthData challenger = challengers.getChallenger(aGuid); - challenger.setAsExpired(); - } - - ChallengerAuthData challengerToResend = - ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - challengerToResend.touch(); - - int challengersToCreate = 110; // 9 of these will be rejected - HttpResponseDetails response = null; - - Map headers = new HashMap<>(); - headers.put("Content-type", "application/json"); - - while (challengersToCreate > 0) { - challengerToResend.setXChallengerGUID(UUID.randomUUID().toString()); - response = - http.send( - "/challenger/" + challengerToResend.getXChallenger(), - "put", - headers, - challengerToResend.asJson()); - challengersToCreate--; - } - - Assertions.assertEquals(429, response.statusCode); - Assertions.assertEquals( - 101, ChallengeMain.getChallenger().getChallengers().getChallengerGuids().size()); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutesTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutesTest.java deleted file mode 100644 index 0cc20da5..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutesTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengeMain; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.ChallengerConfig; -import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitions; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; - -public class ChallengesRoutesTest { - - private static HttpMessageSender http; - private static ChallengerAuthData challenger; - - @BeforeAll - static void createHttp() { - // this uses the Environment to startup the server app to - // issue http tests and test the server routing - http = new HttpMessageSender(Environment.getBaseUri()); - ChallengerConfig config = new ChallengerConfig(); - config.setToNoPersistenceMode(); - Collection challenges = new ChallengeDefinitions(config).getDefinedChallenges(); - Assertions.assertFalse(challenges.isEmpty()); - challenger = ChallengeMain.getChallenger().getChallengers().createNewChallenger(); - } - - @Test - void canChallengesForAnExistingChallenger() { - http.clearHeaders(); - - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - final HttpResponseDetails response = http.send("/challenges", "get"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals(challenger.getXChallenger(), response.getHeader("X-CHALLENGER")); - - Assertions.assertEquals( - "/gui/challenges/" + challenger.getXChallenger(), response.getHeader("Location")); - - Assertions.assertNotNull(response.body); - Assertions.assertTrue(response.body.length() > 200); - } - - @Test - void canOptionsChallenges() { - http.clearHeaders(); - - // http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - final HttpResponseDetails response = http.send("/challenges", "options"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("GET, HEAD, OPTIONS", response.getHeader("Allow")); - - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - } - - static Stream simpleRoutingStatus() { - List args = new ArrayList<>(); - - args.add(Arguments.of(200, "head", "/challenges")); - args.add(Arguments.of(200, "options", "/challenges")); - args.add(Arguments.of(405, "put", "/challenges")); - args.add(Arguments.of(405, "post", "/challenges")); - args.add(Arguments.of(405, "delete", "/challenges")); - args.add(Arguments.of(405, "patch", "/challenges")); - args.add(Arguments.of(405, "trace", "/challenges")); - return args.stream(); - } - - @ParameterizedTest(name = "simple status routing expected {0} for {1} {2}") - @MethodSource("simpleRoutingStatus") - void simpleRoutingTestChallengesRouting(int statusCode, String verb, String url) { - - http.clearHeaders(); - http.setHeader("X-CHALLENGER", challenger.getXChallenger()); - - final HttpResponseDetails response = http.send(url, verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutesTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutesTest.java deleted file mode 100644 index d79bd755..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutesTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package uk.co.compendiumdev.challenge.challengesrouting; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; - -public class HeartBeatRoutesTest { - private static HttpMessageSender http; - - @BeforeAll - static void createHttp() { - // this uses the Environment to startup the server app to - // issue http tests and test the server routing - http = new HttpMessageSender(Environment.getBaseUri()); - } - - @Test - public void optionsOnHeartbeat() { - - final HttpResponseDetails response = http.send("/heartbeat", "OPTIONS"); - - Assertions.assertEquals(204, response.statusCode); - Assertions.assertEquals("GET, HEAD, OPTIONS", response.getHeader("Allow")); - } - - static Stream simpleRoutingStatus() { - List args = new ArrayList<>(); - - args.add(Arguments.of(204, "get")); - args.add(Arguments.of(204, "head")); - args.add(Arguments.of(405, "post")); - args.add(Arguments.of(405, "delete")); - args.add(Arguments.of(405, "put")); - args.add(Arguments.of(500, "patch")); - args.add(Arguments.of(501, "trace")); - return args.stream(); - } - - @ParameterizedTest(name = "simple status routing expected {0} for {1}") - @MethodSource("simpleRoutingStatus") - void simpleRoutingTest(int statusCode, String verb) { - final HttpResponseDetails response = http.send("/heartbeat", verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/gui/MarkdownContentManagerDateMetadataTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/gui/MarkdownContentManagerDateMetadataTest.java deleted file mode 100644 index 139edced..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/gui/MarkdownContentManagerDateMetadataTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package uk.co.compendiumdev.challenge.gui; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class MarkdownContentManagerDateMetadataTest { - - @Test - void resolveDateModifiedPrefersLastmodWhenPresent() { - final String resolved = - MarkdownContentManager.resolveDateModified("2026-02-18", "2021-01-01T09:00:00Z"); - Assertions.assertEquals("2026-02-18", resolved); - } - - @Test - void resolveDateModifiedFallsBackToDateWhenLastmodMissing() { - final String resolved = - MarkdownContentManager.resolveDateModified("", "2021-01-01T09:00:00Z"); - Assertions.assertEquals("2021-01-01T09:00:00Z", resolved); - } - - @Test - void resolveDateModifiedReturnsEmptyWhenBothValuesMissing() { - final String resolved = MarkdownContentManager.resolveDateModified("", ""); - Assertions.assertEquals("", resolved); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutesRepositoryTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutesRepositoryTest.java deleted file mode 100644 index b9856ce9..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutesRepositoryTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package uk.co.compendiumdev.challenge.practicemodes.simulation; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.challenge.ChallengerConfig; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; -import uk.co.compendiumdev.thingifier.core.repository.sqlite.SqliteThingStore; -import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; - -public class SimulationRoutesRepositoryTest { - - @Test - void simulationRoutesUseConfiguredSqliteMemoryRepository() { - ChallengerConfig config = new ChallengerConfig(); - config.setSimulationRepositoryFromArgs(new String[] {"-sim-repository=sqlite-memory"}); - - SimulationRoutes routes = - new SimulationRoutes(new DefaultGUIHTML(), config.getSimulationRepositoryConfig()); - - try { - routes.setUpRepositoryBackedData(); - - ThingStore repository = - routes.simulation.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - Assertions.assertTrue(repository instanceof SqliteThingStore); - Assertions.assertEquals(11, repository.entityQueries().count(routes.entityDefn)); - } finally { - routes.close(); - } - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/ChallengeCompleteTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/ChallengeCompleteTest.java deleted file mode 100644 index 4f1ec1af..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/ChallengeCompleteTest.java +++ /dev/null @@ -1,1358 +0,0 @@ -package uk.co.compendiumdev.challenger.http.completechallenges; - -import com.google.gson.Gson; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengeMain; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.challenger.payloads.Todo; -import uk.co.compendiumdev.challenger.payloads.Todos; -import uk.co.compendiumdev.serverstart.Environment; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -public abstract class ChallengeCompleteTest { - - static Logger logger = LoggerFactory.getLogger(ChallengeCompleteTest.class); - - public static boolean isEnvironmentSet = false; - public static int challengesOffset = 0; - public static Challengers challengers; - public static ChallengerAuthData challenger; - public static HttpMessageSender http; - - private static ChallengerAuthData newChallenger; - - public Map getXChallengerHeader(String guid) { - Map xchallenger_header = new HashMap<>(); - xchallenger_header.put("X-CHALLENGER", guid); - return xchallenger_header; - } - - @BeforeAll - public static void controlEnvStart() { - Environment.stop(); - resetEnvironmentState(); - } - - @AfterAll - public static void controlEnvStop() { - Environment.stop(); - resetEnvironmentState(); - } - - private static void resetEnvironmentState() { - isEnvironmentSet = false; - challengers = null; - challenger = null; - http = null; - newChallenger = null; - } - - abstract boolean getIsSinglePlayerMode(); - - abstract int getNumberOfChallengesToFail(); - - public void createEnvironmentAndChallengerIfNecessary() { - - challengesOffset = getNumberOfChallengesToFail(); - - if (!isEnvironmentSet) { - Environment.getBaseUri(getIsSinglePlayerMode(), true); - isEnvironmentSet = true; - } - - if (challengers == null) { - challengers = ChallengeMain.getChallenger().getChallengers(); - } - - if (this.challenger == null) { - if (getIsSinglePlayerMode()) { - challenger = challengers.SINGLE_PLAYER; - } else { - challenger = challengers.createNewChallenger(); - } - } - - if (http == null) { - http = new HttpMessageSender(Environment.getBaseUri(getIsSinglePlayerMode(), true)); - } - } - - // After all - check that all challenges are complete - @AfterAll - static void alldone() { - - int remainingChallengeCount = 0; - - // only compare with defined challenges, not all challenges - for (CHALLENGE challenge : - ChallengeMain.getChallenger().getChallengers().getDefinedChallenges()) { - if (newChallenger != null && challenge == CHALLENGE.CREATE_NEW_CHALLENGER) { - if (newChallenger.statusOfChallenge(challenge)) { - continue; - } - } - if (!challenger.statusOfChallenge(challenge)) { - remainingChallengeCount++; - logger.warn("Still to complete challenge " + challenge.toString()); - } - } - - logger.error("{} challenges left to complete", remainingChallengeCount); - - if (remainingChallengeCount - challengesOffset > 0) { - Assertions.fail( - String.format("%d challenges left to complete", remainingChallengeCount)); - } - - Environment.stop(); - isEnvironmentSet = false; - challengers = null; - challenger = null; - http = null; - - newChallenger = null; - } - - @BeforeEach - public void setup() { - - createEnvironmentAndChallengerIfNecessary(); - } - - @Test - public void canGetChallengesPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send("/challenges", "GET", x_challenger_header, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_CHALLENGES)); - } - - @Test - public void canGetTodosPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = http.send("/todos", "GET", x_challenger_header, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_TODOS)); - } - - @Test - public void canGetTodosNoAcceptHeaderPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Accept", ""); - - final HttpResponseDetails response = http.send("/todos", "GET", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.GET_JSON_BY_DEFAULT_NO_ACCEPT)); - } - - @Test - public void canGet404TodoPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = http.send("/todo", "GET", x_challenger_header, ""); - - Assertions.assertEquals(404, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_TODOS_NOT_PLURAL_404)); - } - - @Test - public void canHeadTodosPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = http.send("/todos", "HEAD", x_challenger_header, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_HEAD_TODOS)); - } - - @Test - public void canOptionsTodosPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send("/todos", "OPTIONS", x_challenger_header, ""); - - // initial challenge had options as 200, so we have a hack to keep that - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.OPTIONS_TODOS)); - } - - /** CREATE TODOS */ - @Test - public void canPostTodosPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"mytodo\",\"description\":\"a todo\",\"doneStatus\":false}"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_TODOS)); - } - - @Test - public void canPutTodos400FailCreatePass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - // try to create a to do but fail because the AUTO fields mean we can't control the id - final HttpResponseDetails response = - http.send( - "/todos/200", - "PUT", - headers, - "{\"id\":200, \"title\":\"mytodo\",\"description\":\"a todo\",\"doneStatus\":false}"); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.PUT_TODOS_400)); - } - - @Test - public void canPutTodosFull200AmendPass() { - - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - ThingStore repository = - ChallengeMain.getChallenger().getThingifier().getStore(challenger.getXChallenger()); - EntityInstance aTodo = new ArrayList<>(repository.entityQueries().list(todos)).get(0); - - // amend a to do successfully - final HttpResponseDetails response = - http.send( - "/todos/" + aTodo.getPrimaryKeyValue(), - "PUT", - headers, - "{\"title\":\"my put todo\",\"description\":\"a put description\",\"doneStatus\":true}"); - // complete payload to avoid defaults - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.PUT_TODOS_FULL_200)); - } - - @Test - public void canPutTodosPartial200AmendPass() { - - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - ThingStore repository = - ChallengeMain.getChallenger().getThingifier().getStore(challenger.getXChallenger()); - EntityInstance aTodo = new ArrayList<>(repository.entityQueries().list(todos)).get(0); - - // amend a to do successfully - final HttpResponseDetails response = - http.send( - "/todos/" + aTodo.getPrimaryKeyValue(), - "PUT", - headers, - "{\"title\":\"my put todo\"}"); - // only title is mandatory the rest would be set to defaults - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.PUT_TODOS_PARTIAL_200)); - } - - @Test - public void canPutTodos200MissingTitleAmendPass() { - - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - ThingStore repository = - ChallengeMain.getChallenger().getThingifier().getStore(challenger.getXChallenger()); - EntityInstance aTodo = new ArrayList<>(repository.entityQueries().list(todos)).get(0); - - // amend a to do unsuccessfully - final HttpResponseDetails response = - http.send( - "/todos/" + aTodo.getPrimaryKeyValue(), - "PUT", - headers, - "{\"description\":\"my description\"}"); - // title is mandatory so this will fail - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.PUT_TODOS_MISSING_TITLE_400)); - } - - @Test - public void canNotPutTodos400ChangeIdAmendPass() { - - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - ThingStore repository = - ChallengeMain.getChallenger().getThingifier().getStore(challenger.getXChallenger()); - EntityInstance aTodo = new ArrayList<>(repository.entityQueries().list(todos)).get(0); - - // amend a to do unsuccessfully - final HttpResponseDetails response = - http.send( - "/todos/" + aTodo.getPrimaryKeyValue(), - "PUT", - headers, - String.format( - "{\"id\":%d, \"description\":\"my description\"}", - Integer.parseInt(aTodo.getPrimaryKeyValue() + 1))); - // title is mandatory so this will fail - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.PUT_TODOS_400_NO_AMEND_ID)); - } - - @Test - public void canPostTodos404Pass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - // try to create a to do item but fail because the AUTO fields mean we can't control the id - final HttpResponseDetails response = - http.send( - "/todos/2004567", - "POST", - headers, - "{\"id\":2004567, \"title\":\"mytodo\",\"description\":\"a todo\",\"doneStatus\":false}"); - - Assertions.assertEquals(404, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_TODOS_404)); - } - - @Test - public void canPostTodosFailValidationPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"mytodo\",\"description\":\"a todo\",\"doneStatus\":\"bob\"}"); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_TODOS_BAD_DONE_STATUS)); - } - - @Test - public void canPostTodosFailTitleLenValidationPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - // TODO: should send back multiple error messages to allow all validations to fail in one - // request e.g. todo status fails validation before title - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"*3*5*7*9*12*15*18*21*24*27*30*33*36*39*42*45*48*51*\",\"description\":\"a todo\"}"); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertTrue( - response.body.contains( - "Failed Validation: Maximum allowable length exceeded for title - maximum allowed is 50")); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.POST_TODOS_TOO_LONG_TITLE_LENGTH)); - } - - @Test - public void canPostTodosFailDescriptionLenValidationPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - String twoHundredAndOneChars = - "*3*5*7*10*13*16*19*22*25*28*31*34*37*40*43*46*49*52*55*58*61*" - + "64*67*70*73*76*79*82*85*88*91*94*97*101*105*109*113*117*121*" - + "125*129*133*137*141*145*149*153*157*161*165*169*173*177*181*185*189*193*197*201*"; - - // TODO: should send back multiple error messages to allow all validations to fail in one - // request e.g. todo status fails validation before title - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"*3*5*7*9*12*15*18*21*24*27*30*33*36*39*42*45*48*50\",\"description\":\"" - + twoHundredAndOneChars - + "\"}"); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertTrue( - response.body.contains( - "Failed Validation: Maximum allowable length exceeded for description - maximum allowed is 200")); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.POST_TODOS_TOO_LONG_DESCRIPTION_LENGTH)); - } - - @Test - public void canPostTodosWithMaxTitleAndDescriptionLengths() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - // TODO: should send back multiple error messages to allow all validations to fail in one - // request e.g. todo status fails validation before title - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"" - + stringOfLength(50) - + "\",\"description\":\"" - + stringOfLength(200) - + "\"}"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.POST_MAX_OUT_TITLE_DESCRIPTION_LENGTH)); - } - - @Test - public void canPostTodosFailPayloadLenValidationPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - String fiveThousandChars = stringOfLength(5000); - - // TODO: should send back multiple error messages to allow all validations to fail in one - // request e.g. todo status fails validation before title - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"title\",\"description\":\"" + fiveThousandChars + "\"}"); - - Assertions.assertEquals(413, response.statusCode); - Assertions.assertTrue( - response.body.contains("request body too large, max allowed is 5000 bytes")); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.POST_TODOS_TOO_LONG_PAYLOAD_SIZE)); - } - - @Test - public void canPostTodosFailExtraField() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - // TODO: should send back multiple error messages to allow all validations to fail in one - // request e.g. todo status fails validation before title - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"title\",\"description\":\"description\", \"priority\": \"urgent\"}"); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertTrue(response.body.contains("Could not find field: priority")); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.POST_TODOS_INVALID_EXTRA_FIELD)); - } - - private String stringOfLength(final int desiredLength) { - String ofLength = ""; - while (ofLength.length() < desiredLength) { - ofLength = ofLength + "a"; - } - - return ofLength; - } - - private EntityInstance createTodo(final EntityDefinition todos, final String title) { - return createTodo(todos, title, "false"); - } - - private EntityInstance createTodo( - final EntityDefinition todos, final String title, final String doneStatus) { - return ChallengeMain.getChallenger() - .getThingifier() - .getStore(challenger.getXChallenger()) - .entities() - .create( - EntityInstanceDraft.forEntity(todos) - .withField("title", title) - .withField("doneStatus", doneStatus)); - } - - @Test - public void canPostTodosUpdatePass() { - - ensureAtMostXTodoAvailable(10); - - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - final EntityInstance todo = createTodo(todos, "post update fixture"); - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - final HttpResponseDetails response = - http.send( - "/todos/" + todo.getFieldValue("id").asString(), - "POST", - headers, - "{\"title\":\"mytodo\",\"description\":\"a todo\"}"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_UPDATE_TODO)); - } - - @Test - public void canPostTodosAsJsonAndAcceptXmlPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/xml"); - - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"mytodo\",\"description\":\"a todo\",\"doneStatus\":false}"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_CREATE_JSON_ACCEPT_XML)); - } - - @Test - public void canPostTodosAsJsonAndAcceptJSONPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/json"); - - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"mytodo\",\"description\":\"a todo\",\"doneStatus\":false}"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_CREATE_JSON)); - } - - @Test - public void canPostTodosAsXmlAndAcceptJsonPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/xml"); - headers.put("Accept", "application/json"); - - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "mytodoa todofalse"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_CREATE_XML_ACCEPT_JSON)); - } - - @Test - public void canPostTodosAsXml() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/xml"); - headers.put("Accept", "application/xml"); - - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "mytodoa todofalse"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_CREATE_XML)); - } - - @Test - public void canPostTodosWithInvalidContentType() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-type", "application/x-www-form-urlencoded"); - - final HttpResponseDetails response = - http.send( - "/todos", - "POST", - headers, - "mytodoa todofalse"); - - Assertions.assertEquals(415, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_TODOS_415)); - } - - @Test - public void canPostToAddMaxTodosPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - HttpResponseDetails response = null; - - int maxTodos = 20; - // will create 21 which will max out - for (int todoCount = 0; todoCount <= maxTodos; todoCount++) { - response = - http.send( - "/todos", - "POST", - headers, - "{\"title\":\"mytodo\",\"description\":\"a todo\",\"doneStatus\":false}"); - } - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertTrue( - response.body.contains("ERROR: Cannot add instance, maximum limit of 20 reached")); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_ALL_TODOS)); - } - - public void ensureAtMostXTodoAvailable(int x) { - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - ThingStore repository = - ChallengeMain.getChallenger().getThingifier().getStore(challenger.getXChallenger()); - if (repository.entityQueries().count(todos) > x) { - for (int delCount = repository.entityQueries().count(todos) - x; - delCount > 0; - delCount--) { - repository - .entities() - .delete( - (EntityInstance) - (repository.entityQueries().list(todos).toArray()[0])); - } - } - } - - @Test - public void canDeleteTodosPass() { - - ensureAtMostXTodoAvailable(10); - - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - final EntityInstance todo = createTodo(todos, "delete fixture"); - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - - final HttpResponseDetails response = - http.send("/todos/" + todo.getFieldValue("id").asString(), "DELETE", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.DELETE_A_TODO)); - } - - @Test - public void canGetSpecificTodoPass() { - - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - final EntityInstance todo = createTodo(todos, "specific todo fixture"); - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send( - "/todos/" + todo.getFieldValue("id").asString(), - "GET", - x_challenger_header, - ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_TODO)); - } - - @Test - public void canGetTodosXMLPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Accept", "application/xml"); - - final HttpResponseDetails response = http.send("/todos", "GET", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_ACCEPT_XML)); - } - - @Test - public void canGetTodoJsonPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Accept", "application/json"); - - final HttpResponseDetails response = http.send("/todos", "GET", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_ACCEPT_JSON)); - } - - @Test - public void canGetTodoAnyPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Accept", "*/*"); - - final HttpResponseDetails response = http.send("/todos", "GET", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_ACCEPT_ANY_DEFAULT_JSON)); - } - - @Test - public void canGetTodoXmlPreferredPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Accept", "application/xml, application/json"); - - final HttpResponseDetails response = http.send("/todos", "GET", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_ACCEPT_XML_PREFERRED)); - } - - @Test - public void canGetTodosNotAcceptGzip() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Accept", "application/gzip"); - - final HttpResponseDetails response = http.send("/todos", "GET", headers, ""); - - Assertions.assertEquals(406, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_UNSUPPORTED_ACCEPT_406)); - } - - @Test - public void todo404pass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send("/todos/guiddoesnotexist", "GET", x_challenger_header, ""); - - Assertions.assertEquals(404, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_TODO_404)); - } - - @Test - public void canFilterTodoPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - - createTodo(todos, "done filter fixture", "true"); - createTodo(todos, "not done filter fixture", "false"); - - final HttpResponseDetails response = - http.send("/todos?doneStatus=true", "GET", x_challenger_header, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_TODOS_FILTERED)); - } - - /** Heartbeat */ - @Test - public void can405DeleteHeartbeatPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send("/heartbeat", "DELETE", x_challenger_header, ""); - - Assertions.assertEquals(405, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.DELETE_HEARTBEAT_405)); - } - - @Test - public void can500PatchHeartbeatPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send("/heartbeat", "PATCH", x_challenger_header, ""); - - Assertions.assertEquals(500, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.PATCH_HEARTBEAT_500)); - } - - @Test - public void can501TraceHeartbeatPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send("/heartbeat", "TRACE", x_challenger_header, ""); - - Assertions.assertEquals(501, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.TRACE_HEARTBEAT_501)); - } - - @Test - public void canGetHeartbeatPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send("/heartbeat", "GET", x_challenger_header, ""); - - Assertions.assertEquals(204, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_HEARTBEAT_204)); - } - - /* - SECRET TOKEN - */ - - @Test - public void canCreateSecretToken401() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Authorization", "basic YWRtaW46YWRtaW4="); // admin:admin - - final HttpResponseDetails response = http.send("/secret/token", "POST", headers, ""); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.CREATE_SECRET_TOKEN_401)); - } - - @Test - public void canCreateSecretToken201() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Authorization", "basic YWRtaW46cGFzc3dvcmQ="); // admin:password - - final HttpResponseDetails response = http.send("/secret/token", "POST", headers, ""); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.CREATE_SECRET_TOKEN_201)); - } - - /** SECRET NOTE */ - @Test - public void canNotGetSecretNoteWhenBadAuth403() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("X-AUTH-TOKEN", challenger.getXAuthToken() + "bob"); - - final HttpResponseDetails response = http.send("/secret/note", "GET", headers, ""); - - Assertions.assertEquals(403, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_SECRET_NOTE_403)); - } - - @Test - public void canNotGetSecretNoteWhenNoAuth401() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - - final HttpResponseDetails response = http.send("/secret/note", "GET", headers, ""); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_SECRET_NOTE_401)); - } - - @Test - public void canGetSecretNoteWhenAuthToken200() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("X-AUTH-TOKEN", challenger.getXAuthToken()); - - final HttpResponseDetails response = http.send("/secret/note", "GET", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_SECRET_NOTE_200)); - } - - @Test - public void canGetSecretNoteWhenBearerAuthToken200() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Authorization", "bearer " + challenger.getXAuthToken()); - - final HttpResponseDetails response = http.send("/secret/note", "GET", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_SECRET_NOTE_BEARER_200)); - } - - @Test - public void canPostSecretNoteWhenAuth200() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("X-AUTH-TOKEN", challenger.getXAuthToken()); - headers.put("Content-Type", "application/json"); - - // {"note":"bob"} - final HttpResponseDetails response = - http.send("/secret/note", "POST", headers, "{\"note\":\"bob\"}"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_SECRET_NOTE_200)); - } - - @Test - public void canPostSecretNoteWhenBearerAuth200() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Authorization", "bearer " + challenger.getXAuthToken()); - headers.put("Content-Type", "application/json"); - - // {"note":"bob"} - final HttpResponseDetails response = - http.send("/secret/note", "POST", headers, "{\"note\":\"bob\"}"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_SECRET_NOTE_BEARER_200)); - } - - @Test - public void cannotPostSecretNoteWhenNoAuth401() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - // {"note":"bob"} - final HttpResponseDetails response = - http.send("/secret/note", "POST", headers, "{\"note\":\"bob\"}"); - - Assertions.assertEquals(401, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_SECRET_NOTE_401)); - } - - @Test - public void cannotPostSecretNoteWhenWrongAuth403() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("X-AUTH-TOKEN", challenger.getXAuthToken() + "bob"); - headers.put("Content-Type", "application/json"); - - // {"note":"bob"} - final HttpResponseDetails response = - http.send("/secret/note", "POST", headers, "{\"note\":\"bob\"}"); - - Assertions.assertEquals(403, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.POST_SECRET_NOTE_403)); - } - - @Test - public void canUseMethodOverrideHeaderToPatch500() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("X-HTTP-Method-Override", "PATCH"); - headers.put("Content-Type", "application/json"); - - // {"note":"bob"} - final HttpResponseDetails response = http.send("/heartbeat", "POST", headers, ""); - - Assertions.assertEquals(500, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.OVERRIDE_PATCH_HEARTBEAT_500)); - } - - @Test - public void canUseMethodOverrideHeaderToDelete405() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("X-HTTP-Method-Override", "DELETE"); - headers.put("Content-Type", "application/json"); - - // {"note":"bob"} - final HttpResponseDetails response = http.send("/heartbeat", "POST", headers, ""); - - Assertions.assertEquals(405, response.statusCode); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.OVERRIDE_DELETE_HEARTBEAT_405)); - } - - @Test - public void canUseMethodOverrideHeaderToTrace501() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("X-HTTP-Method-Override", "TRACE"); - headers.put("Content-Type", "application/json"); - - // {"note":"bob"} - final HttpResponseDetails response = http.send("/heartbeat", "POST", headers, ""); - - Assertions.assertEquals(501, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.OVERRIDE_TRACE_HEARTBEAT_501)); - } - - @Test - public void canCreateANewChallenger() { - - // no headers means create a new challenger - Map headers = new HashMap<>(); - - // {"note":"bob"} - final HttpResponseDetails response = http.send("/challenger", "POST", headers, ""); - - Assertions.assertEquals(201, response.statusCode); - - final String challengerCode = response.getHeader("x-challenger"); - - // add to the new challenger to allow counting - newChallenger = - ChallengeMain.getChallenger().getChallengers().getChallenger(challengerCode); - - Assertions.assertTrue(newChallenger.statusOfChallenge(CHALLENGE.CREATE_NEW_CHALLENGER)); - } - - @Test - public void canDeleteAllTodos() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final EntityDefinition todos = - ChallengeMain.getChallenger() - .getThingifier() - .getERmodel() - .getSchema() - .getDefinitionWithSingularOrPluralNamed("todo"); - - ThingStore repository = - ChallengeMain.getChallenger().getThingifier().getStore(challenger.getXChallenger()); - for (EntityInstance instance : repository.entityQueries().list(todos)) { - final HttpResponseDetails response = - http.send( - "/todos/" + instance.getFieldValue("id").asString(), - "DELETE", - x_challenger_header, - ""); - Assertions.assertEquals(200, response.statusCode); - } - - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.DELETE_ALL_TODOS)); - - // add some todos in case this is not the last test - - createTodo(todos, "restored fixture 1"); - createTodo(todos, "restored fixture 2"); - createTodo(todos, "restored fixture 3"); - createTodo(todos, "restored fixture 4"); - } - - @Test - public void canGetChallengerToRestorePass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send( - "/challenger/" + challenger.getXChallenger(), - "GET", - x_challenger_header, - ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.GET_RESTORABLE_CHALLENGER_PROGRESS_STATUS)); - } - - @Test - public void canRestoreAnExistingChallengerPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send( - "/challenger/" + challenger.getXChallenger(), - "GET", - x_challenger_header, - ""); - - final HttpResponseDetails restoreResponse = - http.send( - "/challenger/" + challenger.getXChallenger(), - "PUT", - x_challenger_header, - response.body); - - Assertions.assertEquals(200, restoreResponse.statusCode); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.PUT_RESTORABLE_CHALLENGER_PROGRESS_STATUS)); - } - - @Test - public void canGetTodosDatabaseToRestorePass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - if (!challenger.getXChallenger().equals("rest-api-challenges-single-player")) { - ChallengeMain.getChallenger() - .getThingifier() - .ensureCreatedAndPopulatedInstanceDatabaseNamed(challenger.getXChallenger()); - } - - final HttpResponseDetails response = - http.send( - "/challenger/database/" + challenger.getXChallenger(), - "GET", - x_challenger_header, - ""); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.GET_RESTORABLE_TODOS)); - - Todos todos = new Gson().fromJson(response.body, Todos.class); - Assertions.assertNotNull(todos.todos); - } - - @Test - public void canRestoreTodosDatabaseToPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - if (!challenger.getXChallenger().equals("rest-api-challenges-single-player")) { - ChallengeMain.getChallenger() - .getThingifier() - .ensureCreatedAndPopulatedInstanceDatabaseNamed(challenger.getXChallenger()); - } - - // final HttpResponseDetails response = - // http.send("/challenger/database/" + challenger.getXChallenger(), "GET", - // x_challenger_header, ""); - - Todos newTodos = new Todos(); - Todo aTodo = new Todo(); - aTodo.title = "amended for put"; - aTodo.id = 1; - aTodo.description = "describe me"; - newTodos.todos = new ArrayList<>(); - newTodos.todos.add(aTodo); - - final HttpResponseDetails putresponse = - http.send( - "/challenger/database/" + challenger.getXChallenger(), - "PUT", - x_challenger_header, - new Gson().toJson(newTodos)); - - Assertions.assertEquals(204, putresponse.statusCode); - Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.PUT_RESTORABLE_TODOS)); - - final HttpResponseDetails getAgainResponse = - http.send( - "/challenger/database/" + challenger.getXChallenger(), - "GET", - x_challenger_header, - ""); - - Todos todosAfterAmend = new Gson().fromJson(getAgainResponse.body, Todos.class); - Assertions.assertEquals(newTodos.todos.get(0).title, todosAfterAmend.todos.get(0).title); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/CompleteAllChallengesMultiUserTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/CompleteAllChallengesMultiUserTest.java deleted file mode 100644 index a8009e6b..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/CompleteAllChallengesMultiUserTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package uk.co.compendiumdev.challenger.http.completechallenges; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.challenge.CHALLENGE; -import uk.co.compendiumdev.challenge.ChallengeMain; -import uk.co.compendiumdev.challenge.ChallengerAuthData; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; - -public class CompleteAllChallengesMultiUserTest extends ChallengeCompleteTest { - - @Override - public boolean getIsSinglePlayerMode() { - return false; - } - - @Override - public int getNumberOfChallengesToFail() { - // all challenges work in multi-user mode - return 0; - } - - // these tests below will only work in multi user mode - - @Test - public void restoreChallengeWithPOST() { - - // ensure challenger status file exists - ChallengeMain.getChallenger().getChallengers().persistChallengerState(challenger); - // but is not in memory - ChallengeMain.getChallenger().getChallengers().delete(challenger.getXChallenger()); - - // remember x-challenger guid - String guid = challenger.getXChallenger(); - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - final HttpResponseDetails response = http.send("/challenger", "POST", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - challenger = ChallengeMain.getChallenger().getChallengers().getChallenger(guid); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.POST_RESTORE_EXISTING_CHALLENGER)); - } - - @Test - public void restoreChallengeWithGET() { - - // ensure challenger status file exists - ChallengeMain.getChallenger().getChallengers().persistChallengerState(challenger); - // but is not in memory - ChallengeMain.getChallenger().getChallengers().delete(challenger.getXChallenger()); - - // remember x-challenger guid - String guid = challenger.getXChallenger(); - - // Map x_challenger_header = - // getXChallengerHeader(challenger.getXChallenger()); - - Map headers = new HashMap<>(); - // headers.putAll(x_challenger_header); - headers.put("Content-Type", "application/json"); - - final HttpResponseDetails response = http.send("/challenger/" + guid, "GET", headers, ""); - - Assertions.assertEquals(200, response.statusCode); - challenger = ChallengeMain.getChallenger().getChallengers().getChallenger(guid); - Assertions.assertTrue( - challenger.statusOfChallenge(CHALLENGE.GET_RESTORE_EXISTING_CHALLENGER)); - } - - @Test - public void canRestoreAnNonExistingChallengerPass() { - - Map x_challenger_header = getXChallengerHeader(challenger.getXChallenger()); - - final HttpResponseDetails response = - http.send( - "/challenger/" + challenger.getXChallenger(), - "GET", - x_challenger_header, - ""); - - String newGuid = UUID.randomUUID().toString(); - String jsonData = response.body.replaceAll(challenger.getXChallenger(), newGuid); - - Map new_x_challenger_header = getXChallengerHeader(newGuid); - final HttpResponseDetails restoreResponse = - http.send("/challenger/" + newGuid, "PUT", new_x_challenger_header, jsonData); - - Assertions.assertEquals(201, restoreResponse.statusCode); - ChallengerAuthData newChallenger = challengers.getChallenger(newGuid); - Assertions.assertTrue( - newChallenger.statusOfChallenge( - CHALLENGE.PUT_NEW_RESTORED_CHALLENGER_PROGRESS_STATUS)); - - // allow count to pass by changing state of normally used challenger - challenger.pass(CHALLENGE.PUT_NEW_RESTORED_CHALLENGER_PROGRESS_STATUS); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/CompleteAllChallengesSingleUserTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/CompleteAllChallengesSingleUserTest.java deleted file mode 100644 index fd392e40..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/CompleteAllChallengesSingleUserTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package uk.co.compendiumdev.challenger.http.completechallenges; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.challenge.challengers.Challengers; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; - -public class CompleteAllChallengesSingleUserTest extends ChallengeCompleteTest { - - @Override - public boolean getIsSinglePlayerMode() { - return true; - } - - @Override - public int getNumberOfChallengesToFail() { - // POST to retrieve session only works in multi-user - but is excluded in challenges - // GET to retrieve session only works in multi-user - but is excluded in challenges - // PUT new restored challenger progress only works in multi-user - but is excluded in - // challenges - return 0; - } - - @Test - void canSimulateCreateChallenger() throws InterruptedException { - - final HttpResponseDetails response = http.post("/challenger", ""); - - Assertions.assertEquals(Challengers.SINGLE_PLAYER_GUID, response.getHeader("X-CHALLENGER")); - Assertions.assertEquals(201, response.statusCode); - } - - @Test - void getChallengesHasDifferentLocationRouteThanMultiPlayer() throws InterruptedException { - - final HttpResponseDetails response = http.get("/challenges"); - - Assertions.assertEquals("/gui/challenges", response.getHeader("Location")); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/MultiPlayerModeHookTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/MultiPlayerModeHookTest.java deleted file mode 100644 index 6f0b7d26..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/MultiPlayerModeHookTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package uk.co.compendiumdev.challenger.http.completechallenges; - -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; - -public class MultiPlayerModeHookTest { - - @BeforeAll - public static void controlEnv() { - Environment.stop(); - } - - @ParameterizedTest - @ValueSource(strings = {"POST", "PUT", "DELETE"}) - public void inMultiPlayerModeRequestsWithNoXChallengerHeaderAmendmentVerbsShould401( - String verb) { - - int expectedResponse = 401; - - // force multi-player mode for these tests - final HttpMessageSender http = new HttpMessageSender(Environment.getBaseUri(false)); - - final HttpResponseDetails response = http.send("/todos/1", verb); - Assertions.assertEquals(expectedResponse, response.statusCode); - Assertions.assertTrue(response.body.contains("Cannot amend details.")); - } - - @ParameterizedTest - @ValueSource(strings = {"POST", "PUT", "DELETE"}) - public void inMultiPlayerModeRequestsToAmendDefaultDBItemsShould401(String verb) { - - int expectedResponse = 401; - - // force multi-player mode for these tests - final HttpMessageSender http = new HttpMessageSender(Environment.getBaseUri(false)); - - final Map defaultDatabaseChallengerHeaders = new HashMap<>(); - defaultDatabaseChallengerHeaders.put("X-CHALLENGER", EntityRelModel.DEFAULT_DATABASE_NAME); - - final HttpResponseDetails response = - http.send("/todos/1", verb, defaultDatabaseChallengerHeaders, ""); - Assertions.assertEquals(expectedResponse, response.statusCode); - Assertions.assertTrue(response.body.contains("Cannot amend details.")); - } - - @AfterAll - public static void stopEnv() { - Environment.stop(); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/conditions/ConfigSetupTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/conditions/ConfigSetupTest.java deleted file mode 100644 index 627ad12a..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/conditions/ConfigSetupTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package uk.co.compendiumdev.challenger.http.conditions; - -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; - -public class ConfigSetupTest { - - @Test - public void checkMaxMessageSizeEnforced() { - - // create a challenger - final HttpMessageSender http = new HttpMessageSender(Environment.getBaseUri()); - - final HttpResponseDetails response = http.send("/challenger", "POST"); - Assertions.assertEquals(201, response.statusCode); - - String challenger = response.getHeader("X-CHALLENGER"); - - Map headers = new HashMap<>(); - headers.put("X-CHALLENGER", challenger); - - final HttpResponseDetails response413 = - http.send("/todos/1", "POST", headers, stringOfLength(5001)); - - Assertions.assertEquals(413, response413.statusCode); - } - - private String stringOfLength(int length) { - StringBuilder str = new StringBuilder(); - for (int currLen = 0; currLen < length; currLen++) { - str.append('a'); - } - return str.toString(); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/defects/MultiUserDefectsTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/defects/MultiUserDefectsTest.java deleted file mode 100644 index fee0d8db..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/defects/MultiUserDefectsTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package uk.co.compendiumdev.challenger.http.defects; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; - -public class MultiUserDefectsTest { - - @BeforeAll - public static void controlEnv() { - Environment.stop(); - } - - @Test - public void challengerNotLoadedWhenSkipCallToGetChallengers() throws IOException { - - Environment.stop(); - - // have challenger data predefined but not in memory - // 2ce954c6-caa1-4299-85af-205a9d9f7867.data - String notLoadedChallengerGUID = "2ce954c6-caa1-4299-85af-205a9d9f7867"; - final File resourceFile = - new File( - System.getProperty("user.dir"), - "/src/test/resources/" + notLoadedChallengerGUID + ".data.txt"); - final File folder = new File(System.getProperty("user.dir"), "challengersessions"); - folder.mkdirs(); - final File dataFile = new File(folder, notLoadedChallengerGUID + ".data.txt"); - - if (!resourceFile.exists()) { - Assertions.fail("Could not find data file " + notLoadedChallengerGUID); - } - - if (dataFile.exists()) { - dataFile.delete(); - } - - Files.copy(resourceFile.toPath(), dataFile.toPath()); - - final HttpMessageSender http = new HttpMessageSender(Environment.getBaseUri(false, true)); - http.setHeader("X-CHALLENGER", notLoadedChallengerGUID); - http.setHeader("Authorization", "basic YWRtaW46cGFzc3dvcmQ="); // admin:password - - final HttpResponseDetails response = http.send("/secret/token", "POST"); - Assertions.assertEquals(201, response.statusCode); - - Environment.stop(); - } - - @Test - public void inMultiPlayerModeNoXChallengerHeaderPostTokenCausedNullPointer() { - - // in single player mode this test will 201 because we don't need challenger header - int expectedResponse = 401; - // if(Environment.SINGLE_PLAYER_MODE){ - // expectedResponse=201; - // } - - final HttpMessageSender http = new HttpMessageSender(Environment.getBaseUri(false)); - http.setHeader("Authorization", "basic YWRtaW46cGFzc3dvcmQ="); // admin:password - - final HttpResponseDetails response = http.send("/secret/token", "POST"); - Assertions.assertEquals(expectedResponse, response.statusCode); - } - - @Test - public void inMultiPlayerModeNoXChallengerHeaderGetNoteCausedNullPointer() { - - final HttpMessageSender http = new HttpMessageSender(Environment.getBaseUri(false)); - http.setHeader("Authorization", "basic YWRtaW46cGFzc3dvcmQ="); // admin:password - - final HttpResponseDetails response = http.send("/secret/note", "GET"); - Assertions.assertEquals(401, response.statusCode); - } - - @Test - public void inMultiPlayerModeNoXChallengerHeaderPostNoteCausedNullPointer() { - - final HttpMessageSender http = new HttpMessageSender(Environment.getBaseUri(false)); - http.setHeader("Authorization", "basic YWRtaW46cGFzc3dvcmQ="); // admin:password - http.setHeader("Content-Type", "application/json"); - - final HttpResponseDetails response = http.send("/secret/note", "POST"); - Assertions.assertEquals(401, response.statusCode); - } - - @Test - public void getChallengesShouldReturnChallengesNotAnError() { - - final HttpMessageSender http = new HttpMessageSender(Environment.getBaseUri(false)); - http.setHeader("x-challenger", "idonotexist"); - - final HttpResponseDetails response = http.send("/challenges", "GET"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue( - response.body.contains("POST /challenger (201)"), - "Expected challenge information in response"); - } - - @AfterAll - public static void stopEnv() { - Environment.stop(); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/CanSendHttpRequests.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/CanSendHttpRequests.java deleted file mode 100644 index 78327806..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/CanSendHttpRequests.java +++ /dev/null @@ -1,12 +0,0 @@ -package uk.co.compendiumdev.challenger.http.httpclient; - -import java.net.URL; -import java.util.Map; - -public interface CanSendHttpRequests { - HttpResponseDetails send(URL url, String verb, Map headers, String body); - - HttpRequestDetails getLastRequest(); - - HttpResponseDetails getLastResponse(); -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpClientRequestSender.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpClientRequestSender.java deleted file mode 100644 index 5fd4a387..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpClientRequestSender.java +++ /dev/null @@ -1,117 +0,0 @@ -package uk.co.compendiumdev.challenger.http.httpclient; - -import java.net.*; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** */ -public class HttpClientRequestSender implements CanSendHttpRequests { - - static Logger logger = LoggerFactory.getLogger(HttpClientRequestSender.class); - - private final HttpClient client; - // https://stackoverflow.com/questions/1432961/how-do-i-make-httpurlconnection-use-a-proxy - // Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.0.0.1", 8080)); - // conn = new URL(urlString).openConnection(proxy); - Proxy proxy; - private HttpRequestDetails lastRequest; - private HttpResponseDetails lastResponse; - - public HttpClientRequestSender(String proxyHost, int proxyPort) { - if (proxyHost != null) { - proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); - client = - HttpClient.newBuilder() - .proxy(ProxySelector.of(new InetSocketAddress(proxyHost, proxyPort))) - .build(); - } else { - client = HttpClient.newBuilder().proxy(ProxySelector.getDefault()).build(); - } - } - - // public static void setProxy(String ip, int port){ - // proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(ip, port)); - // } - - public HttpRequestDetails getLastRequest() { - return this.lastRequest; - } - - public HttpResponseDetails getLastResponse() { - return this.lastResponse; - } - - public HttpResponseDetails send( - URL url, String verb, Map headers, String body) { - - HttpResponseDetails response = new HttpResponseDetails(); - - try { - - lastRequest = new HttpRequestDetails(); - - String normalizedVerb = verb.toUpperCase(Locale.ROOT); - final HttpRequest.Builder request = - HttpRequest.newBuilder() - .uri(url.toURI()) - .method(normalizedVerb, HttpRequest.BodyPublishers.ofString(body)); - - // SET HEADERS - for (Map.Entry header : headers.entrySet()) { - request.header(header.getKey(), header.getValue()); - logger.info("Header - " + header.getKey() + " : " + headers.get(header.getValue())); - } - - logger.info("\nSending '" + normalizedVerb + "' request to URL : " + url); - - final HttpRequest actualRequest = request.build(); - - lastRequest.body = body; - for (Map.Entry> actualHeader : - actualRequest.headers().map().entrySet()) { - lastRequest.addHeader(actualHeader.getKey(), actualHeader.getValue().get(0)); - logger.info( - String.format( - "request Header - %s:%s", - actualHeader.getKey(), actualHeader.getValue().get(0))); - } - - final HttpResponse actualResponse = - client.send(actualRequest, HttpResponse.BodyHandlers.ofString()); - - response.statusCode = actualResponse.statusCode(); - - logger.info("response Code : " + response.statusCode); - - response.body = actualResponse.body(); - - // print result - logger.info("response Body: " + response.body); - - // add the headers - Map responseHeaders = new HashMap<>(); - for (Map.Entry> header : - actualResponse.headers().map().entrySet()) { - String headerValue = header.getValue().get(0); - responseHeaders.put(header.getKey(), headerValue); - logger.info("Header: " + header.getKey() + " - " + headerValue); - } - response.setHeaders(responseHeaders); - - lastResponse = response; - - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - - return response; - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpMessageSender.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpMessageSender.java deleted file mode 100644 index aeba3bc9..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpMessageSender.java +++ /dev/null @@ -1,152 +0,0 @@ -package uk.co.compendiumdev.challenger.http.httpclient; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class HttpMessageSender { - - static Logger logger = LoggerFactory.getLogger(HttpMessageSender.class); - - private CanSendHttpRequests sender; - private URL baseUrl; - public String DEFAULT_USER_AGENT = "Mozilla/5.0"; - - public final String HEADER_USER_AGENT = "User-Agent"; - public final String HEADER_CONTENT_TYPE = "Content-Type"; - public final String HEADER_ACCEPT = "Accept"; - public final String HEADER_AUTHORIZATION = "Authorization"; - - public final String CONTENT_XML = "application/xml"; - public final String CONTENT_JSON = "application/json"; - - public Map headers; - private String proxyHost; - private int proxyPort; - - public HttpMessageSender(String baseUrl) { - try { - this.baseUrl = new URL(baseUrl); - } catch (MalformedURLException e) { - logger.error("*** BASE URL is wrong!! {}", baseUrl, e); - } - - headers = new HashMap<>(); - - sender = new HttpClientRequestSender(null, 0); - } - - public HttpRequestDetails getLastRequest() { - return sender.getLastRequest(); - } - - public HttpResponseDetails getLastResponse() { - return sender.getLastResponse(); - } - - public void setProxy(String ip, int port) { - proxyHost = ip; - proxyPort = port; - // sender = new HttpRequestSender(proxyHost, proxyPort); - sender = new HttpClientRequestSender(proxyHost, proxyPort); - } - - public void setUserAgent() { - headers.put(HEADER_USER_AGENT, DEFAULT_USER_AGENT); - } - - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods - public HttpResponseDetails get(String url) { - return sender.send(getUrl(url), "GET", headers, ""); - } - - public HttpResponseDetails head(String url) { - return sender.send(getUrl(url), "HEAD", headers, ""); - } - - public HttpResponseDetails post(String url, String body) { - return sender.send(getUrl(url), "POST", headers, body); - } - - public HttpResponseDetails put(String url, String body) { - return sender.send(getUrl(url), "PUT", headers, body); - } - - public HttpResponseDetails delete(String url) { - return sender.send(getUrl(url), "DELETE", headers, ""); - } - - public HttpResponseDetails connect(String url) { - return sender.send(getUrl(url), "CONNECT", headers, ""); - } - - public HttpResponseDetails options(String url) { - return sender.send(getUrl(url), "OPTIONS", headers, ""); - } - - public HttpResponseDetails trace(String url) { - return sender.send(getUrl(url), "TRACE", headers, ""); - } - - public HttpResponseDetails patch(String url, String body) { - return sender.send(getUrl(url), "PATCH", headers, body); - } - - public HttpResponseDetails send(String url, String verb) { - return sender.send(getUrl(url), verb, headers, ""); - } - - public HttpResponseDetails send( - String url, String verb, Map myheaders, String body) { - headers.clear(); - headers.putAll(myheaders); - return sender.send(getUrl(url), verb, headers, body); - } - - private URL getUrl(String url) { - URL thisUrl; - - try { - thisUrl = new URL(url); - return thisUrl; - } catch (MalformedURLException e) { - } - - try { - thisUrl = new URL(this.baseUrl, url); - return thisUrl; - } catch (MalformedURLException e) { - logger.error("What url are you trying to build? {}", this.baseUrl.toString() + url, e); - } - - return null; - } - - public void setHeader(String headername, String headervalue) { - headers.put(headername, headervalue); - } - - public void setHeaders(Map myHeaders) { - headers.putAll(myHeaders); - } - - public void clearHeaders() { - headers.clear(); - } - - public void setBasicAuth(String username, String password) { - - String basicAuth = username + ":" + password; - basicAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes()); - - setHeader(HEADER_AUTHORIZATION, "Basic " + basicAuth); - } - - public void deleteHeader(String headername) { - headers.remove(headername); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpRequestDetails.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpRequestDetails.java deleted file mode 100644 index 9ec7512e..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpRequestDetails.java +++ /dev/null @@ -1,53 +0,0 @@ -package uk.co.compendiumdev.challenger.http.httpclient; - -import java.util.HashMap; -import java.util.Map; - -public class HttpRequestDetails { - - public String body; - private Map headers = new HashMap<>(); - - public Map getHeaders() { - return headers; - } - - public void setHeaders(Map headers) { - - this.headers.putAll(headers); - } - - public void addHeader(String key, String value) { - headers.put(key, value); - } - - public String raw() { - - StringBuilder rawOutput = new StringBuilder(); - - for (String key : headers.keySet()) { - // output only the null key which is the VERB header - if (headers.get(key) == null) { - rawOutput.append(key); - rawOutput.append("\n"); - } - } - - // output the rest of the headers - for (String key : headers.keySet()) { - if (headers.get(key) != null) { - rawOutput.append(key); - rawOutput.append(": "); - rawOutput.append(headers.get(key)); - rawOutput.append("\n"); - } - } - - if (body != null) { - rawOutput.append("\n"); - rawOutput.append(body); - } - - return rawOutput.toString(); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpRequestSender.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpRequestSender.java deleted file mode 100644 index ed7fd494..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpRequestSender.java +++ /dev/null @@ -1,174 +0,0 @@ -package uk.co.compendiumdev.challenger.http.httpclient; - -import java.io.*; -import java.net.HttpURLConnection; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Limitations HttpURLConnection does not sent PATCH or CONNECT verbs these need to use the - * X-HTTP-Method-Override header which not every server honours - */ -public class HttpRequestSender implements CanSendHttpRequests { - - static Logger logger = LoggerFactory.getLogger(HttpRequestSender.class); - - // https://stackoverflow.com/questions/1432961/how-do-i-make-httpurlconnection-use-a-proxy - // Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.0.0.1", 8080)); - // conn = new URL(urlString).openConnection(proxy); - Proxy proxy; - private HttpRequestDetails lastRequest; - private HttpResponseDetails lastResponse; - - public HttpRequestSender(String proxyHost, int proxyPort) { - if (proxyHost != null) { - proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); - } - } - - // public static void setProxy(String ip, int port){ - // proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(ip, port)); - // } - - public HttpRequestDetails getLastRequest() { - return this.lastRequest; - } - - public HttpResponseDetails getLastResponse() { - return this.lastResponse; - } - - public HttpResponseDetails send( - URL url, String verb, Map headers, String body) { - - HttpResponseDetails response = new HttpResponseDetails(); - - try { - - HttpURLConnection con; - - if (proxy == null) { - con = (HttpURLConnection) url.openConnection(); - } else { - con = (HttpURLConnection) url.openConnection(proxy); - } - - // HTTP VERB - switch (verb.toLowerCase()) { - case "patch": - headers.put("X-HTTP-Method-Override", "PATCH"); - // con.setRequestProperty("X-HTTP-Method-Override", "PATCH"); - con.setRequestMethod("POST"); - break; - case "connect": - headers.put("X-HTTP-Method-Override", "CONNECT"); - // con.setRequestProperty("X-HTTP-Method-Override", "PATCH"); - con.setRequestMethod("POST"); - break; - default: - headers.remove( - "X-HTTP-Method-Override"); // make sure we do not override the verb - con.setRequestMethod(verb); - break; - } - - lastRequest = new HttpRequestDetails(); - - // SET HEADERS - for (String headerName : headers.keySet()) { - con.setRequestProperty(headerName, headers.get(headerName)); - logger.info("Header - " + headerName + " : " + headers.get(headerName)); - lastRequest.addHeader(headerName, headers.get(headerName)); - } - - String payload = body; - - logger.info("\nSending '" + verb + "' request to URL : " + url); - - if (body.length() > 0) { - // Send post request - logger.info(verb + " Body : " + payload); - con.setDoOutput(true); - DataOutputStream wr = new DataOutputStream(con.getOutputStream()); - wr.writeBytes(payload); - wr.flush(); - wr.close(); - } - - // this writesout a lot of rubbish to the console when it runs - int statusCode = con.getResponseCode(); - response.statusCode = statusCode; - - logger.info("response Code : " + statusCode); - - String responseBody = getResponseBody(con); - - logger.info("response Body: " + responseBody); - response.body = responseBody.toString(); - - // add the headers to readable response - Map responseHeaders = new HashMap<>(); - for (String headerKey : con.getHeaderFields().keySet()) { - String headerValue = con.getHeaderField(headerKey); - responseHeaders.put(headerKey, headerValue); - logger.info("Header: " + headerKey + " - " + headerValue); - } - response.setHeaders(responseHeaders); - - lastResponse = response; - - for (String sentHeader : lastRequest.getHeaders().keySet()) { - logger.info( - String.format( - "request Header - %s:%s", - sentHeader, lastRequest.getHeaders().get(sentHeader))); - } - - if (body.length() > 0) { - lastRequest.body = body; - } - - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - - return response; - } - - private String getResponseBody(HttpURLConnection con) { - BufferedReader in = null; - - // https://stackoverflow.com/questions/24707506/httpurlconnection-how-to-read-payload-of-400-response - try { - in = new BufferedReader(new InputStreamReader(con.getInputStream())); - } catch (Exception e) { - // handle 400 exception messages - InputStream stream = con.getErrorStream(); - if (stream != null) { - in = new BufferedReader(new InputStreamReader(stream)); - } - } - - String inputLine; - StringBuffer responseBody = new StringBuffer(); - - try { - if (in != null) { - while ((inputLine = in.readLine()) != null) { - responseBody.append(inputLine); - } - in.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - - return responseBody.toString(); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpResponseDetails.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpResponseDetails.java deleted file mode 100644 index e8b85d4c..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/httpclient/HttpResponseDetails.java +++ /dev/null @@ -1,25 +0,0 @@ -package uk.co.compendiumdev.challenger.http.httpclient; - -import java.util.HashMap; -import java.util.Map; - -public class HttpResponseDetails { - - public int statusCode; - public String body; - private Map headers = new HashMap<>(); - - public Map getHeaders() { - return headers; - } - - public void setHeaders(Map headers) { - for (Map.Entry given : headers.entrySet()) { - this.headers.put(given.getKey().toUpperCase(), given.getValue()); - } - } - - public String getHeader(final String headername) { - return headers.get(headername.toUpperCase()); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Challenge.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Challenge.java deleted file mode 100644 index 4b2677f3..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Challenge.java +++ /dev/null @@ -1,7 +0,0 @@ -package uk.co.compendiumdev.challenger.payloads; - -public class Challenge { - public String name; - public String description; - public boolean status; -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Challenges.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Challenges.java deleted file mode 100644 index a8609724..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Challenges.java +++ /dev/null @@ -1,7 +0,0 @@ -package uk.co.compendiumdev.challenger.payloads; - -import java.util.List; - -public class Challenges { - public List challenges; -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Todo.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Todo.java deleted file mode 100644 index f5bcf79e..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Todo.java +++ /dev/null @@ -1,9 +0,0 @@ -package uk.co.compendiumdev.challenger.payloads; - -public class Todo { - - public Integer id; - public String title; - public String description; - public Boolean doneStatus; -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Todos.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Todos.java deleted file mode 100644 index a62d1a25..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/payloads/Todos.java +++ /dev/null @@ -1,7 +0,0 @@ -package uk.co.compendiumdev.challenger.payloads; - -import java.util.List; - -public class Todos { - public List todos; -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/MirrorModeTest.java b/challenger/src/test/java/uk/co/compendiumdev/practicemodes/MirrorModeTest.java deleted file mode 100644 index 637f35f5..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/MirrorModeTest.java +++ /dev/null @@ -1,268 +0,0 @@ -package uk.co.compendiumdev.practicemodes; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; -import org.junit.jupiter.api.*; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; -import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; - -public class MirrorModeTest { - - private static HttpMessageSender http; - - @BeforeAll - static void createHttp() { - // this uses the Environment to startup the server app to - // issue http tests and test the server routing - http = new HttpMessageSender(Environment.getBaseUri()); - } - - @Test - public void canGetMirrorRequestDefaults() { - - final HttpResponseDetails response = http.send("/mirror/request", "GET", Map.of(), ""); - - Assertions.assertEquals(200, response.statusCode); - - // by default the response is application/json - Assertions.assertEquals("application/json", response.getHeader("content-type")); - Assertions.assertTrue(response.body.contains("{\"details\":")); - // check some basic items in the response - - Assertions.assertTrue(response.body.contains("Parsed Query Params")); - Assertions.assertTrue(response.body.contains("Raw Headers")); - Assertions.assertTrue(response.body.contains("Body")); - } - - @Test - public void canGetMirrorRequestAsText() { - - final HttpResponseDetails response = - http.send("/mirror/request", "GET", Map.of("Accept", "text/plain"), ""); - - Assertions.assertEquals(200, response.statusCode); - - Assertions.assertEquals("text/plain", response.getHeader("content-type")); - Assertions.assertTrue(response.body.startsWith("GET ")); - Assertions.assertFalse(response.body.contains("{\"details\":")); - - // check some basic items in the response - Assertions.assertTrue(response.body.contains("Parsed Query Params")); - Assertions.assertTrue(response.body.contains("Raw Headers")); - Assertions.assertTrue(response.body.contains("Body")); - } - - @Test - public void canGetMirrorRequestAsJson() { - - final HttpResponseDetails response = - http.send("/mirror/request", "GET", Map.of("Accept", "application/json"), ""); - - Assertions.assertEquals(200, response.statusCode); - - Assertions.assertEquals("application/json", response.getHeader("content-type")); - Assertions.assertTrue(response.body.startsWith("{")); - Assertions.assertTrue(response.body.contains("{\"details\":")); - - // check some basic items in the response - Assertions.assertTrue(response.body.contains("Parsed Query Params")); - Assertions.assertTrue(response.body.contains("Raw Headers")); - Assertions.assertTrue(response.body.contains("Body")); - } - - @Test - public void canGetMirrorRequestAsXml() { - - final HttpResponseDetails response = - http.send("/mirror/request", "GET", Map.of("Accept", "application/xml"), ""); - - Assertions.assertEquals(200, response.statusCode); - - Assertions.assertEquals("application/xml", response.getHeader("content-type")); - Assertions.assertTrue(response.body.startsWith("")); - Assertions.assertTrue(response.body.contains("
")); - Assertions.assertTrue(response.body.contains("
")); - Assertions.assertTrue(response.body.contains("")); - Assertions.assertTrue(response.body.contains(" ")); - - // check some basic items in the response - Assertions.assertTrue(response.body.contains("Parsed Query Params")); - Assertions.assertTrue(response.body.contains("Raw Headers")); - Assertions.assertTrue(response.body.contains("Body")); - } - - static Stream simpleEndPointMirrorRequestRoutingStatus() { - List args = new ArrayList<>(); - - args.add(Arguments.of(200, "get", "/mirror/request")); - args.add(Arguments.of(200, "head", "/mirror/request")); - args.add(Arguments.of(204, "options", "/mirror/request")); - args.add(Arguments.of(200, "put", "/mirror/request")); - args.add(Arguments.of(200, "post", "/mirror/request")); - args.add(Arguments.of(200, "delete", "/mirror/request")); - args.add(Arguments.of(200, "trace", "/mirror/request")); - return args.stream(); - } - - @ParameterizedTest(name = "can hit mirror mode with multiple verbs {0} for {1} {2}") - @MethodSource("simpleEndPointMirrorRequestRoutingStatus") - void simpleMirrorRequestRoutingTest(int statusCode, String verb, String url) { - final HttpResponseDetails response = http.send(url, verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } - - static Stream simpleEndPointMirrorRawRoutingStatus() { - List args = new ArrayList<>(); - - args.add(Arguments.of(200, "get", "/mirror/raw")); - args.add(Arguments.of(200, "head", "/mirror/raw")); - args.add(Arguments.of(204, "options", "/mirror/raw")); - args.add(Arguments.of(200, "put", "/mirror/raw")); - args.add(Arguments.of(200, "post", "/mirror/raw")); - args.add(Arguments.of(200, "delete", "/mirror/raw")); - args.add(Arguments.of(200, "trace", "/mirror/raw")); - return args.stream(); - } - - @ParameterizedTest(name = "can hit mirror mode with multiple verbs {0} for {1} {2}") - @MethodSource("simpleEndPointMirrorRawRoutingStatus") - void simpleMirrorRawRoutingTest(int statusCode, String verb, String url) { - final HttpResponseDetails response = http.send(url, verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } - - @Test - void rawIsAlwaysATextResponse() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - final HttpResponseDetails response = http.send("/mirror/raw", "get"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("text/plain", response.getHeader("Content-Type")); - Assertions.assertTrue(response.body.startsWith("GET ")); - } - - static Stream acceptHeaders() { - List args = new ArrayList<>(); - - args.add(Arguments.of("application/xml")); - args.add(Arguments.of("application/json")); - args.add(Arguments.of("text/html")); - args.add(Arguments.of("text/plain")); - args.add(Arguments.of("unknown/format")); - return args.stream(); - } - - @ParameterizedTest(name = "can hit mirror raw with any valid accept and get text like {0}") - @MethodSource("acceptHeaders") - public void canOnlyGetMirrorRawAsText(String accept) { - - final HttpResponseDetails response = - http.send("/mirror/raw", "GET", Map.of("Accept", accept), ""); - - Assertions.assertEquals(200, response.statusCode); - - Assertions.assertEquals("text/plain", response.getHeader("content-type")); - Assertions.assertFalse(response.body.contains("{\"details\":")); - - // check some basic items in the response - Assertions.assertTrue(response.body.contains("Parsed Query Params")); - Assertions.assertTrue(response.body.contains("Raw Headers")); - Assertions.assertTrue(response.body.contains("Body")); - } - - static Stream simpleRoutingStatus() { - List args = new ArrayList<>(); - - // get - args.add(Arguments.of(200, "get", "/mirror/request")); - // 200 same as get - args.add(Arguments.of(200, "head", "/mirror/request")); - args.add(Arguments.of(204, "options", "/mirror/request")); - // post - args.add(Arguments.of(200, "post", "/mirror/request")); - args.add(Arguments.of(200, "put", "/mirror/request")); - args.add(Arguments.of(200, "delete", "/mirror/request")); - args.add(Arguments.of(200, "patch", "/mirror/request")); - args.add(Arguments.of(200, "trace", "/mirror/request")); - - args.add(Arguments.of(200, "get", "/mirror/request/bob")); - // 200 same as get - args.add(Arguments.of(200, "head", "/mirror/request/bob")); - args.add(Arguments.of(204, "options", "/mirror/request/bob")); - // post - args.add(Arguments.of(200, "post", "/mirror/request/bob")); - args.add(Arguments.of(200, "put", "/mirror/request/bob")); - args.add(Arguments.of(200, "delete", "/mirror/request/bob")); - args.add(Arguments.of(200, "patch", "/mirror/request/bob")); - args.add(Arguments.of(200, "trace", "/mirror/request/bob")); - // raw routes - args.add(Arguments.of(200, "get", "/mirror/raw")); - // 200 same as get - args.add(Arguments.of(200, "head", "/mirror/raw")); - args.add(Arguments.of(204, "options", "/mirror/raw")); - // post - args.add(Arguments.of(200, "post", "/mirror/raw")); - args.add(Arguments.of(200, "put", "/mirror/raw")); - args.add(Arguments.of(200, "delete", "/mirror/raw")); - args.add(Arguments.of(200, "patch", "/mirror/raw")); - args.add(Arguments.of(200, "trace", "/mirror/raw")); - - args.add(Arguments.of(200, "get", "/mirror/raw/bob")); - // 200 same as get - args.add(Arguments.of(200, "head", "/mirror/raw/bob")); - args.add(Arguments.of(204, "options", "/mirror/raw/bob")); - // post - args.add(Arguments.of(200, "post", "/mirror/raw/bob")); - args.add(Arguments.of(200, "put", "/mirror/raw/bob")); - args.add(Arguments.of(200, "delete", "/mirror/raw/bob")); - args.add(Arguments.of(200, "patch", "/mirror/raw/bob")); - args.add(Arguments.of(200, "trace", "/mirror/raw/bob")); - - return args.stream(); - } - - @ParameterizedTest(name = "simple status routing expected {0} for {1} {2}") - @MethodSource("simpleRoutingStatus") - void simpleRoutingTest(int statusCode, String verb, String url) { - final HttpResponseDetails response = http.send(url, verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } - - @Test - void requestContentLengthIsCheckedForLength() { - http.clearHeaders(); - - HttpHeadersBlock headers = new HttpHeadersBlock(); - headers.put("Accept", "text/plain"); - - final HttpResponseDetails response = - http.send("/mirror/request", "post", headers.asMap(), stringOfLength(24000 + 1)); - - Assertions.assertEquals(413, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertTrue( - response.body.contains("Error: request too large, max allowed is 24000 bytes")); - } - - private String stringOfLength(int length) { - StringBuilder str = new StringBuilder(); - for (int currLen = 0; currLen < length; currLen++) { - str.append('a'); - } - return str.toString(); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/SmulationModeTest.java b/challenger/src/test/java/uk/co/compendiumdev/practicemodes/SmulationModeTest.java deleted file mode 100644 index 26c02cb5..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/SmulationModeTest.java +++ /dev/null @@ -1,380 +0,0 @@ -package uk.co.compendiumdev.practicemodes; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; - -public class SmulationModeTest { - private static HttpMessageSender http; - - @BeforeAll - static void createHttp() { - // this uses the Environment to startup the server app to - // issue http tests and test the server routing - http = new HttpMessageSender(Environment.getBaseUri()); - } - - static Stream simpleRoutingStatus() { - List args = new ArrayList<>(); - - args.add(Arguments.of(200, "get", "/sim/entities")); - args.add(Arguments.of(200, "head", "/sim/entities")); - args.add(Arguments.of(204, "options", "/sim/entities")); - args.add(Arguments.of(501, "patch", "/sim/entities")); - args.add(Arguments.of(501, "trace", "/sim/entities")); - - args.add(Arguments.of(200, "get", "/sim/entities/1")); - args.add(Arguments.of(200, "head", "/sim/entities/1")); - args.add(Arguments.of(204, "options", "/sim/entities/1")); - args.add(Arguments.of(501, "patch", "/sim/entities/1")); - args.add(Arguments.of(501, "trace", "/sim/entities/1")); - - args.add(Arguments.of(405, "delete", "/sim/entities")); - args.add(Arguments.of(404, "delete", "/sim/entities/56")); - args.add(Arguments.of(403, "post", "/sim/entities/8")); - args.add(Arguments.of(403, "put", "/sim/entities/7")); - - return args.stream(); - } - - @ParameterizedTest(name = "simple status routing expected {0} for {1} {2}") - @MethodSource("simpleRoutingStatus") - void simpleRoutingTest(int statusCode, String verb, String url) { - final HttpResponseDetails response = http.send(url, verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } - - @Test - void canSimulateGetOfEntitiesJSON() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.get("/sim/entities"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertTrue(response.body.startsWith("{\"entities\":[{")); - Assertions.assertTrue(response.body.contains("id\":1,")); - Assertions.assertTrue(response.body.contains("id\":2,")); - Assertions.assertTrue(response.body.contains("id\":3,")); - Assertions.assertTrue(response.body.contains("id\":4,")); - Assertions.assertTrue(response.body.contains("id\":5,")); - Assertions.assertTrue(response.body.contains("id\":6,")); - Assertions.assertTrue(response.body.contains("id\":7,")); - Assertions.assertTrue(response.body.contains("id\":8,")); - Assertions.assertTrue(response.body.contains("id\":9,")); - Assertions.assertTrue(response.body.contains("id\":10,")); - - // 11 is in the thingifier but should not be returned - Assertions.assertFalse(response.body.contains("id\":11,")); - } - - @Test - void canFilterSimulatedEntitiesThroughRepository() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.get("/sim/entities?id=11"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("{\"entities\":[]}", response.body); - - response = http.get("/sim/entities?name*=entity%20number%201"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue(response.body.contains("\"id\":1,")); - Assertions.assertFalse(response.body.contains("\"id\":10,")); - } - - @Test - void canSortSimulatedEntitiesThroughRepository() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.get("/sim/entities?sortBy=-id"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue( - response.body.indexOf("\"id\":10,") < response.body.indexOf("\"id\":9,")); - Assertions.assertFalse(response.body.contains("\"id\":11,")); - } - - @Test - void canSimulateGetOfEntitiesXML() { - - http.clearHeaders(); - http.setHeader("Accept", "application/xml"); - - HttpResponseDetails response = http.get("/sim/entities"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/xml", response.getHeader("Content-Type")); - Assertions.assertTrue(response.body.startsWith("")); - Assertions.assertTrue(response.body.contains("1")); - Assertions.assertTrue(response.body.contains("2")); - Assertions.assertTrue(response.body.contains("3")); - Assertions.assertTrue(response.body.contains("4")); - Assertions.assertTrue(response.body.contains("5")); - Assertions.assertTrue(response.body.contains("6")); - Assertions.assertTrue(response.body.contains("7")); - Assertions.assertTrue(response.body.contains("8")); - Assertions.assertTrue(response.body.contains("9")); - Assertions.assertTrue(response.body.contains("10")); - - // 11 is in the thingifier but should not be returned - Assertions.assertFalse(response.body.contains("11")); - } - - @Test - void canSimulateHeadOfEntitiesJSON() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.head("/sim/entities"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertTrue(response.body.equals("")); - } - - @Test - void canSimulateHeadOfEntitiesXML() { - - http.clearHeaders(); - http.setHeader("Accept", "application/xml"); - - HttpResponseDetails response = http.head("/sim/entities"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/xml", response.getHeader("Content-Type")); - Assertions.assertTrue(response.body.equals("")); - } - - @Test - void canSimulatePostCreateAndGetOfEntity11() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.post("/sim/entities", "{\"name\":\"bob\"}"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals("/sim/entities/11", response.getHeader("Location")); - String entity11 = "{\"id\":11,\"name\":\"bob\",\"description\":\"\"}"; - Assertions.assertEquals(entity11, response.body); - - response = http.get("/sim/entities/11"); - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals(entity11, response.body); - } - - @Test - void canSimulatePostCreateOfEntity11() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.post("/sim/entities/11", "{\"name\":\"bob\"}"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals("/sim/entities/11", response.getHeader("Location")); - String entity11 = "{\"id\":11,\"name\":\"bob\",\"description\":\"\"}"; - Assertions.assertEquals(entity11, response.body); - - response = http.get("/sim/entities/11"); - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals(entity11, response.body); - } - - @Test - void canSimulatePutCreateOfEntity11() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.put("/sim/entities/11", "{\"name\":\"bob\"}"); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals("/sim/entities/11", response.getHeader("Location")); - String entity11 = "{\"id\":11,\"name\":\"bob\",\"description\":\"\"}"; - Assertions.assertEquals(entity11, response.body); - - response = http.get("/sim/entities/11"); - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals(entity11, response.body); - } - - @Test - void canSimulatePostAmendOfEntity10() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.post("/sim/entities/10", "{\"name\":\"eris\"}"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals( - null, response.getHeader("Location"), "expected no location header on amend"); - String entity11 = "{\"id\":10,\"name\":\"eris\",\"description\":\"\"}"; - Assertions.assertEquals(entity11, response.body); - - response = http.get("/sim/entities/10"); - Assertions.assertEquals(200, response.statusCode); - String entity10 = "{\"id\":10,\"name\":\"eris\",\"description\":\"\"}"; - Assertions.assertEquals(entity10, response.body); - } - - @Test - void canSimulatePutAmendOfEntity10() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.put("/sim/entities/10", "{\"name\":\"eris\"}"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("Content-Type")); - Assertions.assertEquals( - null, response.getHeader("Location"), "expected no location header on amend"); - String entity11 = "{\"id\":10,\"name\":\"eris\",\"description\":\"\"}"; - Assertions.assertEquals(entity11, response.body); - - response = http.get("/sim/entities/10"); - Assertions.assertEquals(200, response.statusCode); - String entity10 = "{\"id\":10,\"name\":\"eris\",\"description\":\"\"}"; - Assertions.assertEquals(entity10, response.body); - } - - @Test - void canSimulatePostAmendErrors() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.post("/sim/entities/23", "{\"name\":\"eris\"}"); - Assertions.assertEquals(404, response.statusCode); - Assertions.assertTrue(response.body.contains("Could not find Entity with ID")); - - response = http.post("/sim/entities/7", "{\"name\":\"eris\"}"); - Assertions.assertEquals(403, response.statusCode); - Assertions.assertTrue(response.body.contains("Not authorised to amend that entity")); - } - - @Test - void canSimulatePutAmendErrors() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.put("/sim/entities/23", "{\"name\":\"eris\"}"); - Assertions.assertEquals(403, response.statusCode); - Assertions.assertTrue(response.body.contains("Not authorised to create that entity")); - - response = http.put("/sim/entities/7", "{\"name\":\"eris\"}"); - Assertions.assertEquals(403, response.statusCode); - Assertions.assertTrue(response.body.contains("Not authorised to amend that entity")); - } - - @Test - void canSimulateDeleteOfEntity9() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.delete("/sim/entities/9"); - Assertions.assertEquals(204, response.statusCode); - - response = http.get("/sim/entities/9"); - Assertions.assertEquals(404, response.statusCode); - } - - @Test - void canSimulateGetEntityId() { - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - // cannot get something not in the thingifier - HttpResponseDetails response = http.get("/sim/entities/23"); - Assertions.assertEquals(404, response.statusCode); - - // can get something in the thingifier - response = http.get("/sim/entities/3"); - Assertions.assertEquals(200, response.statusCode); - } - - @Test - void canSimulateHeadEntityId() { - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - // cannot get something not in the thingifier - HttpResponseDetails response = http.head("/sim/entities/23"); - Assertions.assertEquals(404, response.statusCode); - Assertions.assertEquals("", response.body); - - // can get something in the thingifier - response = http.head("/sim/entities/3"); - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("", response.body); - - // can get fake TODO: create an amendment test and move this there - response = http.head("/sim/entities/10"); - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("", response.body); - } - - @Test - void canNotDeleteOtherEntities() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.delete("/sim/entities/1"); - Assertions.assertEquals(403, response.statusCode); - - response = http.delete("/sim/entities/8"); - Assertions.assertEquals(403, response.statusCode); - } - - @Test - void optionsAtTopLevel() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.options("/sim/entities"); - Assertions.assertEquals(204, response.statusCode); - Assertions.assertEquals( - "GET, POST, PUT, HEAD, OPTIONS", response.getHeader("allow").toUpperCase()); - } - - @Test - void optionsAtEntityLevel() { - - http.clearHeaders(); - http.setHeader("Accept", "application/json"); - - HttpResponseDetails response = http.options("/sim/entities/1"); - Assertions.assertEquals(204, response.statusCode); - Assertions.assertEquals( - "GET, POST, PUT, DELETE, HEAD, OPTIONS", response.getHeader("allow").toUpperCase()); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/SimpleApiCrudTest.java b/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/SimpleApiCrudTest.java deleted file mode 100644 index 609f8e58..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/SimpleApiCrudTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package uk.co.compendiumdev.practicemodes.simpleapi; - -import com.google.gson.Gson; -import java.util.Random; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.practicemodes.simpleapi.testabstractions.Item; -import uk.co.compendiumdev.practicemodes.simpleapi.testabstractions.SimpleAPIApi; -import uk.co.compendiumdev.serverstart.Environment; - -public class SimpleApiCrudTest { - - /* - Simple tests based on CRUD flows for ELH - - Item - / | \ - Create Existing (*) Delete - / \ - Read (o) Update (o) - */ - - /* - Paths To Cover: - - - C - leave alone - - CD - - CRD - - CRUD - - existing variants (how far to push it?) - - CRRRD - how many? why? this would primarily be for multi-user interaction (can we see others' amendments) - - CUUUUD - how many? why? this would be for field considerations and potential multi-user update clashes - - CRRUURRD - variants of RU - again why? at a path level this is overkill, what do we think we would gain from this? - - For a CRUD flow to work it has to be valid, so all of the above are 'valid' - - e.g. - - - C - - valid details (valid values for all fields) - - minimum (mandatory fields only) - - mix of optional fields + mandatory fields - - maximum (all fields) - - field formats? e.g. can use strings for price? "12.12" [exclude from CRUD] - - field values? min max etc. [exclude from CRUD] - - R - - existing - - U - - existing - - minimum (mandatory fields only) (PUT and POST are different) - - mix ( mix of optional and mandatory) - - maximum (all fields) - - D - - existing - - update based on a variety of low level field changes - - REST Overlay (POST, PUT) - - expand diagram to include a new layer under update - - POST (all field update) - - PUT (all field update) - - Invalid operations: - - - C - - with invalid data such that it is not created - - duplicate - - R - - a non-existing item - - (not-created yet, previously deleted) - - invalid for rendering in output(could invalid data get into the system?) - [doesn't seem feasible with current data fields] - - in invalid state (e.g. multi-user, while being updated) [exclude from CRUD] - - U - - update a non-existing item - - (not-created yet, previously deleted) - - update to become duplicate item should fail - - can't update id [double check this] - - can't have duplicate iban13 - - update when someone else has updated (multi-user) [exclude from CRUD] - - PUT and POST have different update conditions? - - treat updates as a Matrix transformation of field conditions - - below minimum (none of the fields - error or no update?) - - above maximum (more fields than required: extra fields, duplicate fields) [exclude from CRUD?] - - D - - delete a non-existing item - - (not-created yet, previously deleted) - - */ - - private static HttpMessageSender http; - private static SimpleAPIApi api; - private static Random random; - - @BeforeAll - static void createHttp() { - // this uses the Environment to startup the server app to - // issue http tests and test the server routing - http = new HttpMessageSender(Environment.getBaseUri()); - api = new SimpleAPIApi(http); - random = new Random(); - } - - @Test - public void createAnItemAndLeaveItUntouched() { - - Item create = new Item(); - create.isbn13 = Item.randomIsbn(random); - create.type = "dvd"; - create.price = 01.99F; - - HttpResponseDetails response = api.apiCreateItemResponse(create); - - System.out.println(response.body); - Assertions.assertEquals(201, response.statusCode); - Assertions.assertTrue(response.getHeader("location").startsWith("/simpleapi/items/")); - - Item created = new Gson().fromJson(response.body, Item.class); - - Assertions.assertEquals(create.type, created.type); - Assertions.assertEquals(create.isbn13, created.isbn13); - Assertions.assertEquals(create.price, created.price); - Assertions.assertEquals(0, created.numberinstock); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/SimpleApiModeTest.java b/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/SimpleApiModeTest.java deleted file mode 100644 index a753b8f2..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/SimpleApiModeTest.java +++ /dev/null @@ -1,482 +0,0 @@ -package uk.co.compendiumdev.practicemodes.simpleapi; - -import com.google.gson.Gson; -import java.util.*; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.practicemodes.simpleapi.testabstractions.ErrorMessagesResponse; -import uk.co.compendiumdev.practicemodes.simpleapi.testabstractions.Item; -import uk.co.compendiumdev.practicemodes.simpleapi.testabstractions.Items; -import uk.co.compendiumdev.practicemodes.simpleapi.testabstractions.SimpleAPIApi; -import uk.co.compendiumdev.serverstart.Environment; - -/* - The Simple API is wired up using the default thingifier so we are mainly confirming format, - specific API Config, and wiring -*/ -public class SimpleApiModeTest { - - private static HttpMessageSender http; - private static SimpleAPIApi api; - - @BeforeAll - static void createHttp() { - // this uses the Environment to startup the server app to - // issue http tests and test the server routing - http = new HttpMessageSender(Environment.getBaseUri()); - api = new SimpleAPIApi(http); - } - - static Stream simpleRoutingStatus() { - List args = new ArrayList<>(); - - args.add(Arguments.of(200, "get", "/simpleapi/items")); - args.add(Arguments.of(200, "head", "/simpleapi/items")); - args.add(Arguments.of(204, "options", "/simpleapi/items")); - // deliberate 'bug' of 501 instead of 405 - args.add(Arguments.of(501, "patch", "/simpleapi/items")); - args.add(Arguments.of(501, "trace", "/simpleapi/items")); - args.add(Arguments.of(405, "delete", "/simpleapi/items")); - args.add(Arguments.of(405, "put", "/simpleapi/items")); - args.add(Arguments.of(400, "post", "/simpleapi/items")); - - args.add(Arguments.of(200, "get", "/simpleapi/randomisbn")); - args.add(Arguments.of(200, "head", "/simpleapi/randomisbn")); - args.add(Arguments.of(200, "options", "/simpleapi/randomisbn")); - args.add(Arguments.of(405, "post", "/simpleapi/randomisbn")); - args.add(Arguments.of(405, "put", "/simpleapi/randomisbn")); - args.add(Arguments.of(405, "patch", "/simpleapi/randomisbn")); - args.add(Arguments.of(405, "trace", "/simpleapi/randomisbn")); - - return args.stream(); - } - - @ParameterizedTest(name = "simple status routing expected {0} for {1} {2}") - @MethodSource("simpleRoutingStatus") - void simpleRoutingTest(int statusCode, String verb, String url) { - final HttpResponseDetails response = http.send(url, verb); - - Assertions.assertEquals(statusCode, response.statusCode); - } - - @Test - public void canPostItemAsXmlAndAcceptJson() { - - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/xml"); - headers.put("Accept", "application/json"); - - // Minimum payload - final HttpResponseDetails response = - http.send( - "/simpleapi/items", - "POST", - headers, - """ - - 1.64 - 128-6-32-856404-0 - cd - - """ - .stripIndent()); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("content-type")); - - Item item = new Gson().fromJson(response.body, Item.class); - Assertions.assertEquals(1.64f, item.price); - Assertions.assertEquals(0, item.numberinstock); - Assertions.assertEquals("128-6-32-856404-0", item.isbn13); - Assertions.assertEquals("cd", item.type); - } - - @Test - public void canPostItemAsJsonAndAcceptJson() { - - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/json"); - - // full valid payload - final HttpResponseDetails response = - http.send( - "/simpleapi/items", - "POST", - headers, - """ - { - "price":2.00, - "numberinstock":3, - "isbn13": "1234567890123", - "type":book - } - """ - .stripIndent()); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("content-type")); - - Item item = new Gson().fromJson(response.body, Item.class); - Assertions.assertEquals(2f, item.price); - Assertions.assertEquals(3, item.numberinstock); - Assertions.assertEquals("1234567890123", item.isbn13); - Assertions.assertEquals("book", item.type); - } - - @ParameterizedTest - @ValueSource(strings = {"", "-"}) - public void canNotPostCreateItemsWithDuplicateISBN(String dupeSynonymReplace) { - - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/json"); - - Random random = new Random(); - String aRandomIsbn = Item.randomIsbn(random); - - // full valid payload - final HttpResponseDetails response = - http.send( - "/simpleapi/items", - "POST", - headers, - """ - { - "price":2.00, - "numberinstock":2, - "isbn13": "%s", - "type":book - } - """ - .formatted(aRandomIsbn) - .stripIndent()); - - final HttpResponseDetails duplicateIsbnResponse = - http.send( - "/simpleapi/items", - "POST", - headers, - """ - { - "price":2.00, - "numberinstock":2, - "isbn13": "%s", - "type":book - } - """ - .formatted(aRandomIsbn.replace("-", dupeSynonymReplace)) - .stripIndent()); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("content-type")); - - Assertions.assertEquals(400, duplicateIsbnResponse.statusCode); - Assertions.assertTrue( - duplicateIsbnResponse.body.contains("Field isbn13 Value is not unique"), - "did not expect " + duplicateIsbnResponse.body); - } - - @ParameterizedTest - @ValueSource(strings = {"POST", "PUT"}) - public void canNotAmendItemsToHaveDuplicateISBN(String verbPostPut) { - - Random random = new Random(); - - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/json"); - - Item anItem = new Item(); - anItem.type = "book"; - anItem.price = 2.00F; - anItem.isbn13 = Item.randomIsbn(random); - - Item anItemToAmend = new Item(); - anItemToAmend.type = "book"; - anItemToAmend.price = 3.00F; - anItemToAmend.isbn13 = Item.randomIsbn(random); - - api.apiCreateItem(anItem); - Item itemToAmend = api.apiCreateItem(anItemToAmend); - - final HttpResponseDetails amendResponse = - http.send( - "/simpleapi/items/" + itemToAmend.id, - verbPostPut, - headers, - """ - { - "price":2.00, - "numberinstock":2, - "isbn13": "%s", - "type":book - } - """ - .formatted(anItem.isbn13) - .stripIndent()); - - Assertions.assertEquals( - 400, - amendResponse.statusCode, - "should not be able to amend item to " + amendResponse.body); - Assertions.assertTrue( - amendResponse.body.contains("Field isbn13 Value is not unique"), - "did not expect " + amendResponse.body); - } - - @ParameterizedTest - @ValueSource(strings = {"", "-"}) - public void canNotAmendItemsToHaveDuplicateISBNBasedOnUniqueComparison( - String dupeSynonymReplace) { - - Random random = new Random(); - - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/json"); - - Item anItem = new Item(); - anItem.type = "book"; - anItem.price = 2.00F; - anItem.isbn13 = Item.randomIsbn(random); - - Item anItemToAmend = new Item(); - anItemToAmend.type = "book"; - anItemToAmend.price = 3.00F; - anItemToAmend.isbn13 = Item.randomIsbn(random); - - api.apiCreateItem(anItem); - Item itemToAmend = api.apiCreateItem(anItemToAmend); - - final HttpResponseDetails amendResponse = - http.send( - "/simpleapi/items/" + itemToAmend.id, - "POST", - headers, - """ - { - "price":2.00, - "numberinstock":2, - "isbn13": "%s", - "type":book - } - """ - .formatted(anItem.isbn13.replace("-", dupeSynonymReplace)) - .stripIndent()); - - Assertions.assertEquals( - 400, - amendResponse.statusCode, - "should not be able to amend item to " + amendResponse.body); - Assertions.assertTrue( - amendResponse.body.contains("Field isbn13 Value is not unique"), - "did not expect " + amendResponse.body); - } - - @Test - public void cannotPostItemWithId() { - - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/json"); - - // full valid payload - final HttpResponseDetails response = - http.send( - "/simpleapi/items", - "POST", - headers, - """ - { - "id" : 1, - "price":2.00, - "numberinstock":2, - "isbn13": "1234567890123", - "type":book - } - """ - .stripIndent()); - - Assertions.assertEquals(400, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("content-type")); - - ErrorMessagesResponse error = - new Gson().fromJson(response.body, ErrorMessagesResponse.class); - Assertions.assertEquals(1, error.errorMessages.size()); - Assertions.assertTrue(error.errorMessages.get(0).contains("Not allowed to create with id")); - } - - @Test - public void canGetItemsAsJson() { - - Items items = api.apiGetItems(); - Assertions.assertFalse(items.items.isEmpty()); - } - - @Test - public void canCreateAnItemAndThenGetIt() { - - Item itemToCreate = new Item(); - itemToCreate.isbn13 = "1111111111111"; - itemToCreate.price = 1.23f; - itemToCreate.type = "book"; - - Item createdItem = api.apiCreateItem(itemToCreate); - - Item retrievedItem = api.apiGetItem(createdItem.id); - - Assertions.assertEquals(createdItem.id, retrievedItem.id); - Assertions.assertEquals(itemToCreate.isbn13, retrievedItem.isbn13); - Assertions.assertEquals(itemToCreate.price, retrievedItem.price); - Assertions.assertEquals(itemToCreate.type, retrievedItem.type); - } - - @Test - public void canCreateAnItemAndThenDeleteIt() { - - Item itemToCreate = new Item(); - itemToCreate.isbn13 = "9111111111111"; - itemToCreate.price = 1.23f; - itemToCreate.type = "book"; - - Item createdItem = api.apiCreateItem(itemToCreate); - - HttpResponseDetails response = api.apiDeleteItem(createdItem.id); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals(404, api.apiGetItemResponse(createdItem.id).statusCode); - } - - @Test - public void canOnlyCreateAMaxOf100Items() { - - Items items = api.apiGetItems(); - - int numberToCreate = 100 - items.items.size(); - - Item itemToCreate = new Item(); - itemToCreate.isbn13 = "0001234567890"; - itemToCreate.price = 1.23f; - itemToCreate.type = "book"; - - for (int itemx = 0; itemx < numberToCreate; itemx++) { - itemToCreate.isbn13 = String.format("%03d", itemx) + "1234567890"; - api.apiCreateItem(itemToCreate); - } - - itemToCreate.isbn13 = "101" + "1234567890"; - HttpResponseDetails finalResponse = api.apiCreateItemResponse(itemToCreate); - Assertions.assertEquals(400, finalResponse.statusCode); - - ErrorMessagesResponse errorMessage = - new Gson().fromJson(finalResponse.body, ErrorMessagesResponse.class); - Assertions.assertEquals( - errorMessage.errorMessages.get(0), - "ERROR: Cannot add instance, maximum limit of 100 reached"); - } - - @Test - public void canNeverDeleteItemsToEmpty() { - - Items items = api.apiGetItems(); - - for (Item item : items.items) { - api.apiDeleteItem(item.id); - } - - // and yet there are some left - Items moreItems = api.apiGetItems(); - - // the data populator creates 8 items - Assertions.assertEquals(8, moreItems.items.size()); - } - - @Test - public void dataRepopulationHappensWhen3Left() { - - Items items = api.apiGetItems(); - - int itemsLeft = items.items.size(); - - for (Item item : items.items) { - api.apiDeleteItem(item.id); - itemsLeft--; - if (itemsLeft == 5) break; - } - - // population kicks in when less than 5 - Items itemsPrePopulation = api.apiGetItems(); - Assertions.assertEquals(5, itemsPrePopulation.items.size()); - - // delete one more for repopulation - api.apiDeleteItem(itemsPrePopulation.items.get(0).id); - - // repopulation should kick in - // so check there are some left - Items moreItems = api.apiGetItems(); - - // the data populator creates 8 items - Assertions.assertEquals(4 + 8, moreItems.items.size()); - } - - @Test - public void canGetARandomISBN() { - - // full valid payload - final HttpResponseDetails response = - http.send("/simpleapi/randomisbn", "GET", new HashMap<>(), ""); - - Assertions.assertEquals(200, response.statusCode); - String isbn = response.body.replace("-", ""); - Assertions.assertTrue(isbn.length() == 13); - } - - @Test - public void canUseRandomISBNToCreateItem() { - - final HttpResponseDetails isbnresponse = - http.send("/simpleapi/randomisbn", "GET", new HashMap<>(), ""); - - Assertions.assertEquals(200, isbnresponse.statusCode); - String isbn = isbnresponse.body.replace("-", ""); - - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/json"); - - // full valid payload - final HttpResponseDetails response = - http.send( - "/simpleapi/items", - "POST", - headers, - """ - { - "price":3.00, - "numberinstock":4, - "isbn13": "%s", - "type":book - } - """ - .formatted(isbn) - .stripIndent()); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("content-type")); - - Item item = new Gson().fromJson(response.body, Item.class); - Assertions.assertEquals(3f, item.price); - Assertions.assertEquals(4, item.numberinstock); - Assertions.assertEquals(isbn, item.isbn13); - Assertions.assertEquals("book", item.type); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/ErrorMessagesResponse.java b/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/ErrorMessagesResponse.java deleted file mode 100644 index 782b9b7d..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/ErrorMessagesResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package uk.co.compendiumdev.practicemodes.simpleapi.testabstractions; - -import java.util.List; - -public class ErrorMessagesResponse { - public List errorMessages; -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/Item.java b/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/Item.java deleted file mode 100644 index 38147844..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/Item.java +++ /dev/null @@ -1,23 +0,0 @@ -package uk.co.compendiumdev.practicemodes.simpleapi.testabstractions; - -import java.util.Random; - -public class Item { - - public Integer id; - public Float price; - public Integer numberinstock; - public String isbn13; - public String type; - - public static String randomIsbn(Random random) { - - String isbn13 = "xxx-x-xx-xxxxxx-x"; - - while (isbn13.contains("x")) { - isbn13 = isbn13.replaceFirst("x", String.valueOf(random.nextInt(9))); - } - - return isbn13; - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/Items.java b/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/Items.java deleted file mode 100644 index 3323bed9..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/Items.java +++ /dev/null @@ -1,7 +0,0 @@ -package uk.co.compendiumdev.practicemodes.simpleapi.testabstractions; - -import java.util.List; - -public class Items { - public List items; -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/SimpleAPIApi.java b/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/SimpleAPIApi.java deleted file mode 100644 index ae671052..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/practicemodes/simpleapi/testabstractions/SimpleAPIApi.java +++ /dev/null @@ -1,82 +0,0 @@ -package uk.co.compendiumdev.practicemodes.simpleapi.testabstractions; - -import com.google.gson.Gson; -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.Assertions; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; - -public class SimpleAPIApi { - private final HttpMessageSender http; - - public SimpleAPIApi(HttpMessageSender http) { - this.http = http; - } - - /* - All the api abstraction classes to support the simple HTTP Tests - */ - - public HttpResponseDetails apiGetItemResponse(Integer id) { - Map headers = new HashMap<>(); - headers.put("Accept", "application/json"); - - return http.send("/simpleapi/items/" + id, "GET", headers, ""); - } - - public Item apiGetItem(Integer id) { - final HttpResponseDetails response = apiGetItemResponse(id); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("content-type")); - - // thingifier is configurable and can send a single item back on a GET /things/:id - return new Gson().fromJson(response.body, Item.class); - } - - public HttpResponseDetails apiCreateItemResponse(Item itemToCreate) { - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/json"); - - return http.send("/simpleapi/items", "POST", headers, new Gson().toJson(itemToCreate)); - } - - public Item apiCreateItem(Item itemToCreate) { - - final HttpResponseDetails response = apiCreateItemResponse(itemToCreate); - - Assertions.assertEquals(201, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("content-type")); - - return new Gson().fromJson(response.body, Item.class); - } - - public HttpResponseDetails apiDeleteItem(Integer id) { - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("Accept", "application/json"); - - return http.send("/simpleapi/items/" + id, "DELETE", headers, ""); - } - - public Items apiGetItems() { - Map headers = new HashMap<>(); - headers.put("Accept", "application/json"); - - final HttpResponseDetails response = - http.send( - "/simpleapi/items", - "GET", - headers, - """ - """ - .stripIndent()); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertEquals("application/json", response.getHeader("content-type")); - - return new Gson().fromJson(response.body, Items.class); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/serverstart/Environment.java b/challenger/src/test/java/uk/co/compendiumdev/serverstart/Environment.java deleted file mode 100644 index 970895c8..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/serverstart/Environment.java +++ /dev/null @@ -1,91 +0,0 @@ -package uk.co.compendiumdev.serverstart; - -import java.util.ArrayList; -import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.co.compendiumdev.challenge.ChallengeMain; - -public class Environment { - - static Logger logger = LoggerFactory.getLogger(Port.class); - - // these unit tests only work in multi-player mode - public static boolean SINGLE_PLAYER_MODE = false; - - public static String getEnv(String urlPath) { - return getBaseUri() + urlPath; - } - - public static String getBaseUri() { - return Environment.getBaseUri(SINGLE_PLAYER_MODE); - } - - public static String getBaseUri(boolean isSinglePlayerMode) { - return Environment.getBaseUri(isSinglePlayerMode, false); - } - - public static String getBaseUri(boolean isSinglePlayerMode, boolean writeFiles) { - - // return environment if want to run externally - // if(true) - // return "https://apichallenges.eviltester.com"; - - logger.info("Checking app running"); - // if not running then start the server - if (ChallengeMain.getChallenger() == null || !Port.inUse("localhost", 4567)) { - - logger.info( - "App not running starting with single player mode == " + isSinglePlayerMode); - // start it up - List args = new ArrayList<>(); - - if (isSinglePlayerMode) { - // setup an single player mode args here - } else { - // setup an multi player mode args here - args.add("-multiplayer"); - } - - if (!writeFiles) { - args.add("-nostorage"); - } - - ChallengeMain.main(args.toArray(new String[0])); - waitTillRunningStatus(true); - - logger.info( - "App running started with single player mode == " - + ChallengeMain.getChallenger().isSinglePlayerMode()); - } - - return "http://localhost:4567"; - - // TODO: incorporate browsermob proxy and allow configuration of all - // requests through a proxy file to output a HAR file of all requests for later review - } - - public static void waitTillRunningStatus(final boolean running) { - // wait till running - int maxtries = 10; - logger.info("Waiting for server to be in state " + running); - while (Port.inUse("localhost", 4567) != running) { - maxtries--; - try { - logger.info(String.format("Waiting for server %d", maxtries)); - Thread.sleep(500); - } catch (InterruptedException e) { - logger.error("Interruption during running check ", e); - } - if (maxtries <= 0) { - logger.info("Max retries in running status finished."); - return; - } - } - } - - public static void stop() { - ChallengeMain.stop(); - waitTillRunningStatus(false); - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/serverstart/Port.java b/challenger/src/test/java/uk/co/compendiumdev/serverstart/Port.java deleted file mode 100644 index 1b5cec35..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/serverstart/Port.java +++ /dev/null @@ -1,50 +0,0 @@ -package uk.co.compendiumdev.serverstart; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** check if port is in use */ -public class Port { - - static Logger logger = LoggerFactory.getLogger(Port.class); - - public static boolean inUse(String host, String port) { - return inUse(host, Integer.valueOf(port)); - } - - // http://stackoverflow.com/questions/434718/sockets-discover-port-availability-using-java - public static boolean inUse(String host, int port) { - Socket s = null; - logger.info("Checking for port on " + host + ":" + port); - - try { - - // prevent this taking ages when no proxy setup by timing out after 5 seconds - SocketAddress address = new InetSocketAddress(host, port); - s = new Socket(); - s.connect(address, 5000); - - // If the code makes it this far without an exception it means - // something is using the port and has responded. - logger.warn("Port " + port + " is in use, assuming proxy is running"); - return true; - } catch (IOException e) { - logger.info("Port " + port + " is free, no proxy running"); - return false; - } finally { - if (s != null) { - try { - s.close(); - } catch (IOException e) { - logger.error("Port " + port + " check had an error ", e); - // swallow exception and return false for our use case - return false; - } - } - } - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/uirouting/SeoTitleContentValidationTest.java b/challenger/src/test/java/uk/co/compendiumdev/uirouting/SeoTitleContentValidationTest.java deleted file mode 100644 index 1ea862c4..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/uirouting/SeoTitleContentValidationTest.java +++ /dev/null @@ -1,257 +0,0 @@ -package uk.co.compendiumdev.uirouting; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class SeoTitleContentValidationTest { - - @Test - void allContentMarkdownFilesContainSeoTitleAndMeetQualityChecks() throws IOException { - - final Path contentRoot = resolveContentRoot(); - - final List markdownFiles = new ArrayList<>(); - try (Stream walk = Files.walk(contentRoot)) { - walk.filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".md")) - .forEach(markdownFiles::add); - } - - Assertions.assertFalse( - markdownFiles.isEmpty(), "No markdown files found in content directory"); - - final Map> seoTitlesToPaths = new HashMap<>(); - final Set allowedMetadataKeys = - new HashSet<>( - Arrays.asList( - "seo_title", - "seo_description", - "meta_robots", - "og_image", - "og_image_alt", - "og_type", - "twitter_card", - "twitter_site", - "schema_type", - "schema_author", - "schema_publisher", - "schema_image", - "schema_breadcrumb_enabled", - "schema_howto_enabled", - "schema_howto_steps", - "schema_video_enabled", - "schema_video_id")); - final List missingSeoTitle = new ArrayList<>(); - final List emptySeoTitle = new ArrayList<>(); - final List outOfRangeSeoTitle = new ArrayList<>(); - final List missingSeoDescription = new ArrayList<>(); - final List emptySeoDescription = new ArrayList<>(); - final List outOfRangeSeoDescription = new ArrayList<>(); - final List missingLastmod = new ArrayList<>(); - final List emptyLastmod = new ArrayList<>(); - final List invalidLastmodFormat = new ArrayList<>(); - final List missingDescriptionForIndexablePage = new ArrayList<>(); - final List malformedMetadataKeys = new ArrayList<>(); - final List invalidOgImageOverrides = new ArrayList<>(); - - final Path publicRoot = resolvePublicRoot(); - - for (Path markdownFile : markdownFiles) { - final String relativePath = - contentRoot.relativize(markdownFile).toString().replace("\\", "/"); - final List lines = Files.readAllLines(markdownFile, StandardCharsets.UTF_8); - final String seoTitle = extractHeaderValue(lines, "seo_title"); - final String description = extractHeaderValue(lines, "description"); - final String seoDescription = extractHeaderValue(lines, "seo_description"); - final String lastmod = extractHeaderValue(lines, "lastmod"); - final String metaRobots = extractHeaderValue(lines, "meta_robots"); - final String ogImage = extractHeaderValue(lines, "og_image"); - final List headerKeys = extractHeaderKeys(lines); - - if (seoTitle == null) { - missingSeoTitle.add(relativePath); - continue; - } - - if (seoTitle.trim().isEmpty()) { - emptySeoTitle.add(relativePath); - continue; - } - - if (seoTitle.length() < 45 || seoTitle.length() > 70) { - outOfRangeSeoTitle.add(relativePath + " (" + seoTitle.length() + "): " + seoTitle); - } - - seoTitlesToPaths.computeIfAbsent(seoTitle, key -> new ArrayList<>()).add(relativePath); - - if (lastmod == null) { - missingLastmod.add(relativePath); - } else if (lastmod.trim().isEmpty()) { - emptyLastmod.add(relativePath); - } else if (!lastmod.matches("^\\d{4}-\\d{2}-\\d{2}$")) { - invalidLastmodFormat.add(relativePath + " -> " + lastmod); - } - - if (seoDescription == null) { - missingSeoDescription.add(relativePath); - } else if (seoDescription.trim().isEmpty()) { - emptySeoDescription.add(relativePath); - } else { - // keep explicit fixture override free for escaping/special-char assertions - if (!relativePath.equals("seo-metadata-test-page.md") - && (seoDescription.length() < 110 || seoDescription.length() > 170)) { - outOfRangeSeoDescription.add( - relativePath + " (" + seoDescription.length() + "): " + seoDescription); - } - } - - final boolean indexable = - metaRobots == null || !metaRobots.toLowerCase().contains("noindex"); - if (indexable && (description == null || description.trim().isEmpty())) { - missingDescriptionForIndexablePage.add(relativePath); - } - - for (String key : headerKeys) { - final boolean isMetadataKey = - key.startsWith("seo_") - || key.startsWith("og_") - || key.startsWith("twitter_") - || key.startsWith("schema_") - || key.equals("meta_robots"); - if (isMetadataKey && !allowedMetadataKeys.contains(key)) { - malformedMetadataKeys.add(relativePath + " -> " + key); - } - } - - if (ogImage != null && !ogImage.trim().isEmpty() && ogImage.startsWith("/")) { - final Path ogImagePath = - publicRoot.resolve( - ogImage.substring(1).replace("/", java.io.File.separator)); - if (!Files.exists(ogImagePath)) { - invalidOgImageOverrides.add(relativePath + " -> " + ogImage); - } - } - } - - final List duplicateSeoTitles = new ArrayList<>(); - for (Map.Entry> entry : seoTitlesToPaths.entrySet()) { - if (entry.getValue().size() > 1) { - duplicateSeoTitles.add( - entry.getKey() + " -> " + String.join(", ", entry.getValue())); - } - } - - Assertions.assertTrue( - missingSeoTitle.isEmpty(), - "Missing seo_title in: " + String.join("; ", missingSeoTitle)); - Assertions.assertTrue( - emptySeoTitle.isEmpty(), "Empty seo_title in: " + String.join("; ", emptySeoTitle)); - Assertions.assertTrue( - outOfRangeSeoTitle.isEmpty(), - "seo_title out of range (45-70 chars): " + String.join("; ", outOfRangeSeoTitle)); - Assertions.assertTrue( - missingSeoDescription.isEmpty(), - "Missing seo_description in: " + String.join("; ", missingSeoDescription)); - Assertions.assertTrue( - emptySeoDescription.isEmpty(), - "Empty seo_description in: " + String.join("; ", emptySeoDescription)); - Assertions.assertTrue( - outOfRangeSeoDescription.isEmpty(), - "seo_description out of range (110-170 chars): " - + String.join("; ", outOfRangeSeoDescription)); - Assertions.assertTrue( - missingLastmod.isEmpty(), - "Missing lastmod in: " + String.join("; ", missingLastmod)); - Assertions.assertTrue( - emptyLastmod.isEmpty(), "Empty lastmod in: " + String.join("; ", emptyLastmod)); - Assertions.assertTrue( - invalidLastmodFormat.isEmpty(), - "Invalid lastmod format (expected YYYY-MM-DD): " - + String.join("; ", invalidLastmodFormat)); - Assertions.assertTrue( - duplicateSeoTitles.isEmpty(), - "Duplicate seo_title values found: " + String.join("; ", duplicateSeoTitles)); - Assertions.assertTrue( - missingDescriptionForIndexablePage.isEmpty(), - "Indexable pages missing description: " - + String.join("; ", missingDescriptionForIndexablePage)); - Assertions.assertTrue( - malformedMetadataKeys.isEmpty(), - "Malformed SEO/OG/Twitter metadata keys: " - + String.join("; ", malformedMetadataKeys)); - Assertions.assertTrue( - invalidOgImageOverrides.isEmpty(), - "og_image override paths not found in public assets: " - + String.join("; ", invalidOgImageOverrides)); - } - - private Path resolveContentRoot() { - final Path moduleRelative = Paths.get("src", "main", "resources", "content"); - if (Files.exists(moduleRelative)) { - return moduleRelative; - } - return Paths.get("challenger", "src", "main", "resources", "content"); - } - - private Path resolvePublicRoot() { - final Path moduleRelative = Paths.get("src", "main", "resources", "public"); - if (Files.exists(moduleRelative)) { - return moduleRelative; - } - return Paths.get("challenger", "src", "main", "resources", "public"); - } - - private String extractHeaderValue(final List lines, final String key) { - - if (lines.size() < 3 || !lines.get(0).trim().equals("---")) { - return null; - } - - for (int i = 1; i < lines.size(); i++) { - final String line = lines.get(i); - - if (line.trim().equals("---")) { - break; - } - - if (line.startsWith(key + ": ")) { - return line.replaceFirst("^" + key + ":\\s*", "").trim(); - } - } - - return null; - } - - private List extractHeaderKeys(final List lines) { - final List keys = new ArrayList<>(); - if (lines.size() < 3 || !lines.get(0).trim().equals("---")) { - return keys; - } - - for (int i = 1; i < lines.size(); i++) { - final String line = lines.get(i); - - if (line.trim().equals("---")) { - break; - } - - final int keySeparator = line.indexOf(": "); - if (keySeparator > 0) { - keys.add(line.substring(0, keySeparator).trim()); - } - } - return keys; - } -} diff --git a/challenger/src/test/java/uk/co/compendiumdev/uirouting/UiPagesAreReachableTest.java b/challenger/src/test/java/uk/co/compendiumdev/uirouting/UiPagesAreReachableTest.java deleted file mode 100644 index 982ee632..00000000 --- a/challenger/src/test/java/uk/co/compendiumdev/uirouting/UiPagesAreReachableTest.java +++ /dev/null @@ -1,502 +0,0 @@ -package uk.co.compendiumdev.uirouting; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import uk.co.compendiumdev.challenger.http.httpclient.HttpMessageSender; -import uk.co.compendiumdev.challenger.http.httpclient.HttpResponseDetails; -import uk.co.compendiumdev.serverstart.Environment; - -public class UiPagesAreReachableTest { - - /* - Check UI routing without spinning up a browser test tool - */ - - private static HttpMessageSender http; - - @BeforeAll - static void createHttp() { - // this uses the Environment to startup the server app to - // issue http tests and test the server routing - http = new HttpMessageSender(Environment.getBaseUri()); - - // Basic Browser Headers - http.clearHeaders(); - http.setHeader("ContentType", "text/html; charset=utf-8"); - http.setHeader( - "Accept", - "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"); - } - - @Test - void noProcessingWhenNoBasicAuth() { - - final HttpResponseDetails response = http.send("/", "get"); - - Assertions.assertEquals(200, response.statusCode); - Assertions.assertTrue( - response.body.contains( - "")); - Assertions.assertTrue( - response.body.contains("")); - Assertions.assertTrue( - response.body.contains( - "")); - Assertions.assertTrue(response.body.contains("application/ld+json")); - Assertions.assertTrue(response.body.contains("\"@type\":\"Organization\"")); - Assertions.assertTrue(response.body.contains("\"@type\":\"WebSite\"")); - Assertions.assertTrue(response.body.contains("\"@type\":\"WebPage\"")); - assertContainsHeaderAndFooter(response); - } - - @Test - void receive404onMissingPage() { - - final HttpResponseDetails response = http.send("/bob", "get"); - - Assertions.assertEquals(404, response.statusCode); - } - - @Test - void simulated404PageExistsAndReportsAs404() { - - // we currently don't have 404 because of the way the app is constructed - // instead we should trap a 404 response and return a 307 redirecting to - // 404 page with the original url appended to allow javascript to render - // as if it was a 404 page - - final HttpResponseDetails response = http.send("/gui/404", "get"); - - Assertions.assertEquals(404, response.statusCode); - assertContainsHeaderAndFooter(response); - Assertions.assertTrue(response.body.contains("

Page Not Found

")); - } - - @Test - void simulated404PageExistsAndReportsAs404WithPath() { - - // we currently don't have 404 because of the way the app is constructed - // instead we should trap a 404 response and return a 307 redirecting to - // 404 page with the original url appended to allow javascript to render - // as if it was a 404 page - - final HttpResponseDetails response = http.send("/gui/404/bob/dobbs", "get"); - - Assertions.assertEquals(404, response.statusCode); - assertContainsHeaderAndFooter(response); - Assertions.assertTrue(response.body.contains("

Page Not Found

")); - } - - static Stream simplePageRoutingStatus() { - List args = new ArrayList<>(); - - // home page - args.add(Arguments.of(200, "API Challenges Tutorials and Testing Practice", "")); - args.add(Arguments.of(200, "API Challenges Tutorials and Testing Practice", "/")); - // entities - args.add(Arguments.of(200, "Entities Menu", "/gui/entities")); - args.add(Arguments.of(200, "todo Instances", "gui/instances?entity=todo")); - - // Challenges - args.add(Arguments.of(200, "API Challenges - Improve your API Skills", "/gui/challenges")); - args.add( - Arguments.of( - 200, - "API Challenges - Improve your API Skills", - "/gui/challenges/unkownchallenger")); - - // Additional Pages - args.add( - Arguments.of( - 200, "Learning Utilities and Resources | API Challenges", "/learning")); - args.add( - Arguments.of( - 200, "Multi-User Instructions | API Challenges Guide", "/gui/multiuser")); - args.add(Arguments.of(200, "API Challenges API Documentation | API Challenges", "/docs")); - args.add( - Arguments.of( - 200, - "HTTP Mirror Mode | API Challenges Practice Mode", - "/practice-modes/mirror")); - args.add( - Arguments.of( - 200, - "Simulation Mode | API Challenges Practice Mode", - "/practice-modes/simulation")); - return args.stream(); - } - - @ParameterizedTest(name = "simple known page routing expected {0} for {1} {2}") - @MethodSource("simplePageRoutingStatus") - void simplePageRoutingTest(int statusCode, String title, String url) { - final HttpResponseDetails response = http.send(url, "get"); - - Assertions.assertEquals(statusCode, response.statusCode); - Assertions.assertTrue( - response.body.contains(String.format("%s", title)), - String.format("Title not found %s", title)); - assertContainsHeaderAndFooter(response); - } - - private void assertContainsHeaderAndFooter(HttpResponseDetails response) { - - if (!response.body.contains("
")) { - Assertions.fail("Page did not contain header menu"); - } - if (!response.body.contains("