Skip to content

Commit

Permalink
added more PUT challenges and a TOC to challenge list
Browse files Browse the repository at this point in the history
  • Loading branch information
eviltester committed Feb 25, 2024
1 parent b007cd5 commit 097778f
Show file tree
Hide file tree
Showing 9 changed files with 404 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@ public enum CHALLENGE {
POST_TODOS_INVALID_EXTRA_FIELD,
POST_MAX_OUT_TITILE_DESCRIPTION_LENGTH,
PUT_TODOS_400,
POST_TODOS_404;
POST_TODOS_404,
PUT_TODOS_FULL_200,
PUT_TODOS_PARTIAL_200, PUT_TODOS_MISSING_TITLE_400, PUT_TODOS_400_NO_AMEND_ID;
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,30 @@ public void run(final HttpApiRequest request, final InternalHttpResponse respons
}
}

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") &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ private String renderChallengeNumber(int challengeOrder){
return String.format("%02d", challengeOrder);
}

// TODO: refactor this into private methods to make it easier to re-order and manage

public ChallengeDefinitions(){

challengeData = new HashMap<>();
Expand All @@ -50,6 +52,7 @@ public ChallengeDefinitions(){
"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 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("Watch Insomnia Solution", "YOUTUBE", "tNGuZMQgHxw");
getStarted.addChallenge(aChallenge);
Expand Down Expand Up @@ -105,6 +108,9 @@ public ChallengeDefinitions(){
aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "1S5kpd8-xfM");
challengeOrder++;

getChallenges.addChallenge(getTodosFiltered200(challengeOrder));
challengeOrder++;

// HEAD
ChallengeSection headChallenges = new ChallengeSection("HEAD Challenges",
"A HEAD request, is like a GET request, but only returns the headers and status code.");
Expand All @@ -130,16 +136,7 @@ public ChallengeDefinitions(){
challengeOrder++;


aChallenge = createChallenge(CHALLENGE.GET_TODOS_FILTERED, 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.");
postCreateChallenges.addChallenge(aChallenge);
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", "https://www.eviltester.com/apichallenges/howto/get-todos-200-filter");
aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "G-sLuhyPMuw");
challengeOrder++;


aChallenge = createChallenge(CHALLENGE.POST_TODOS_BAD_DONE_STATUS, renderChallengeNumber(challengeOrder), "POST /todos (400) doneStatus",
"Issue a POST request to create a todo but fail validation on the `doneStatus` field");
Expand Down Expand Up @@ -230,6 +227,52 @@ public ChallengeDefinitions(){
//aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "feXdRpZ_tgs");
challengeOrder++;



// UPDATE wtih 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);

aChallenge = createChallenge(CHALLENGE.PUT_TODOS_FULL_200, 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.");
putUpdateChallenges.addChallenge(aChallenge);
// todo: create solution for PUT todos full 200 challenge
//aChallenge.addSolutionLink("Read Solution", "HREF", "https://www.eviltester.com/apichallenges/howto/post-todos-201");
//aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA");
challengeOrder++;

aChallenge = createChallenge(CHALLENGE.PUT_TODOS_PARTIAL_200, 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.");
putUpdateChallenges.addChallenge(aChallenge);
// todo: create solution for PUT todos partial 200 challenge
//aChallenge.addSolutionLink("Read Solution", "HREF", "https://www.eviltester.com/apichallenges/howto/post-todos-201");
//aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA");
challengeOrder++;

aChallenge = createChallenge(CHALLENGE.PUT_TODOS_MISSING_TITLE_400, 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.");
putUpdateChallenges.addChallenge(aChallenge);
// todo: create solution for PUT todos partial 200 challenge
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("Read Solution", "HREF", "https://www.eviltester.com/apichallenges/howto/post-todos-201");
//aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA");
// TODO: add solution text to summarise solution
challengeOrder++;

aChallenge = createChallenge(CHALLENGE.PUT_TODOS_400_NO_AMEND_ID, 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.");
putUpdateChallenges.addChallenge(aChallenge);
// 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("Read Solution", "HREF", "https://www.eviltester.com/apichallenges/howto/post-todos-201");
//aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "T0LFHwavsNA");
// TODO: add solution text to summarise solution
challengeOrder++;



// 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.");
Expand Down Expand Up @@ -612,6 +655,18 @@ public ChallengeDefinitions(){
}
}

private ChallengeDefinitionData getTodosFiltered200(int challengeOrder) {
ChallengeDefinitionData aChallenge = createChallenge(CHALLENGE.GET_TODOS_FILTERED, 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", "https://www.eviltester.com/apichallenges/howto/get-todos-200-filter");
aChallenge.addSolutionLink("Watch Insomnia Solution", "YOUTUBE", "G-sLuhyPMuw");
return aChallenge;
}

private ChallengeDefinitionData createChallenge(final CHALLENGE id,
final String orderId,
final String name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,29 @@ public class ChallengeSolutionLink {
public final String linkType;
public final String linkData;

public ChallengeSolutionLink(final String linkText, final String linkType, final String linkData) {
this.linkText = linkText;
this.linkType = linkType.toUpperCase();
this.linkData = 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;
}
return String.format("<a href='%s' target='_blank'>%s</a>",linkData, linkText);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,17 @@ private String renderChallengeData(final ChallengeDefinitions challengeDefinitio

final Collection<ChallengeSection> sections = challengeDefinitions.getChallengeSections();

// add a toc
html.append("<p id='toc'><strong>Sections</strong></p>");
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("<h2>" + section.getTitle() + "</h2>");
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<>();
Expand All @@ -429,6 +437,7 @@ private String renderChallengeData(final ChallengeDefinitions challengeDefinitio
}

html.append(renderChallengeData(sectionData));
html.append("<p><a href='#toc'>Back to Section List</a></p>");
}

return html.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -232,6 +233,105 @@ public void canPutTodos400FailCreatePass() {
Assertions.assertTrue(challenger.statusOfChallenge(CHALLENGE.PUT_TODOS_400));
}

@Test
public void canPutTodosFull200AmendPass() {

final EntityInstanceCollection todos = ChallengeMain.getChallenger().getThingifier().getThingInstancesNamed("todo", challenger.getXChallenger());

Map<String, String> x_challenger_header = getXChallengerHeader(challenger.getXChallenger());

Map<String, String> headers = new HashMap<>();
headers.putAll(x_challenger_header);
headers.put("Content-Type", "application/json");

EntityInstance aTodo = new ArrayList<>(todos.getInstances()).get(0);

// amend a todo 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 EntityInstanceCollection todos = ChallengeMain.getChallenger().getThingifier().getThingInstancesNamed("todo", challenger.getXChallenger());

Map<String, String> x_challenger_header = getXChallengerHeader(challenger.getXChallenger());

Map<String, String> headers = new HashMap<>();
headers.putAll(x_challenger_header);
headers.put("Content-Type", "application/json");

EntityInstance aTodo = new ArrayList<>(todos.getInstances()).get(0);

// amend a todo 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 EntityInstanceCollection todos = ChallengeMain.getChallenger().getThingifier().getThingInstancesNamed("todo", challenger.getXChallenger());

Map<String, String> x_challenger_header = getXChallengerHeader(challenger.getXChallenger());

Map<String, String> headers = new HashMap<>();
headers.putAll(x_challenger_header);
headers.put("Content-Type", "application/json");

EntityInstance aTodo = new ArrayList<>(todos.getInstances()).get(0);

// amend a todo 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 EntityInstanceCollection todos = ChallengeMain.getChallenger().getThingifier().getThingInstancesNamed("todo", challenger.getXChallenger());

Map<String, String> x_challenger_header = getXChallengerHeader(challenger.getXChallenger());

Map<String, String> headers = new HashMap<>();
headers.putAll(x_challenger_header);
headers.put("Content-Type", "application/json");

EntityInstance aTodo = new ArrayList<>(todos.getInstances()).get(0);

// amend a todo 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() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@
import io.restassured.response.Response;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import uk.co.compendiumdev.challenger.payloads.ErrorMessages;
import uk.co.compendiumdev.challenger.payloads.Todo;
import uk.co.compendiumdev.challenger.restassured.api.ChallengesStatus;
import uk.co.compendiumdev.challenger.restassured.api.RestAssuredBaseTest;
import uk.co.compendiumdev.challenger.restassured.api.TodosApi;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CanCreateTodosWithPOSTTest extends RestAssuredBaseTest {
public class CanPostCreateTodosTest extends RestAssuredBaseTest {

@Test
void canCreateATodoWithPost(){
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package uk.co.compendiumdev.challenger.restassured;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
Expand All @@ -15,7 +13,7 @@

import java.util.List;

public class CanUpdateTodosWithPOSTTest extends RestAssuredBaseTest {
public class CanPostUpdateTodosTest extends RestAssuredBaseTest {

@Test
void canUpdateATodoWithPost(){
Expand Down
Loading

0 comments on commit 097778f

Please sign in to comment.