From c31847691c711e6383ce0a21d8ee940f2f9a1991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20Martinussen?= Date: Sat, 27 Nov 2021 15:50:07 +0100 Subject: [PATCH] Resolve "Write tests for the API in the javafx app Flashy" --- flashy/core/pom.xml | 9 ++ .../storage/RemoteCardDeckStorageTest.java | 105 ++++++++++++ .../groups2021/gr2141/TestAppState.java | 27 ---- flashy/pom.xml | 18 +++ flashy/report/pom.xml | 5 + flashy/rest/pom.xml | 30 +++- .../gr2141/restapi/FlashyModelService.java | 34 ++-- .../groups2021/gr2141/restserver/Server.java | 8 +- .../gr2141/restserver/ServerConfig.java | 39 +++-- flashy/rest/src/main/module-info.java | 9 ++ .../gr2141/restserver/ServerTest.java | 151 ++++++++++++++++++ flashy/rest/src/test/java/module-info.test | 16 ++ flashy/rest/src/test/resources/test-deck.json | 28 ++++ 13 files changed, 410 insertions(+), 69 deletions(-) create mode 100644 flashy/core/src/test/java/it1901/groups2021/gr2141/core/storage/RemoteCardDeckStorageTest.java delete mode 100644 flashy/fxui/src/test/java/it1901/groups2021/gr2141/TestAppState.java create mode 100644 flashy/rest/src/test/java/it1901/groups2021/gr2141/restserver/ServerTest.java create mode 100644 flashy/rest/src/test/java/module-info.test create mode 100644 flashy/rest/src/test/resources/test-deck.json diff --git a/flashy/core/pom.xml b/flashy/core/pom.xml index e36028f..19fcc8e 100644 --- a/flashy/core/pom.xml +++ b/flashy/core/pom.xml @@ -22,6 +22,15 @@ jackson-databind + + + com.github.tomakehurst + wiremock-jre8 + 2.27.2 + test + + + org.mockito diff --git a/flashy/core/src/test/java/it1901/groups2021/gr2141/core/storage/RemoteCardDeckStorageTest.java b/flashy/core/src/test/java/it1901/groups2021/gr2141/core/storage/RemoteCardDeckStorageTest.java new file mode 100644 index 0000000..9d35803 --- /dev/null +++ b/flashy/core/src/test/java/it1901/groups2021/gr2141/core/storage/RemoteCardDeckStorageTest.java @@ -0,0 +1,105 @@ +package it1901.groups2021.gr2141.core.storage; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.WireMockServer; +import it1901.groups2021.gr2141.core.models.CardDeck; +import it1901.groups2021.gr2141.core.models.Flashcard; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.givenThat; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +public class RemoteCardDeckStorageTest { + + private RemoteCardDeckStorage remoteCardDeckStorage; + private String exampleSerializedFlashcard; + private String exampleSerializedFlashCard2; + private String exampleSerializedCardDeck; + + private WireMockConfiguration config; + private WireMockServer wireMockServer; + + @BeforeEach + public void startWireMockServerAndSetup() { + config = WireMockConfiguration.wireMockConfig().port(8089); + wireMockServer = new WireMockServer(config.portNumber()); + wireMockServer.start(); + WireMock.configureFor("localhost", config.portNumber()); + remoteCardDeckStorage = new RemoteCardDeckStorage(URI.create(String.format("http://%s:%s", "localhost", wireMockServer.port()))); + + exampleSerializedFlashcard = "[{\"front\": {\"lines\": [\"TestCard1Front\"]},\"back\": {\"lines\": [\"TestCard1Back\"]}}"; + exampleSerializedFlashCard2 = ",{\"front\": {\"lines\": [\"TestCard2Front\"]},\"back\": {\"lines\": [\"TestCard2Back\"]}}"; + exampleSerializedCardDeck = "{ \"flashcards\": " + exampleSerializedFlashcard + exampleSerializedFlashCard2 + "]}"; + } + + @Test + public void testWriteDeckWithNullInput() { + assertThrows(IllegalArgumentException.class, () -> remoteCardDeckStorage.setServerAddress(null)); + assertThrows(IllegalArgumentException.class, () -> remoteCardDeckStorage.writeDeck(null)); + assertThrows(IllegalArgumentException.class, () -> remoteCardDeckStorage.writeDeck(null, 0)); + } + + @Test + public void testWriteWithWrongInput() { + assertThrows(IOException.class, () -> remoteCardDeckStorage.writeDeck(new CardDeck())); + assertThrows(IOException.class, () -> remoteCardDeckStorage.writeDeck(new CardDeck(), 0)); + } + + @Test + public void testReadDeckWithIndex() throws IOException { + givenThat(get(urlEqualTo("/flashy/deck/" + 0)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(exampleSerializedCardDeck) + ) + ); + + List flashCards = remoteCardDeckStorage.readDeck(0).getFlashcards(); + + assertEquals(flashCards.size(), 2); + + assertEquals("TestCard1Front", flashCards.get(0).getFront().getText()); + assertEquals("TestCard1Back", flashCards.get(0).getBack().getText()); + assertEquals("TestCard2Front", flashCards.get(1).getFront().getText()); + assertEquals("TestCard2Back", flashCards.get(1).getBack().getText()); + } + + @Test + public void testReadAllDecks() throws IOException { + givenThat(get(urlEqualTo("/flashy/deck/")) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(String.format("[%s,%s]", exampleSerializedCardDeck, exampleSerializedCardDeck)) + ) + ); + + List cardDecksList = remoteCardDeckStorage.readAllDecks(); + + assertEquals(2, cardDecksList.size()); + assertEquals(2, cardDecksList.get(0).getNumberOfCards()); + assertEquals(2, cardDecksList.get(1).getNumberOfCards()); + + assertEquals("TestCard1Front", cardDecksList.get(0).getFlashcards().get(0).getFront().getText()); + assertEquals("TestCard1Back", cardDecksList.get(0).getFlashcards().get(0).getBack().getText()); + assertEquals("TestCard1Front", cardDecksList.get(1).getFlashcards().get(0).getFront().getText()); + assertEquals("TestCard1Back", cardDecksList.get(1).getFlashcards().get(0).getBack().getText()); + } + + @AfterEach + public void stopWireMockServer() { + wireMockServer.stop(); + } +} diff --git a/flashy/fxui/src/test/java/it1901/groups2021/gr2141/TestAppState.java b/flashy/fxui/src/test/java/it1901/groups2021/gr2141/TestAppState.java deleted file mode 100644 index 4b6e013..0000000 --- a/flashy/fxui/src/test/java/it1901/groups2021/gr2141/TestAppState.java +++ /dev/null @@ -1,27 +0,0 @@ -// package it1901.groups2021.gr2141; - -// import static org.junit.jupiter.api.Assertions.assertEquals; -// import static org.junit.jupiter.api.Assertions.assertThrows; - -// import java.util.List; - -// import org.junit.jupiter.api.Order; -// import org.junit.jupiter.api.Test; - -// import it1901.groups2021.gr2141.core.domainlogic.FlashcardProvider; -// import it1901.groups2021.gr2141.core.models.CardDeck; -// import it1901.groups2021.gr2141.core.models.Flashcard; -// import it1901.groups2021.gr2141.state.LocalAppState; - -// public class TestAppState { - -// private static FlashcardProvider exampleFlashcardProvider = new FlashcardProvider(new CardDeck(List.of(Flashcard.of("a", "b")))); -// private static final LocalAppState appState = new LocalAppState(); - -// @Test -// public void testSetGetFlashcardProvider() { -// assertThrows(IllegalArgumentException.class, () -> appState.setFlashcardProvider(null)); -// appState.setFlashcardProvider(exampleFlashcardProvider); -// assertEquals(exampleFlashcardProvider, appState.getFlashcardProvider()); -// } -// } diff --git a/flashy/pom.xml b/flashy/pom.xml index 23e2b92..b917d57 100644 --- a/flashy/pom.xml +++ b/flashy/pom.xml @@ -113,6 +113,24 @@ jersey-container-grizzly2-http ${jersey.version} + + org.glassfish.jersey.test-framework + jersey-test-framework-core + ${jersey.version} + test + + + org.glassfish.jersey.test-framework + jersey-test-framework-util + ${jersey.version} + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + ${jersey.version} + test + diff --git a/flashy/report/pom.xml b/flashy/report/pom.xml index f0157fb..aeedc2c 100644 --- a/flashy/report/pom.xml +++ b/flashy/report/pom.xml @@ -37,6 +37,11 @@ fxui ${project.parent.version} + + ${project.parent.groupId} + rest + ${project.parent.version} + diff --git a/flashy/rest/pom.xml b/flashy/rest/pom.xml index 013a56f..621cbc1 100644 --- a/flashy/rest/pom.xml +++ b/flashy/rest/pom.xml @@ -11,7 +11,7 @@ REST API ${project.parent.version} - A REST server providing an API to interact with the backend of the application. + A REST server providing an API to interact with the backend of the application. @@ -57,7 +57,30 @@ jersey-container-grizzly2-http + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-engine + + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + + + org.glassfish.jersey.test-framework + jersey-test-framework-util + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + + + @@ -76,6 +99,11 @@ it1901.groups2021.gr2141.restserver.Server + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + \ No newline at end of file diff --git a/flashy/rest/src/main/java/it1901/groups2021/gr2141/restapi/FlashyModelService.java b/flashy/rest/src/main/java/it1901/groups2021/gr2141/restapi/FlashyModelService.java index 631ed0a..e935071 100644 --- a/flashy/rest/src/main/java/it1901/groups2021/gr2141/restapi/FlashyModelService.java +++ b/flashy/rest/src/main/java/it1901/groups2021/gr2141/restapi/FlashyModelService.java @@ -1,13 +1,10 @@ package it1901.groups2021.gr2141.restapi; -import it1901.groups2021.gr2141.core.domainlogic.FlashcardProvider; import it1901.groups2021.gr2141.core.models.CardDeck; -import it1901.groups2021.gr2141.core.storage.CardDeckStorage; import it1901.groups2021.gr2141.core.storage.LocalCardDeckStorage; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; @@ -25,39 +22,29 @@ import java.util.List; @Path("/flashy/deck") @Produces(MediaType.APPLICATION_JSON) public class FlashyModelService { - private static String path = "/flashy/deck"; - private LocalCardDeckStorage localCardDeckStorage = new LocalCardDeckStorage(); + + public static String path = "/flashy/deck"; + + @Context + private LocalCardDeckStorage localCardDeckStorage; @GET - public List getCurrentCardDeck() throws IOException { - System.out.println("GET [" + path + "]"); - try { - var deck = localCardDeckStorage.readAllDecks(); - return deck; - } catch (IOException e) { - System.err.println(e); - } - return null; + public List getAllDecks() throws IOException { + var deck = localCardDeckStorage.readAllDecks(); + return deck; } @GET @Path("/{index}") public CardDeck getCurrentCardDeck(@PathParam("index") String index) throws IOException { - System.out.println("GET [" + path + "/" + index + "]"); - try { - var deck = localCardDeckStorage.readDeck(Integer.parseInt(index)); - return deck; - } catch (IOException e) { - System.err.println(e); - } - return null; + var deck = localCardDeckStorage.readDeck(Integer.parseInt(index)); + return deck; } @POST @Path("/{index}") @Consumes(MediaType.APPLICATION_JSON) public boolean saveCurrentCardDeck(CardDeck deck, @PathParam("index") String index) { - System.out.println("POST [" + path + "/" + index + "]"); try { localCardDeckStorage.writeDeck(deck, Integer.parseInt(index)); return true; @@ -70,7 +57,6 @@ public class FlashyModelService { @Path("/new") @Consumes(MediaType.APPLICATION_JSON) public boolean saveCurrentCardDeck(CardDeck deck) { - System.out.println("POST [" + path + "/add]"); try { localCardDeckStorage.writeDeck(deck); return true; diff --git a/flashy/rest/src/main/java/it1901/groups2021/gr2141/restserver/Server.java b/flashy/rest/src/main/java/it1901/groups2021/gr2141/restserver/Server.java index 732c158..a009693 100644 --- a/flashy/rest/src/main/java/it1901/groups2021/gr2141/restserver/Server.java +++ b/flashy/rest/src/main/java/it1901/groups2021/gr2141/restserver/Server.java @@ -2,6 +2,8 @@ package it1901.groups2021.gr2141.restserver; import org.glassfish.jersey.server.ResourceConfig; +import it1901.groups2021.gr2141.core.storage.LocalCardDeckStorage; + import java.io.IOException; import java.net.URI; import java.util.List; @@ -14,8 +16,10 @@ import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; * The main class of the REST server. */ public class Server { + private static String address = "localhost"; private static String port = "8080"; + public static final LocalCardDeckStorage localCardDeckStorage = new LocalCardDeckStorage(); private static void parseArgs(List args) { if (args.contains("-a")) { @@ -37,8 +41,8 @@ public class Server { return URI.create(String.format("http://%s:%s/", address, port)); } - private static HttpServer startServer() { - final ResourceConfig rc = new ServerConfig(); + public static HttpServer startServer() { + final ResourceConfig rc = new ServerConfig(localCardDeckStorage); return GrizzlyHttpServerFactory.createHttpServer(makeURI(), rc); } diff --git a/flashy/rest/src/main/java/it1901/groups2021/gr2141/restserver/ServerConfig.java b/flashy/rest/src/main/java/it1901/groups2021/gr2141/restserver/ServerConfig.java index 363510f..c741ff3 100644 --- a/flashy/rest/src/main/java/it1901/groups2021/gr2141/restserver/ServerConfig.java +++ b/flashy/rest/src/main/java/it1901/groups2021/gr2141/restserver/ServerConfig.java @@ -1,16 +1,10 @@ package it1901.groups2021.gr2141.restserver; -import java.util.List; - -import com.fasterxml.jackson.databind.ObjectMapper; import org.glassfish.jersey.internal.inject.AbstractBinder; import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.server.ResourceConfig; -import it1901.groups2021.gr2141.core.domainlogic.FlashcardProvider; -import it1901.groups2021.gr2141.core.models.CardDeck; -import it1901.groups2021.gr2141.core.models.Flashcard; -import it1901.groups2021.gr2141.core.models.serializers.ModelSerializingModule; +import it1901.groups2021.gr2141.core.storage.LocalCardDeckStorage; import it1901.groups2021.gr2141.restapi.FlashyModelService; /** @@ -20,12 +14,27 @@ import it1901.groups2021.gr2141.restapi.FlashyModelService; */ public class ServerConfig extends ResourceConfig { - /** - * @see ServerConfig - */ - public ServerConfig() { - register(JacksonFeature.class); - register(JsonMiddleware.class); - register(FlashyModelService.class); - } + private LocalCardDeckStorage localCardDeckStorage; + + + public ServerConfig(LocalCardDeckStorage localCardDeckStorage) { + setLocalCardDeckStorage(localCardDeckStorage); + register(JacksonFeature.class); + register(JsonMiddleware.class); + register(FlashyModelService.class); + register(new AbstractBinder() { + @Override + protected void configure() { + bind(ServerConfig.this.localCardDeckStorage); + } + }); + } + + protected LocalCardDeckStorage getLocalCardDeckStorage() { + return this.localCardDeckStorage; + } + + protected void setLocalCardDeckStorage(LocalCardDeckStorage localCardDeckStorage) { + this.localCardDeckStorage = localCardDeckStorage; + } } diff --git a/flashy/rest/src/main/module-info.java b/flashy/rest/src/main/module-info.java index 5ad5875..963637c 100644 --- a/flashy/rest/src/main/module-info.java +++ b/flashy/rest/src/main/module-info.java @@ -3,6 +3,15 @@ */ module it1901.groups2021.gr2141.rest { requires it1901.groups2021.gr2141.core; + requires jakarta.ws.rs; + + requires jersey.common; + requires jersey.server; + requires jersey.media.json.jackson; + + requires org.glassfish.hk2.api; + + opens it1901.groups2021.gr2141.restapi to jersey.server; exports it1901.groups2021.gr2141.restserver; } \ No newline at end of file diff --git a/flashy/rest/src/test/java/it1901/groups2021/gr2141/restserver/ServerTest.java b/flashy/rest/src/test/java/it1901/groups2021/gr2141/restserver/ServerTest.java new file mode 100644 index 0000000..db489bf --- /dev/null +++ b/flashy/rest/src/test/java/it1901/groups2021/gr2141/restserver/ServerTest.java @@ -0,0 +1,151 @@ +package it1901.groups2021.gr2141.restserver; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import it1901.groups2021.gr2141.core.models.CardContent; +import it1901.groups2021.gr2141.core.models.CardDeck; +import it1901.groups2021.gr2141.core.models.Flashcard; +import it1901.groups2021.gr2141.core.models.serializers.ModelSerializingModule; +import it1901.groups2021.gr2141.core.storage.LocalCardDeckStorage; +import it1901.groups2021.gr2141.restapi.FlashyModelService; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.glassfish.jersey.test.JerseyTest; + +public class ServerTest extends JerseyTest { + + private ObjectMapper objectMapper = ModelSerializingModule.buildMapper(); + private LocalCardDeckStorage localCardDeckStorage; + private CardDeck exampleCardDeck; + private Flashcard card1; + private Flashcard card2; + + @TempDir + private static File tempDir; + + @Override + protected ServerConfig configure() { + localCardDeckStorage = new LocalCardDeckStorage(); + localCardDeckStorage.setUserDataDirectory(tempDir.toPath()); + final ServerConfig sc = new ServerConfig(localCardDeckStorage); + return sc; + } + + private static void moveTestDeckFileToTmpDir() throws IOException, URISyntaxException { + var testDeckResource = new File(ServerTest.class.getResource("/test-deck.json").toURI()).toPath(); + Files.copy(testDeckResource, tempDir.toPath().resolve("deck-0.json"), StandardCopyOption.REPLACE_EXISTING); + } + + @BeforeAll + public static void setupAll() throws IOException, URISyntaxException { + moveTestDeckFileToTmpDir(); + } + + @BeforeEach + public void setUpExampleDeck() { + card1 = new Flashcard(new CardContent(List.of("1", "2")), new CardContent(List.of("3"))); + card2 = new Flashcard(new CardContent(List.of("why")), new CardContent(List.of("por qué"))); + List exampleFlashcardList = List.of(card1, card2); + exampleCardDeck = new CardDeck(exampleFlashcardList); + } + + @Test + public void testGetCurrentCardDeck() { + Response response = target(FlashyModelService.path) + .path("0") + .request(MediaType.APPLICATION_JSON) + .get(); + assertEquals(200, response.getStatus()); + + try { + CardDeck cardDeck = objectMapper.readValue(response.readEntity(String.class), CardDeck.class); + assertEquals(2, cardDeck.getFlashcards().size()); + + List flashCards = cardDeck.getFlashcards(); + + assertEquals("front1", flashCards.get(0).getFront().getText()); + assertEquals("back1", flashCards.get(0).getBack().getText()); + assertEquals("front2", flashCards.get(1).getFront().getText()); + assertEquals("back2", flashCards.get(1).getBack().getText()); + } catch (JsonProcessingException e) { + System.out.println(e); + } + } + + @Test + public void testAddNewCardDeck() throws InterruptedException { + Entity cardDeckEntity = Entity.entity(exampleCardDeck, MediaType.APPLICATION_JSON); + + target(FlashyModelService.path) + .path("new") + .request(MediaType.APPLICATION_JSON) + .put(cardDeckEntity); + + Response response = target(FlashyModelService.path) + .request(MediaType.APPLICATION_JSON) + .get(); + assertEquals(200, response.getStatus()); + + try { + List cardDeckList = objectMapper.readValue(response.readEntity(String.class), new TypeReference>(){}); + assertEquals(2, cardDeckList.size()); + } catch (JsonProcessingException e) { + System.out.println(e); + } + } + + @Test + public void testAddNewFlashCardsToExistingDeck() { + Response response = target(FlashyModelService.path) + .path("0") + .request(MediaType.APPLICATION_JSON) + .get(); + assertEquals(200, response.getStatus()); + + CardDeck cardDeck = new CardDeck(); + try { + cardDeck = objectMapper.readValue(response.readEntity(String.class), CardDeck.class); + assertEquals(2, cardDeck.getNumberOfCards()); + } catch (JsonProcessingException e) { + System.out.println(e); + } + + cardDeck.add(card1); + cardDeck.add(card2); + Entity cardDeckEntity = Entity.entity(cardDeck, MediaType.APPLICATION_JSON); + + target(FlashyModelService.path) + .path("0") + .request(MediaType.APPLICATION_JSON) + .post(cardDeckEntity); + + Response response2 = target(FlashyModelService.path) + .path("0") + .request(MediaType.APPLICATION_JSON) + .get(); + assertEquals(200, response2.getStatus()); + + CardDeck updatedCardDeck = new CardDeck(); + try { + updatedCardDeck = objectMapper.readValue(response2.readEntity(String.class), CardDeck.class); + assertEquals(4, updatedCardDeck.getNumberOfCards()); + } catch (JsonProcessingException e) { + System.out.println(e); + } + } +} + diff --git a/flashy/rest/src/test/java/module-info.test b/flashy/rest/src/test/java/module-info.test new file mode 100644 index 0000000..4417418 --- /dev/null +++ b/flashy/rest/src/test/java/module-info.test @@ -0,0 +1,16 @@ +open module it1901.groups2021.gr2141.rest { + requires it1901.groups2021.gr2141.core; + requires jakarta.ws.rs; + + requires jersey.common; + requires jersey.server; + requires jersey.media.json.jackson; + + requires org.glassfish.hk2.api; + + requires org.junit.jupiter; + requires org.junit.jupiter.api; + requires org.glassfish.jersey.test; + + opens it1901.groups2021.gr2141.restapi to jersey.server; +} diff --git a/flashy/rest/src/test/resources/test-deck.json b/flashy/rest/src/test/resources/test-deck.json new file mode 100644 index 0000000..4a68883 --- /dev/null +++ b/flashy/rest/src/test/resources/test-deck.json @@ -0,0 +1,28 @@ +{ + "flashcards": [ + { + "front": { + "lines": [ + "front1" + ] + }, + "back": { + "lines": [ + "back1" + ] + } + }, + { + "front": { + "lines": [ + "front2" + ] + }, + "back": { + "lines": [ + "back2" + ] + } + } + ] +} \ No newline at end of file