From ca3cde129895ef2c8888782e672184cc8efd79c6 Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sat, 7 Nov 2020 22:21:16 +0700
Subject: [PATCH 01/10] Create API documentation in README.md (#5)

---
 README.md | 165 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 164 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 2546d20..f7199ac 100644
--- a/README.md
+++ b/README.md
@@ -6,4 +6,167 @@
 [![pipeline status](https://gitlab.cs.ui.ac.id/bukan-macan-ternak/liongame-backend/badges/master/pipeline.svg)](https://gitlab.cs.ui.ac.id/bukan-macan-ternak/liongame-backend/-/commits/master)
 [![coverage report](https://gitlab.cs.ui.ac.id/bukan-macan-ternak/liongame-backend/badges/master/coverage.svg)](https://gitlab.cs.ui.ac.id/bukan-macan-ternak/liongame-backend/-/commits/master)
 
-TBD.
+## Authorization
+
+The API uses simple authentication by verifying `Authorization` header. The
+value must match with `API_KEY` environment variable loaded by the backend.
+Every request must contain `Authorization` header and its value.
+
+Example request:
+
+```bash
+curl "https://example.com/api/logs" \
+  -H "Authorization: 00ff11ee22dd"
+```
+
+## Play Session Logs
+
+### Append
+
+Submits a single play session log.
+
+```endpoint
+POST /logs
+```
+
+Example request:
+
+```bash
+curl -X POST https://liongame.adian.to/api/logs \
+  -H "Authorization: {{ API_KEY }}"
+```
+
+Example request body:
+
+> Note: `//` comments are embedded for clarity. The JSON schema corresponds to
+> the columns in the actual spreadsheet.
+> **Do not actually include the comments in the actual JSON!**
+
+```json
+{
+  [
+    "VSWM try out",     // project_name
+    "Try Out",          // organisation_name
+    "Minggu 1 Jo",      // group_name
+    "1",                // participant_case_number
+    "Par1",             // participant_name
+    "",                 // participant_symbol
+    "",                 // participant_color
+    "LvTLiO",           // participant_token
+    "",                 // taak (?)
+    "5",                // #levels
+    "4",                // #items
+    "IND",              // language
+    "default_theme",    // theme
+    "16",               // matrix_size
+    "",                 // id
+    "60",               // task_id
+    "",                 // participant_id
+    "0",                // level
+    "1",                // item
+    "1",                // question_number
+    "red",              // correct_answer
+    "8",                // correct_matrix_number
+    "NA",               // response
+    "12",               // response_matrix_number (index position in 1-dimensional array representation)
+    "131037",           // response_time (in milliseconds)
+    "0",                // score
+    "",                 // created_at (sent from the game)
+    "",                 // updated_at (sent from the game)
+    "",                 // deleted_at
+  ]
+}
+```
+
+Example response:
+
+> `HTTP 200 OK`
+
+```json
+{
+    "status": 200,
+    "data": {
+        "participant_name": "{{ participant_name }}",
+        "created_at": "{{ created_at }}",
+    }
+}
+```
+
+### Batch Append
+
+Submits a list of play session logs.
+
+```endpoint
+POST /logs/batch
+```
+
+Example request:
+
+```bash
+curl -X POST https://liongame.adian.to/api/logs/batch \
+  -H "Authorization: {{ API_KEY }}"
+```
+
+Example request body:
+
+```json
+{
+  [
+    [
+      "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
+      "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "1", "1",
+      "red", "8", "NA", "12", "131037", "0", "", "", ""
+    ],
+    [
+      "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
+      "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "1", "2",
+      "blue", "3", "NA", "3", "62826", "1", "", "", ""
+    ],
+    [
+      "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
+      "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "2", "1",
+      "red", "9", "NA", "13", "32213", "0", "", "", ""
+    ]
+  ]
+}
+```
+
+Example response:
+
+> HTTP 200
+
+```json
+{
+    "participant_name": "{{ participant_name }}",
+    "created_at": "{{ created_at }}",               // Latest created_at timestamp
+}
+```
+
+## Errors Response
+
+The possible errors and their example responses are as follows:
+
+> `HTTP 400 Bad Request` caused by incorrect request body.
+
+```json
+{
+    "status": 400
+}
+```
+
+> `HTTP 403 Forbidden` caused by failed authorization.
+
+```json
+{
+    "status": 403
+}
+```
+
+> `HTTP 503 Service Unavailable` is possible due to connection issues to Google
+> Sheets API.
+
+```json
+{
+    "status": 503
+}
+```
-- 
GitLab


From 7679a84ba155eea2052c684e21c8b9b9588fecac Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sat, 7 Nov 2020 22:21:36 +0700
Subject: [PATCH 02/10] Tidy up files (#5)

---
 src/.env.example | 2 +-
 src/App.php      | 3 +--
 src/index.php    | 3 +--
 3 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/.env.example b/src/.env.example
index 79d0e6f..e289357 100644
--- a/src/.env.example
+++ b/src/.env.example
@@ -1,4 +1,4 @@
-API_KEY=sha512(bangtoyib-cepatpulang)
+API_KEY=sha256(bangtoyib-cepatpulang)
 DEBUG=true
 GOOGLE_APPLICATION_CREDENTIALS=/opt/liongame/service-account.json
 SPREADSHEET_ID=1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms
diff --git a/src/App.php b/src/App.php
index 7f698ce..3d7abd9 100644
--- a/src/App.php
+++ b/src/App.php
@@ -1,5 +1,4 @@
-<?php
-declare(strict_types=1);
+<?php declare(strict_types=1);
 
 namespace addianto\LionGame\Backend;
 
diff --git a/src/index.php b/src/index.php
index 6f99f83..fe9090b 100644
--- a/src/index.php
+++ b/src/index.php
@@ -1,5 +1,4 @@
-<?php
-declare(strict_types=1);
+<?php declare(strict_types=1);
 
 namespace addianto\LionGame\Backend;
 
-- 
GitLab


From a52023a547fb370952f0c1c40fce489e4544ad70 Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sat, 7 Nov 2020 15:35:52 +0000
Subject: [PATCH 03/10] Modify endpoint URL (#5)

---
 src/App.php       | 3 ++-
 tests/AppTest.php | 9 +++++----
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/App.php b/src/App.php
index 3d7abd9..5509410 100644
--- a/src/App.php
+++ b/src/App.php
@@ -42,8 +42,9 @@ class App
 
     private function setupController(): void
     {
-        $this->app->post("/logs", function (Request $request, Response $response, $args) {
+        $this->app->post("/api/logs", function (Request $request, Response $response, $args) {
             $response->getBody()->write(json_encode(array(
+                "status" => 200,
                 "message" => "TODO: Implement me!"
             )));
             return $response;
diff --git a/tests/AppTest.php b/tests/AppTest.php
index 3fb849c..c8636d0 100644
--- a/tests/AppTest.php
+++ b/tests/AppTest.php
@@ -38,23 +38,24 @@ class AppTest extends TestCase
         $this->assertInstanceOf(SlimApp::class, $this->app);
     }
 
-    public function testPOSTLogsShouldResolve(): void
+    public function testAppendLogShouldResolve() : void
     {
-        $request = $this->buildRequest("POST", "/logs");
+        $request = $this->buildRequest("POST", "/api/logs");
         $response = $this->app->handle($request);
 
         $this->assertEquals(200, $response->getStatusCode());
     }
 
-    public function testPOSTLogsShouldReturnJSON(): void
+    public function testAppendLogShouldReturnJSON(): void
     {
-        $request = $this->buildRequest("POST", "/logs");
+        $request = $this->buildRequest("POST", "/api/logs");
 
         $response = $this->app->handle($request);
         $jsonString = $response->getBody()->__toString();
         $json = json_decode($jsonString);
 
         $this->assertEquals(self::SUCCESS_JSON_DECODE, json_last_error());
+        $this->assertEquals(200, $json->status);
     }
 
     private function buildRequest(string $method, string $path)
-- 
GitLab


From 5e3fa79ed8cfa771096d39627cf66c12764b1633 Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sat, 7 Nov 2020 15:39:12 +0000
Subject: [PATCH 04/10] Add tests (#5)

---
 tests/AppTest.php | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/tests/AppTest.php b/tests/AppTest.php
index c8636d0..560fe41 100644
--- a/tests/AppTest.php
+++ b/tests/AppTest.php
@@ -38,7 +38,7 @@ class AppTest extends TestCase
         $this->assertInstanceOf(SlimApp::class, $this->app);
     }
 
-    public function testAppendLogShouldResolve() : void
+    public function testAppendLogShouldResolve(): void
     {
         $request = $this->buildRequest("POST", "/api/logs");
         $response = $this->app->handle($request);
@@ -58,6 +58,26 @@ class AppTest extends TestCase
         $this->assertEquals(200, $json->status);
     }
 
+    public function testBatchAppendLogShouldResolve(): void
+    {
+        $request = $this->buildRequest("POST", "/api/logs/batch");
+        $response = $this->app->handle($request);
+
+        $this->assertEquals(200, $response->getStatusCode());
+    }
+
+    public function testBatchAppendLogsShouldReturnJSON(): void
+    {
+        $request = $this->buildRequest("POST", "/api/logs/batch");
+
+        $response = $this->app->handle($request);
+        $jsonString = $response->getBody()->__toString();
+        $json = json_decode($jsonString);
+
+        $this->assertEquals(self::SUCCESS_JSON_DECODE, json_last_error());
+        $this->assertEquals(200, $json->status);
+    }
+
     private function buildRequest(string $method, string $path)
     {
         return RequestFactory::create()
-- 
GitLab


From 8d9fe834b181d35b96ddb07bb705b0967c670624 Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sat, 7 Nov 2020 17:10:29 +0000
Subject: [PATCH 05/10] Implement append log endpoint (#5)

---
 src/App.php       |  72 ++++++++++++++++++++++++---
 tests/AppTest.php | 122 +++++++++++++++++++++++++++++++++++++---------
 2 files changed, 164 insertions(+), 30 deletions(-)

diff --git a/src/App.php b/src/App.php
index 5509410..cf06e9c 100644
--- a/src/App.php
+++ b/src/App.php
@@ -8,9 +8,11 @@ use Slim\Factory\AppFactory;
 use Slim\App as SlimApp;
 use addianto\LionGame\Backend\Config\Config;
 use addianto\LionGame\Backend\Service\SheetsService;
+use count;
 
 class App
 {
+    const VALID_LENGTH = 29;
     private string $apiKey;
     private SlimApp $app;
     private SheetsService $sheets;
@@ -37,17 +39,75 @@ class App
 
     private function setupMiddleware(): void
     {
+        $this->app->addBodyParsingMiddleware();
         $this->app->addErrorMiddleware($this->debug, true, true);
     }
 
     private function setupController(): void
     {
-        $this->app->post("/api/logs", function (Request $request, Response $response, $args) {
-            $response->getBody()->write(json_encode(array(
-                "status" => 200,
-                "message" => "TODO: Implement me!"
-            )));
-            return $response;
+        $this->app->post("/api/logs", function (Request $request, Response $response, $args): Response {
+            if (!self::isAuthorized($request, $this->apiKey)) {
+                $response
+                    ->withStatus(403)
+                    ->getBody()->write(json_encode(array(
+                        "status" => 403,
+                    )));
+
+                return $response;
+            }
+
+            $data = $request->getParsedBody();
+
+            if (!self::isValidData($data)) {
+                $response
+                    ->withStatus(400)
+                    ->getBody()->write(json_encode(array(
+                        "status" => 400,
+                    )));
+
+                return $response;
+            }
+
+            if ($this->sheets->appendValues(
+                "'Play Session Analytics (Example)'A:AC",
+                $data
+            )) {
+                $response
+                    ->withStatus(200)
+                    ->getBody()->write(json_encode(array(
+                        "status" => 200,
+                        "data" => array(
+                            "participant_name" => $data[4],
+                            "created_at" => $data[26],
+                        ),
+                    )));
+
+                return $response;
+            } else {
+                $response
+                    ->withStatus(503)
+                    ->getBody()->write(json_encode(array(
+                        "status" => 503,
+                    )));
+
+                return $response;
+            }
         });
     }
+
+    private static function isAuthorized(Request $request, string $apiKey): bool
+    {
+        if (!$request->hasHeader("Authorization")) {
+            return false;
+        }
+
+        $headerValue = $request->getHeader("Authorization");
+
+        return $headerValue[0] == $apiKey;
+    }
+
+    private static function isValidData(array $data): bool
+    {
+        return count($data) == self::VALID_LENGTH;
+    }
 }
diff --git a/tests/AppTest.php b/tests/AppTest.php
index 560fe41..e17c437 100644
--- a/tests/AppTest.php
+++ b/tests/AppTest.php
@@ -23,12 +23,14 @@ class AppTest extends TestCase
     const SUCCESS_JSON_DECODE = 0;
 
     private SlimApp $app;
+    private SheetsService $sheets;
 
     protected function setUp(): void
     {
+        $this->sheets = $this->createMock(SheetsService::class);
         $this->app = (new App(
             self::API_KEY,
-            $this->createMock(SheetsService::class),
+            $this->sheets,
             self::DEBUG
         ))->get();
     }
@@ -38,51 +40,123 @@ class AppTest extends TestCase
         $this->assertInstanceOf(SlimApp::class, $this->app);
     }
 
-    public function testAppendLogShouldResolve(): void
+    public function testAppendLogShouldReturnJSON(): void
     {
-        $request = $this->buildRequest("POST", "/api/logs");
-        $response = $this->app->handle($request);
+        $this->sheets
+            ->method("appendValues")
+            ->willReturn(true);
+        $request = $this->buildRequest(
+            "POST",
+            "/api/logs",
+            self::API_KEY,
+            $this->getValidRequestBody()
+        );
+
+        $json = $this->getJSON($this->app->handle($request));
 
-        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals(200, $json->status);
     }
 
-    public function testAppendLogShouldReturnJSON(): void
+
+    public function testBatchAppendLogsShouldReturnJSON(): void
     {
-        $request = $this->buildRequest("POST", "/api/logs");
+        $request = $this->buildRequest(
+            "POST",
+            "/api/logs/batch",
+            self::API_KEY,
+            $this->getValidRequestBody()
+        );
 
-        $response = $this->app->handle($request);
-        $jsonString = $response->getBody()->__toString();
-        $json = json_decode($jsonString);
+        $json = $this->getJSON($this->app->handle($request));
 
-        $this->assertEquals(self::SUCCESS_JSON_DECODE, json_last_error());
         $this->assertEquals(200, $json->status);
+        $this->assertEquals("Par1", $json->data->participant_name);
     }
 
-    public function testBatchAppendLogShouldResolve(): void
+    public function testFailedAuthorizationOnAppendLog(): void
     {
-        $request = $this->buildRequest("POST", "/api/logs/batch");
-        $response = $this->app->handle($request);
+        $request = $this->buildRequest(
+            "POST",
+            "/api/logs",
+            "a",
+            $this->getValidRequestBody()
+        );
+
+        $json = $this->getJSON($this->app->handle($request));
 
-        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals(403, $json->status);
     }
 
-    public function testBatchAppendLogsShouldReturnJSON(): void
+    public function testInvalidDataOnAppendLog(): void
     {
-        $request = $this->buildRequest("POST", "/api/logs/batch");
+        $request = $this->buildRequest(
+            "POST",
+            "/api/logs",
+            self::API_KEY,
+            $this->getInvalidRequestBody()
+        );
 
-        $response = $this->app->handle($request);
-        $jsonString = $response->getBody()->__toString();
-        $json = json_decode($jsonString);
+        $json = $this->getJSON($this->app->handle($request));
 
-        $this->assertEquals(self::SUCCESS_JSON_DECODE, json_last_error());
-        $this->assertEquals(200, $json->status);
+        $this->assertEquals(400, $json->status);
     }
 
-    private function buildRequest(string $method, string $path)
+    public function testGoogleSheetsAPIUnavailableOnAppendLog(): void
     {
+        $this->sheets
+            ->method("appendValues")
+            ->willReturn(false);
+        $request = $this->buildRequest(
+            "POST",
+            "/api/logs",
+            self::API_KEY,
+            $this->getValidRequestBody()
+        );
+
+        $json = $this->getJSON($this->app->handle($request));
+
+        $this->assertEquals(503, $json->status);
+    }
+
+    private function buildRequest(
+        string $method,
+        string $path,
+        string $authorization,
+        array $body = []
+    ) {
         return RequestFactory::create()
             ->createServerRequestFromGlobals()
+            ->withHeader("Authorization", $authorization)
+            ->withHeader("Content-Type", "application/json")
             ->withMethod(strtoupper($method))
-            ->withUri(UriFactory::createUri($path));
+            ->withUri(UriFactory::createUri($path))
+            ->withParsedBody($body);
+    }
+
+    private function getJSON(Response $response): object
+    {
+        $jsonString = $response->getBody()->__toString();
+        $json = json_decode($jsonString);
+
+        $this->assertEquals(self::SUCCESS_JSON_DECODE, json_last_error());
+
+        return $json;
+    }
+
+    private function getValidRequestBody(): array
+    {
+        return [
+            "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
+            "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "1", "1",
+            "red", "8", "NA", "12", "131037", "0", "", "", ""
+        ];
+    }
+
+    private function getInvalidRequestBody(): array
+    {
+        return [
+            "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
+            "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "1", "1"
+        ];
     }
 }
-- 
GitLab


From ff4f970989026f6d046bd67c7c5f2b6195d3ff99 Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sat, 7 Nov 2020 17:36:49 +0000
Subject: [PATCH 06/10] Implement batch append log endpoint (#5)

---
 src/App.php                   | 68 ++++++++++++++++++++++++++---------
 src/service/SheetsService.php |  2 +-
 tests/AppTest.php             | 28 +++++++++++++--
 3 files changed, 79 insertions(+), 19 deletions(-)

diff --git a/src/App.php b/src/App.php
index cf06e9c..17858cb 100644
--- a/src/App.php
+++ b/src/App.php
@@ -9,6 +9,7 @@ use Slim\App as SlimApp;
 use addianto\LionGame\Backend\Config\Config;
 use addianto\LionGame\Backend\Service\SheetsService;
 use count;
+use json_encode;
 
 class App
 {
@@ -47,25 +48,49 @@ class App
     {
         $this->app->post("/api/logs", function (Request $request, Response $response, $args): Response {
             if (!self::isAuthorized($request, $this->apiKey)) {
-                $response
-                    ->withStatus(403)
-                    ->getBody()->write(json_encode(array(
-                        "status" => 403,
-                    )));
-
+                self::buildErrorResponse($response, 403);
                 return $response;
             }
 
             $data = $request->getParsedBody();
 
             if (!self::isValidData($data)) {
+                self::buildErrorResponse($response, 400);
+                return $response;
+            }
+
+            if ($this->sheets->appendValues(
+                "'Play Session Analytics (Example)'A:AC",
+                array($data)
+            )) {
                 $response
-                    ->withStatus(400)
+                    ->withStatus(200)
                     ->getBody()->write(json_encode(array(
-                        "status" => 400,
+                        "status" => 200,
+                        "data" => array(
+                            "participant_name" => $data[0][4],
+                            "created_at" => $data[0][26],
+                        ),
                     )));
 
                 return $response;
+            } else {
+                self::buildErrorResponse($response, 503);
+                return $response;
+            }
+        });
+
+        $this->app->post("/api/logs/batch", function (Request $request, Response $response, $args): Response {
+            if (!self::isAuthorized($request, $this->apiKey)) {
+                self::buildErrorResponse($response, 403);
+                return $response;
+            }
+
+            $data = $request->getParsedBody();
+
+            if (!self::isValidData($data)) {
+                self::buildErrorResponse($response, 400);
+                return $response;
             }
 
             if ($this->sheets->appendValues(
@@ -77,24 +102,26 @@ class App
                     ->getBody()->write(json_encode(array(
                         "status" => 200,
                         "data" => array(
-                            "participant_name" => $data[4],
-                            "created_at" => $data[26],
+                            "participant_name" => $data[count($data)-1][4],
+                            "created_at" => $data[count($data)-1][26],
                         ),
                     )));
 
                 return $response;
             } else {
-                $response
-                    ->withStatus(503)
-                    ->getBody()->write(json_encode(array(
-                        "status" => 503,
-                    )));
-
+                self::buildErrorResponse($response, 503);
                 return $response;
             }
         });
     }
 
+    private static function buildErrorResponse(Response $response, int $code): void
+    {
+        $response->withStatus($code)->getBody()->write(json_encode(array(
+            "status" => $code,
+        )));
+    }
+
     private static function isAuthorized(Request $request, string $apiKey): bool
     {
         if (!$request->hasHeader("Authorization")) {
@@ -108,6 +135,15 @@ class App
 
     private static function isValidData(array $data): bool
     {
+        if (is_array($data[0])) {
+            foreach ($data as $entry) {
+                if (count($entry) != self::VALID_LENGTH) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
         return count($data) == self::VALID_LENGTH;
     }
 }
diff --git a/src/service/SheetsService.php b/src/service/SheetsService.php
index 9c6a0c3..1db27f9 100644
--- a/src/service/SheetsService.php
+++ b/src/service/SheetsService.php
@@ -45,7 +45,7 @@ class SheetsService
         try {
             $body = new ValueRange([
                 "majorDimension" => "ROWS",
-                "values" => array($values),
+                "values" => $values,
             ]);
 
             $optionalParameters = array(
diff --git a/tests/AppTest.php b/tests/AppTest.php
index e17c437..6b8b936 100644
--- a/tests/AppTest.php
+++ b/tests/AppTest.php
@@ -57,14 +57,20 @@ class AppTest extends TestCase
         $this->assertEquals(200, $json->status);
     }
 
-
     public function testBatchAppendLogsShouldReturnJSON(): void
     {
+        $this->sheets
+            ->method("appendValues")
+            ->willReturn(true);
         $request = $this->buildRequest(
             "POST",
             "/api/logs/batch",
             self::API_KEY,
-            $this->getValidRequestBody()
+            array(
+                $this->getValidRequestBody(),
+                $this->getValidRequestBody(),
+                $this->getValidRequestBody(),
+            )
         );
 
         $json = $this->getJSON($this->app->handle($request));
@@ -101,6 +107,24 @@ class AppTest extends TestCase
         $this->assertEquals(400, $json->status);
     }
 
+    public function testInvalidDataOnBatchAppendLog(): void
+    {
+        $request = $this->buildRequest(
+            "POST",
+            "/api/logs",
+            self::API_KEY,
+            array(
+                $this->getValidRequestBody(),
+                $this->getInvalidRequestBody(),
+                $this->getValidRequestBody(),
+            )
+        );
+
+        $json = $this->getJSON($this->app->handle($request));
+
+        $this->assertEquals(400, $json->status);
+    }
+
     public function testGoogleSheetsAPIUnavailableOnAppendLog(): void
     {
         $this->sheets
-- 
GitLab


From f7cbc664ae3738999eb1591d395d3eba439b47e0 Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sun, 8 Nov 2020 00:41:01 +0700
Subject: [PATCH 07/10] Tidy source code (#5)

---
 src/service/SheetsService.php | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/service/SheetsService.php b/src/service/SheetsService.php
index 1db27f9..abb256a 100644
--- a/src/service/SheetsService.php
+++ b/src/service/SheetsService.php
@@ -25,8 +25,7 @@ class SheetsService
         $values = array();
 
         try {
-            $response = $this->api
-                ->spreadsheets_values
+            $response = $this->api->spreadsheets_values
                 ->get($this->spreadsheetId, $range);
 
             $values = $response->getValues();
@@ -53,8 +52,7 @@ class SheetsService
                 "valueInputOption" => "RAW",
             );
 
-            $response = $this->api
-                ->spreadsheets_values
+            $this->api->spreadsheets_values
                 ->append($this->spreadsheetId, $range, $body, $optionalParameters);
 
             $isAppended = true;
-- 
GitLab


From 3ef82d1be36b854667c548a5b070fbecbd208842 Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sun, 8 Nov 2020 03:06:50 +0700
Subject: [PATCH 08/10] Fix use statements (#5)

---
 src/index.php | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/index.php b/src/index.php
index fe9090b..884e1b9 100644
--- a/src/index.php
+++ b/src/index.php
@@ -4,6 +4,8 @@ namespace addianto\LionGame\Backend;
 
 use Dotenv\Dotenv;
 use addianto\LionGame\Backend\App;
+use addianto\LionGame\Backend\Config\Config;
+use addianto\LionGame\Backend\Service\SheetsService;
 
 require __DIR__ . '/../vendor/autoload.php';
 
-- 
GitLab


From 69afcf480faae63845c0ed4406de1467b94f239e Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sun, 8 Nov 2020 03:07:03 +0700
Subject: [PATCH 09/10] Fix documentation (#5)

---
 README.md | 96 ++++++++++++++++++++++++++-----------------------------
 1 file changed, 46 insertions(+), 50 deletions(-)

diff --git a/README.md b/README.md
index f7199ac..64afc8f 100644
--- a/README.md
+++ b/README.md
@@ -43,39 +43,37 @@ Example request body:
 > **Do not actually include the comments in the actual JSON!**
 
 ```json
-{
-  [
-    "VSWM try out",     // project_name
-    "Try Out",          // organisation_name
-    "Minggu 1 Jo",      // group_name
-    "1",                // participant_case_number
-    "Par1",             // participant_name
-    "",                 // participant_symbol
-    "",                 // participant_color
-    "LvTLiO",           // participant_token
-    "",                 // taak (?)
-    "5",                // #levels
-    "4",                // #items
-    "IND",              // language
-    "default_theme",    // theme
-    "16",               // matrix_size
-    "",                 // id
-    "60",               // task_id
-    "",                 // participant_id
-    "0",                // level
-    "1",                // item
-    "1",                // question_number
-    "red",              // correct_answer
-    "8",                // correct_matrix_number
-    "NA",               // response
-    "12",               // response_matrix_number (index position in 1-dimensional array representation)
-    "131037",           // response_time (in milliseconds)
-    "0",                // score
-    "",                 // created_at (sent from the game)
-    "",                 // updated_at (sent from the game)
-    "",                 // deleted_at
-  ]
-}
+[
+  "VSWM try out",     // project_name
+  "Try Out",          // organisation_name
+  "Minggu 1 Jo",      // group_name
+  "1",                // participant_case_number
+  "Par1",             // participant_name
+  "",                 // participant_symbol
+  "",                 // participant_color
+  "LvTLiO",           // participant_token
+  "",                 // taak (?)
+  "5",                // #levels
+  "4",                // #items
+  "IND",              // language
+  "default_theme",    // theme
+  "16",               // matrix_size
+  "",                 // id
+  "60",               // task_id
+  "",                 // participant_id
+  "0",                // level
+  "1",                // item
+  "1",                // question_number
+  "red",              // correct_answer
+  "8",                // correct_matrix_number
+  "NA",               // response
+  "12",               // response_matrix_number (index position in 1-dimensional array representation)
+  "131037",           // response_time (in milliseconds)
+  "0",                // score
+  "",                 // created_at (sent from the game)
+  "",                 // updated_at (sent from the game)
+  "",                 // deleted_at
+]
 ```
 
 Example response:
@@ -110,25 +108,23 @@ curl -X POST https://liongame.adian.to/api/logs/batch \
 Example request body:
 
 ```json
-{
+[
+  [
+    "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
+    "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "1", "1",
+    "red", "8", "NA", "12", "131037", "0", "", "", ""
+  ],
   [
-    [
-      "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
-      "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "1", "1",
-      "red", "8", "NA", "12", "131037", "0", "", "", ""
-    ],
-    [
-      "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
-      "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "1", "2",
-      "blue", "3", "NA", "3", "62826", "1", "", "", ""
-    ],
-    [
-      "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
-      "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "2", "1",
-      "red", "9", "NA", "13", "32213", "0", "", "", ""
-    ]
+    "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
+    "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "1", "2",
+    "blue", "3", "NA", "3", "62826", "1", "", "", ""
+  ],
+  [
+    "VSWM try out", "Try Out", "Minggu 1 Jo", "1", "Par1", "", "", "LvTLiO",
+    "", "5", "4", "IND", "default_theme", "16", "", "60", "", "0", "2", "1",
+    "red", "9", "NA", "13", "32213", "0", "", "", ""
   ]
-}
+]
 ```
 
 Example response:
-- 
GitLab


From 375d278b9b686dfc07ce6f349964bb36251522de Mon Sep 17 00:00:00 2001
From: Daya Adianto <dayaadianto@cs.ui.ac.id>
Date: Sun, 8 Nov 2020 03:08:03 +0700
Subject: [PATCH 10/10] Fix sheets range (#5)

---
 src/App.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/App.php b/src/App.php
index 17858cb..575a7c0 100644
--- a/src/App.php
+++ b/src/App.php
@@ -60,7 +60,7 @@ class App
             }
 
             if ($this->sheets->appendValues(
-                "'Play Session Analytics (Example)'A:AC",
+                "'Play Session Analytics (Example)'!A:AC",
                 array($data)
             )) {
                 $response
@@ -94,7 +94,7 @@ class App
             }
 
             if ($this->sheets->appendValues(
-                "'Play Session Analytics (Example)'A:AC",
+                "'Play Session Analytics (Example)'!A:AC",
                 $data
             )) {
                 $response
-- 
GitLab