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 index ddabc4c0..2b23b1a6 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeSolutionLink.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/ChallengeSolutionLink.java @@ -29,6 +29,11 @@ public String asHtmlAHref() { if(linkData.isEmpty()){ return linkText; } - return String.format("%s",linkData, linkText); + + String target="target='_blank'"; + if(!linkData.startsWith("http")){ + target=""; + } + return String.format("%s",linkData, target, linkText); } } 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 index 1ca36ff7..3a305fe6 100644 --- 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 @@ -12,7 +12,7 @@ public static ChallengeDefinitionData createChallenger201(int challengeOrder) { ); aChallenge.addHint("In multi-user mode, you need to create an X-CHALLENGER Session first", "/gui/multiuser.html"); 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", "https://www.eviltester.com/apichallenges/howto/post-challenger-201"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/create-session/post-challenger-201"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw"); return aChallenge; } @@ -28,7 +28,7 @@ public static ChallengeDefinitionData getRestoreExistingChallenger200(int challe 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", "https://www.eviltester.com/apichallenges/howto/post-challenger-201"); + //aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/post-create/post-challenger-201"); //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 index 47326d23..d062d784 100644 --- 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 @@ -13,7 +13,7 @@ public static ChallengeDefinitionData deleteTodosId200(int challengeOrder) { 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", "https://www.eviltester.com/apichallenges/howto/delete-todos-id-200"); + 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 index c7659a16..9de1d4b1 100644 --- 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 @@ -10,7 +10,7 @@ public static ChallengeDefinitionData getChallenges200(int challengeOrder) { "GET /challenges (200)", "Issue a GET request on the `/challenges` end point"); - aChallenge.addSolutionLink("Read Solution", "HREF", "https://www.eviltester.com/apichallenges/howto/get-challenges-200/"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/first-challenge/get-challenges-200"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "DrAjk2NaPRo"); return aChallenge; @@ -23,7 +23,7 @@ public static ChallengeDefinitionData getTodos200(int challengeOrder) { "GET /todos (200)", "Issue a GET request on the `/todos` end point"); - aChallenge.addSolutionLink("Read Solution", "HREF", "https://www.eviltester.com/apichallenges/howto/get-todos-200"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/get/get-todos-200"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "OpisB0UZq0c"); return aChallenge; @@ -35,7 +35,7 @@ public static ChallengeDefinitionData getTodos404(int 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", "https://www.eviltester.com/apichallenges/howto/get-todo-404"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/get/get-todo-404"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "gAJzqgcN9dc"); return aChallenge; @@ -48,7 +48,7 @@ public static ChallengeDefinitionData getTodo200(int challengeOrder) { "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", "https://www.eviltester.com/apichallenges/howto/get-todos-id-200"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/get/get-todos-id-200"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "JDbbSY3U_rY"); return aChallenge; @@ -63,7 +63,7 @@ public static ChallengeDefinitionData getTodo404(int challengeOrder) { 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", "https://www.eviltester.com/apichallenges/howto/get-todos-id-404"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/get/get-todos-id-404"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "1S5kpd8-xfM"); return aChallenge; @@ -78,7 +78,7 @@ public static ChallengeDefinitionData getTodosFiltered200(int challengeOrder) { 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", "https://www.eviltester.com/apichallenges/howto/get-todos-200-filter"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/get/get-todos-200-filter"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "G-sLuhyPMuw"); return aChallenge; } @@ -102,7 +102,7 @@ public static ChallengeDefinitionData getTodosAcceptXML200(int 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","https://www.eviltester.com/apichallenges/howto/get-todos-xml-200/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/accept-header/get-todos-200-xml"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "cLeEuZm2VG8"); return aChallenge; } @@ -113,7 +113,7 @@ public static ChallengeDefinitionData getTodosAcceptJson200(int 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","https://www.eviltester.com/apichallenges/howto/get-todos-json-200/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/accept-header/get-todos-200-json"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "79JTHiby2Qw"); return aChallenge; } @@ -124,7 +124,7 @@ public static ChallengeDefinitionData getTodosAcceptAny200(int 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","https://www.eviltester.com/apichallenges/howto/get-todos-any-200/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/accept-header/get-todos-200-any"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "O4DhJ8Ohkk8"); return aChallenge; } @@ -135,7 +135,7 @@ public static ChallengeDefinitionData getTodosPreferAcceptXML200(int challengeOr "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","https://www.eviltester.com/apichallenges/howto/get-todos-xml-preference-200/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/accept-header/get-todos-200-xml-pref"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "sLChuy9pc9U"); return aChallenge; } @@ -146,7 +146,7 @@ public static ChallengeDefinitionData getTodosNoAccept200(int 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","https://www.eviltester.com/apichallenges/howto/get-todos-no-accept-200/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/accept-header/get-todos-200-no-accept"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "CSVP2PcvOdg"); return aChallenge; } @@ -157,7 +157,7 @@ public static ChallengeDefinitionData getTodosUnavailableAccept406(int challenge "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","https://www.eviltester.com/apichallenges/howto/19-get-todos-invalid-accept-406/"); + 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 index 5da910d6..5269289a 100644 --- 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 @@ -10,7 +10,7 @@ public static ChallengeDefinitionData headTodos200(int challengeOrder) { "HEAD /todos (200)", "Issue a HEAD request on the `/todos` end point"); - aChallenge.addSolutionLink("Read Solution", "HREF", "https://www.eviltester.com/apichallenges/howto/head-todos-200"); + 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/OptionsChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/OptionsChallenges.java index b38af95d..166b192a 100644 --- 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 @@ -10,7 +10,7 @@ public static ChallengeDefinitionData optionsTodos200(int 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","https://www.eviltester.com/apichallenges/howto/options-todos-200/"); + 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 index f36fc7be..356439a7 100644 --- 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 @@ -16,7 +16,7 @@ public static ChallengeDefinitionData postTodos201(int challengeOrder) { "POST /todos (201)", "Issue a POST request to successfully create a todo"); - aChallenge.addSolutionLink("Read Solution", "HREF", "https://www.eviltester.com/apichallenges/howto/post-todos-201"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/post-create/post-todos-201"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA"); return aChallenge; } @@ -40,7 +40,7 @@ public static ChallengeDefinitionData postTodosBadDoneStatus400(int challengeOrd "POST /todos (400) doneStatus", "Issue a POST request to create a todo but fail validation on the `doneStatus` field"); - aChallenge.addSolutionLink("Read Solution", "HREF", "https://www.eviltester.com/apichallenges/howto/post-todos-400"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/post-create/post-todos-400"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "tlye5bQ72g0"); return aChallenge; } @@ -109,7 +109,7 @@ public static ChallengeDefinitionData postTodosId200(int challengeOrder) { "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", "https://www.eviltester.com/apichallenges/howto/post-todos-id-200"); + aChallenge.addSolutionLink("Read Solution", "HREF", "/apichallenges/solutions/post-update/post-todos-id-200"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "feXdRpZ_tgs"); return aChallenge; } @@ -139,7 +139,7 @@ public static ChallengeDefinitionData postCreateTodoWithXMLAcceptXML(int challen "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","https://www.eviltester.com/apichallenges/howto/20-post-todos-xml/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/content-type-header/post-todos-xml"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "2-KBYHwb7MM"); return aChallenge; } @@ -151,7 +151,7 @@ public static ChallengeDefinitionData postCreateTodoWithJsonAcceptJson(int chall "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","https://www.eviltester.com/apichallenges/howto/21-post-todos-json/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/content-type-header/post-todos-json"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "VS9qIhgp51Q"); return aChallenge; } @@ -163,7 +163,7 @@ public static ChallengeDefinitionData postCreateUnsupportedContentType415(int ch "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","https://www.eviltester.com/apichallenges/howto/22-post-todos-unsupported-415/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/content-type-header/post-todos-415"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "L8H-vkbXyr0"); return aChallenge; } @@ -180,7 +180,7 @@ public static ChallengeDefinitionData postTodosXmlToJson201(int 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","https://www.eviltester.com/apichallenges/howto/23-post-xml-accept-json/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/mix-accept-content/post-xml-accept-json"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "kfe7VtaV7u0"); return aChallenge; } @@ -192,7 +192,7 @@ public static ChallengeDefinitionData postTodosJsonToXml201(int 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","https://www.eviltester.com/apichallenges/howto/24-post-json-accept-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/SecretTokenChallenges.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challenges/definitions/SecretTokenChallenges.java index 278a5639..bde94fbf 100644 --- 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 @@ -12,7 +12,7 @@ public static ChallengeDefinitionData createSecretTokenNotAuthenticated401(int c "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","https://www.eviltester.com/apichallenges/howto/29-authentication-post-secret-token/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/authentication/post-secret-401"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "RSQGADU3SLA"); return aChallenge; } @@ -25,7 +25,7 @@ public static ChallengeDefinitionData createSecretTokenAuthenticated201(int chal "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","https://www.eviltester.com/apichallenges/howto/30-authentication-post-secret-token-201/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/authentication/post-secret-201"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "J2GQiuEfHkI"); return aChallenge; } @@ -39,7 +39,7 @@ public static ChallengeDefinitionData forbiddenNotAuthorized403(int challengeOrd "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","https://www.eviltester.com/apichallenges/howto/31-secret-note-forbidden-403/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/authorization/get-secret-note-403"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "77mnUQezdas"); return aChallenge; } @@ -52,7 +52,7 @@ public static ChallengeDefinitionData invalidRequestNoAuthHeader401(int challeng "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","https://www.eviltester.com/apichallenges/howto/32-secret-note-401-unauthorized/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/authorization/get-secret-note-401"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "__uZlQZ48io"); return aChallenge; } @@ -65,7 +65,7 @@ public static ChallengeDefinitionData authorizedGet200(int challengeOrder) { "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","https://www.eviltester.com/apichallenges/howto/33-authorized-get-secret-note-200/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/authorization/get-secret-note-200"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "2uRpzr2OmEY"); return aChallenge; } @@ -78,7 +78,7 @@ public static ChallengeDefinitionData authorizedUpdate200(int challengeOrder) { "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","https://www.eviltester.com/apichallenges/howto/34-post-amend-secret-note-200/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/authorization/post-secret-note-200"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "A9T9yjzEOEE"); return aChallenge; } @@ -91,7 +91,7 @@ public static ChallengeDefinitionData postMissingTokenAuth401(int challengeOrder "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","https://www.eviltester.com/apichallenges/howto/35-36-post-unauthorised-401-403/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/authorization/post-secret-note-401-403"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "76U5TEvLzLI"); return aChallenge; } @@ -104,7 +104,7 @@ public static ChallengeDefinitionData postInvalidTokenAuth401(int challengeOrder "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","https://www.eviltester.com/apichallenges/howto/35-36-post-unauthorised-401-403/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/authorization/post-secret-note-401-403"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "76U5TEvLzLI"); return aChallenge; } @@ -117,7 +117,7 @@ public static ChallengeDefinitionData getWithValidBearerToken200(int challengeOr "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","https://www.eviltester.com/apichallenges/howto/37-38-bearer-token-access/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/authorization/get-post-secret-note-bearer"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "8GsMTZxEItw"); return aChallenge; } @@ -130,7 +130,7 @@ public static ChallengeDefinitionData postUpdateWithValidBearerToken200(int chal "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","https://www.eviltester.com/apichallenges/howto/37-38-bearer-token-access/"); + 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 index 677ddbaa..fe4e5dea 100644 --- 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 @@ -12,7 +12,7 @@ public static ChallengeDefinitionData methodNotAllowed405UsingDelete(int challen "DELETE /heartbeat (405)", "Issue a DELETE request on the `/heartbeat` end point and receive 405 (Method Not Allowed)"); - aChallenge.addSolutionLink("Read Solution", "HREF","https://www.eviltester.com/apichallenges/howto/25-26-27-28-status-codes-405-500-501-204/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/status-codes/status-codes-405-500-501-204"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); return aChallenge; } @@ -24,7 +24,7 @@ public static ChallengeDefinitionData serverError500UsingPatch(int challengeOrde "PATCH /heartbeat (500)", "Issue a PATCH request on the `/heartbeat` end point and receive 500 (internal server error)"); - aChallenge.addSolutionLink("Read Solution", "HREF","https://www.eviltester.com/apichallenges/howto/25-26-27-28-status-codes-405-500-501-204/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/status-codes/status-codes-405-500-501-204"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); return aChallenge; } @@ -36,7 +36,7 @@ public static ChallengeDefinitionData notImplemented501UsingTrace(int challengeO "TRACE /heartbeat (501)", "Issue a TRACE request on the `/heartbeat` end point and receive 501 (Not Implemented)"); - aChallenge.addSolutionLink("Read Solution", "HREF","https://www.eviltester.com/apichallenges/howto/25-26-27-28-status-codes-405-500-501-204/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/status-codes/status-codes-405-500-501-204"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); return aChallenge; } @@ -48,7 +48,7 @@ public static ChallengeDefinitionData noContent204UsingGet(int 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","https://www.eviltester.com/apichallenges/howto/25-26-27-28-status-codes-405-500-501-204/"); + aChallenge.addSolutionLink("Read Solution", "HREF","/apichallenges/solutions/status-codes/status-codes-405-500-501-204"); aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "SGfKVFdylVI"); return aChallenge; } 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 index d26dbd2a..82124204 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/ChallengerWebGUI.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/gui/ChallengerWebGUI.java @@ -16,8 +16,10 @@ import uk.co.compendiumdev.thingifier.htmlgui.DefaultGUIHTML; import java.io.*; +import java.lang.reflect.Array; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -43,7 +45,8 @@ public void setup(final Challengers challengers, guiManagement.appendMenuItem("Challenges", "/gui/challenges"); guiManagement.removeMenuItem("Home"); guiManagement.prefixMenuItem("Home", "/"); - guiManagement.appendMenuItem("Learning", "/learning.html"); + guiManagement.appendMenuItem("API documentation","/docs"); + guiManagement.appendMenuItem("Learning", "/learning"); guiManagement.setHomePageContent("

Challenges

\n" + "

The challenges can be completed by issuing API requests to the API.

\n" + @@ -261,28 +264,42 @@ public void setup(final Challengers challengers, }else{ - String[] breadcrumbs = contentPath.split("/"); + String[] breadcrumbs = Arrays.stream( + contentPath.split("/")). + filter(item -> item != null && !"".equals(item) + ).toArray(String[]::new); + StringBuilder bcHeader = new StringBuilder(); bcHeader.append("\n"); String bcPath =""; + int linksInBreadcrumb=0; if(breadcrumbs.length>0){ bcHeader.append("> "); - } - for(String bc : breadcrumbs){ - bcPath = bcPath + bc; - if(!bc.isEmpty()) { - if(contentPath.endsWith(bc)){ - bcHeader.append( bc ); - }else { - // if there is an index file then show the breadcrumb - if(getResourceAsStream(contentFolder + bcPath + ".md")!=null) { - bcHeader.append(String.format(" [%s](%s) > ", bc, bcPath)); + + for(String bc : breadcrumbs){ + bcPath = bcPath + bc; + if(!bc.isEmpty()) { + if(contentPath.endsWith(bc)){ + bcHeader.append( bc ); + }else { + // if there is an index file then show the breadcrumb + if(getResourceAsStream(contentFolder + "/" + bcPath + ".md")!=null) { + linksInBreadcrumb++; + bcHeader.append(String.format(" [%s](%s) > ", bc, "/" + bcPath)); + } } } + bcPath = bcPath + "/"; } - bcPath = bcPath + "/"; + bcHeader.append("\n"); + } + + if(linksInBreadcrumb==0){ + // do not output the breadcrumb + bcHeader = new StringBuilder(); } - bcHeader.append("\n"); + + @@ -356,6 +373,7 @@ public void setup(final Challengers challengers, StringBuilder html = new StringBuilder(); html.append(guiManagement.getPageStart(pageTitle,"")); html.append(guiManagement.getMenuAsHTML()); + html.append(dropDownMenuAsSummary()); html.append(renderer.render(document)); html.append(guiManagement.getPageFooter()); html.append(guiManagement.getPageEnd()); @@ -370,6 +388,22 @@ public void setup(final Challengers challengers, } + private String dropDownMenuAsSummary(){ + return + """ +
+ Learn About... +
    +
  1. How to Learn APIs
  2. +
  3. Mirror Mode
  4. +
  5. Simulation Mode
  6. +
  7. Challenge Solutions
  8. +
  9. Our Sponsors
  10. +
+
+ """; + } + private String processMacrosInContentLine(String line) { if(!line.contains("{{<")) @@ -542,9 +576,13 @@ private String renderChallengeData(final List reportOn) for(ChallengeHint hint : challenge.hints){ hintHtml = hintHtml + "
  • " + hint.hintText; if(hint.hintLink!=null && hint.hintLink.length()>0){ + String target="target='_blank'"; + if(!hint.hintLink.startsWith("http")){ + target=""; + } hintHtml = hintHtml + - String.format(" Learn More", - hint.hintLink); + String.format(" Learn More", + hint.hintLink, target); } hintHtml = hintHtml + "
  • "; } diff --git a/challenger/src/main/resources/content/learning.md b/challenger/src/main/resources/content/learning.md index e2631f33..75f856dd 100644 --- a/challenger/src/main/resources/content/learning.md +++ b/challenger/src/main/resources/content/learning.md @@ -8,7 +8,7 @@ title: Learning Utilities and Resources The Mirror mode is a good way to test out your tooling and see the details of your requests without using a proxy. -[Access the Mirror Mode Here](/mirror.html) +[Learn About the Mirror Mode Here](/practice-modes/mirror) ## Simulation Mode @@ -20,7 +20,7 @@ The simulator is stateless and does not track your usage, making it deterministi The simulator is a good place to get started because it will respond nicely... unless you mess up the request syntax. -[Access the Simulator Here](/simulation.html) +[Learn About the Simulator Here](/practice-modes/simulation) ## Automating and Testing a REST API Book @@ -30,9 +30,14 @@ Buying the book helps support this web site and application. ## Challenge Tutorials -All of the Challenges have solution tutorials that are available on the [EvilTester.com](https://eviltester.com) blog. +The Challenges have solution tutorials. -[Read the API Challenges Posts](https://www.eviltester.com/categories/api-challenges/) +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 diff --git a/challenger/src/main/resources/content/practice-modes/mirror.md b/challenger/src/main/resources/content/practice-modes/mirror.md new file mode 100644 index 00000000..00cabeab --- /dev/null +++ b/challenger/src/main/resources/content/practice-modes/mirror.md @@ -0,0 +1,159 @@ +--- +title: API Challenges Mirror Mode +--- + +# Mirror Mode + +{{}} + +[Patreon ad free video](https://www.patreon.com/posts/54382928) + +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. + +## Request EndPoint + +e.g. + +``` +GET https://apichallenges.eviltester.com/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 http://apichallenges.eviltester.com/mirror/request\n\nQuery Params\n==..."} +``` + + +## Raw EndPoint + +e.g. + +``` +GET https://apichallenges.eviltester.com/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 https://apichallenges.eviltester.com/mirror/raw + +Query Params +============ + +IP +======= +127.0.0.1 + +Raw Headers +======= +Accept: application/json +Content-Length: 2 +Host: localhost:4567 + +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 Usefyl? + +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. + + + \ No newline at end of file diff --git a/challenger/src/main/resources/content/practice-modes/simulation.md b/challenger/src/main/resources/content/practice-modes/simulation.md new file mode 100644 index 00000000..8390a318 --- /dev/null +++ b/challenger/src/main/resources/content/practice-modes/simulation.md @@ -0,0 +1,113 @@ +--- +title: API Challenges Simulation Mode +--- + +# Simulation Mode + +The API has a simulation mode, it uses hard coded data in responses, but tries to mimic some conditions. + +{{}} + + +[Patreon ad free video](https://www.patreon.com/posts/54383023) + +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. 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. +* etc. there are 'inconsistencies' but they are logical based on the needs of a stateless simulator. Use the actual API that underpins the challenges 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. + +GET https://CURRENTHOST/sim/entities (200) + +* Entities 1-10 +* Get all the entities in the simulator + +GET https://CURRENTHOST/sim/entities/1 (200) + +* Return entity number 1... try any of the entities listed +* Entities 1-8 are suitable for getting, 9 and 10 are for deletes and amendments so you may not get the response you are expecting + +GET https://CURRENTHOST/sim/entities/404 (404) + +* Entity does not exist, receive a 404 response + +POST https://CURRENTHOST/sim/entities (201) + +* Create an entity...note we assume you are creating with the payload below, because that is what we return. Creates an entity that is not listed in the /entities list, but it will be returned by GET to keep consistent with the location header. +* Will create Entity with ID 11 +* Entity 11 - Get will work for this but it will not show in the entities list. It will appear in the location header + + {"name": "bob"} + +POST https://CURRENTHOST/sim/entities/10 (200) + +* Amend an entity...note we assume you are amending to the payload below, because that is what we return. Creates an entity that is not listed in the /entities list, but it will be returned by GET to keep consistent with the location header. +* Will amend Entity with ID 10, once you amend you can GET this item and check it has amended +* Entity 10 - Get will show the amendment, but the list view will show the original name "entity number 10" + + {"name": "eris"} + +PUT https://CURRENTHOST/sim/entities/id (200) + +* Amend an entity...note we assume you are amending to the payload below, because that is what we return. Creates an entity that is not listed in the /entities list, but it will be returned by GET to keep consistent with the location header. +* Can amend Entity with ID 10, once you amend you can GET this item and check it has amended +* Entity 10 - Get will show the amendment, but the list view will show the original name "entity number 10" + + {"name": "eris"} + +* Can also create entity with id 11 + + {"name": "bob"} + +DELETE https://CURRENTHOST/sim/entities/id (204) + +* the only entity you can delete is id 9 +* if you GET id 9 then you will find it 404's + +## Other things to try: + +* Try malformed XML and JSON for POST and PUT +* Try amending the Content-Type to XML and passing in `bob` +* Try accept `application/xml` and `application/json` +* Delete an entity listed in the /entities call is forbidden id < 9 +* POST/PUT an entity listed in the /entities call is forbidden id < 10 +* PATCH and TRACE should be 501 Unimplemented for all end endpoints +* other /sim/* endpoints should 404 + + + + + +## 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/public/gui/multiuser.html b/challenger/src/main/resources/public/gui/multiuser.html index 1f96dfdd..140bf72f 100644 --- a/challenger/src/main/resources/public/gui/multiuser.html +++ b/challenger/src/main/resources/public/gui/multiuser.html @@ -8,7 +8,7 @@
    - +

    Multi-User Help

    diff --git a/challenger/src/main/resources/public/index.html b/challenger/src/main/resources/public/index.html index fefac40e..366c5bde 100644 --- a/challenger/src/main/resources/public/index.html +++ b/challenger/src/main/resources/public/index.html @@ -11,7 +11,7 @@
    - + - -

    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.

    -

    Request EndPoint

    -

    e.g.

    -
    GET https://apichallenges.herokuapp.com/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.

    -

    Raw EndPoint

    -

    e.g.

    -
    GET https://apichallenges.herokuapp.com/mirror/raw
    -

    Will return 200.

    -

    The mirror/raw endpoint will not use the Accept header to format the response and will always return a text representation.

    -

    Example

    -

    e.g.

    -
    GET http://apichallenges.herokuapp.com/mirror/request
    -
    -Query Params
    -============
    -
    -IP
    -=======
    -0:0:0:0:0:0:0:1
    -
    -Headers
    -=======
    -Accept: */*
    -Content-Length: 0
    -Host: localhost:4567
    -User-Agent: insomnia/2021.2.2
    -
    -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.

    - - - - - - - - -

     


    - -
    - - diff --git a/challenger/src/main/resources/public/simulation.html b/challenger/src/main/resources/public/simulation.html deleted file mode 100644 index 51819508..00000000 --- a/challenger/src/main/resources/public/simulation.html +++ /dev/null @@ -1,184 +0,0 @@ - - - - Simulation Mode - - - - -
    - - - - - -

    Simulation Mode

    - -

    Simulation Mode Overview

    - - - -

    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. 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.
    • -
    • etc. there are 'inconsistencies' but they are logical based on the needs of a stateless simulator. Use the actual API that underpins the challenges if you want a 'real' API.
    • -
    -

    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.

    -

    GET https://CURRENTHOST/sim/entities (200)

    -
      -
    • Entities 1-10
    • -
    • Get all the entities in the simulator
    • -
    -

    GET https://CURRENTHOST/sim/entities/1 (200)

    -
      -
    • Return entity number 1... try any of the entities listed
    • -
    • Entities 1-8 are suitable for getting, 9 and 10 are for deletes and amendments so you may not get the response you are expecting
    • -
    -

    GET https://CURRENTHOST/sim/entities/404 (404)

    -
      -
    • Entity does not exist, receive a 404 response
    • -
    -

    POST https://CURRENTHOST/sim/entities (201)

    -
      -
    • Create an entity...note we assume you are creating with the payload below, because that is what we return. Creates an entity that is not listed in the /entities list, but it will be returned by GET to keep consistent with the location header.
    • -
    • Will create Entity with ID 11
    • -
    • Entity 11 - Get will work for this but it will not show in the entities list. It will appear in the location header
    • -
    -
    {"name": "bob"}
    -

    POST https://CURRENTHOST/sim/entities/10 (200)

    -
      -
    • Amend an entity...note we assume you are amending to the payload below, because that is what we return. Creates an entity that is not listed in the /entities list, but it will be returned by GET to keep consistent with the location header.
    • -
    • Will amend Entity with ID 10, once you amend you can GET this item and check it has amended
    • -
    • Entity 10 - Get will show the amendment, but the list view will show the original name "entity number 10"
    • -
    -
    {"name": "eris"}
    -

    PUT https://CURRENTHOST/sim/entities/id (200)

    -
      -
    • Amend an entity...note we assume you are amending to the payload below, because that is what we return. Creates an entity that is not listed in the /entities list, but it will be returned by GET to keep consistent with the location header.
    • -
    • Can amend Entity with ID 10, once you amend you can GET this item and check it has amended
    • -
    • Entity 10 - Get will show the amendment, but the list view will show the original name "entity number 10"
    • -
    -
    {"name": "eris"}
    -
      -
    • Can also create entity with id 11

      -
      {"name": "bob"}
      -
    • -
    -

    DELETE https://CURRENTHOST/sim/entities/id (204)

    -
      -
    • the only entity you can delete is id 9
    • -
    • if you GET id 9 then you will find it 404's
    • -
    -

    Other things to try:

    -
      -
    • Try malformed XML and JSON for POST and PUT
    • -
    • Try amending the Content-Type to XML and passing in <entity><name>bob</name></entity>
    • -
    • Try accept application/xml and application/json
    • -
    • Delete an entity listed in the /entities call is forbidden id < 9
    • -
    • POST/PUT an entity listed in the /entities call is forbidden id < 10
    • -
    • PATCH and TRACE should be 501 Unimplemented for all end endpoints
    • -
    • other /sim/* endpoints should 404
    • -
    - - - - - -

    Simulation Mode Walkthrough - Insomnia

    - - - -

    Simulation Mode Walkthrough - Postman

    - - - -

     


    - -
    - - diff --git a/challenger/src/test/java/uk/co/compendiumdev/uirouting/UiPagesAreReachableTest.java b/challenger/src/test/java/uk/co/compendiumdev/uirouting/UiPagesAreReachableTest.java index 6f549e74..abccaeb1 100644 --- a/challenger/src/test/java/uk/co/compendiumdev/uirouting/UiPagesAreReachableTest.java +++ b/challenger/src/test/java/uk/co/compendiumdev/uirouting/UiPagesAreReachableTest.java @@ -100,10 +100,10 @@ static Stream simplePageRoutingStatus(){ args.add(Arguments.of(200, "Challenges", "/gui/challenges/unkownchallenger")); // Additional Pages - args.add(Arguments.of(200, "Learning Utilities and Resources", "/learning.html")); + args.add(Arguments.of(200, "Learning Utilities and Resources", "/learning")); args.add(Arguments.of(200, "API Documentation", "/docs")); - args.add(Arguments.of(200, "HTTP Request Mirror", "/mirror.html")); - args.add(Arguments.of(200, "Simulation Mode", "/simulation.html")); + args.add(Arguments.of(200, "API Challenges Mirror Mode", "/practice-modes/mirror")); + args.add(Arguments.of(200, "API Challenges Simulation Mode", "/practice-modes/simulation")); return args.stream(); } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/htmlgui/DefaultGUIHTML.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/htmlgui/DefaultGUIHTML.java index 4015f7b7..febd785e 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/htmlgui/DefaultGUIHTML.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/htmlgui/DefaultGUIHTML.java @@ -18,6 +18,12 @@ public DefaultGUIHTML(){ } public void appendMenuItem(final String title, final String url) { + for(GuiMenuItem item : menuItems){ + if(item.menuTitle.equals(title) || item.url.equals(url)){ + // avoid adding duplicates + return; + } + } menuItems.add(new GuiMenuItem(title, url)); }