Fill out AddToStats handler and test

This patch makes it so we can submit data scouting data to the
database. All tests are updated.

This patch also adds more error checking code to the database code to
make sure we get notified of some important edge cases.

The majority of the patch is written by Sabina. I amended the
`cli_test.py` file.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Signed-off-by: Sabina Leaver <100027607@mvla.net>
Change-Id: I9c70b8b02bbd3ff434fbbb857c2ed65f2684a50e
diff --git a/scouting/webserver/requests/debug/cli/cli_test.py b/scouting/webserver/requests/debug/cli/cli_test.py
index a737d59..64d79a6 100644
--- a/scouting/webserver/requests/debug/cli/cli_test.py
+++ b/scouting/webserver/requests/debug/cli/cli_test.py
@@ -7,6 +7,7 @@
 import shutil
 import socket
 import subprocess
+import textwrap
 import time
 from typing import Any, Dict, List
 import unittest
@@ -55,11 +56,8 @@
 
         # Copy the test data into place so that the final API call can be
         # emulated.
-        tba_api_dir = tmpdir / "api" / "v3" / "event" / "1234event_key"
-        os.makedirs(tba_api_dir)
-        (tba_api_dir / "matches").write_text(
-            Path("scouting/scraping/test_data/2016_nytr.json").read_text()
-        )
+        self.set_up_tba_api_dir(tmpdir, year=2016, event_code="nytr")
+        self.set_up_tba_api_dir(tmpdir, year=2020, event_code="fake")
 
         # Create a fake TBA server to serve the static match list.
         self.fake_tba_api = subprocess.Popen(
@@ -93,34 +91,61 @@
         self.fake_tba_api.wait()
         self.webserver.wait()
 
-    def refresh_match_list(self):
+    def set_up_tba_api_dir(self, tmpdir, year, event_code):
+        tba_api_dir = tmpdir / "api" / "v3" / "event" / f"{year}{event_code}"
+        os.makedirs(tba_api_dir)
+        (tba_api_dir / "matches").write_text(
+            Path(f"scouting/scraping/test_data/{year}_{event_code}.json").read_text()
+        )
+
+    def refresh_match_list(self, year=2016, event_code="nytr"):
         """Triggers the webserver to fetch the match list."""
         json_path = write_json_request({
-            "year": 1234,
-            "event_code": "event_key",
+            "year": year,
+            "event_code": event_code,
         })
         exit_code, stdout, stderr = run_debug_cli(["-refreshMatchList", json_path])
         self.assertEqual(exit_code, 0, stderr)
         self.assertIn("(refresh_match_list_response.RefreshMatchListResponseT)", stdout)
 
-    def test_submit_data_scouting(self):
-        json_path = write_json_request({
-            "team": 971,
-            "match": 42,
-            "missed_shots_auto": 9971,
-            "upper_goal_auto": 9971,
-            "lower_goal_auto": 9971,
-            "missed_shots_tele": 9971,
-            "upper_goal_tele": 9971,
-            "lower_goal_tele": 9971,
-            "defense_rating": 9971,
-            "climbing": 9971,
-        })
-        exit_code, _stdout, stderr = run_debug_cli(["-submitDataScouting", json_path])
+    def test_submit_and_request_data_scouting(self):
+        self.refresh_match_list(year=2020, event_code="fake")
 
-        # The SubmitDataScouting message isn't handled yet.
-        self.assertEqual(exit_code, 1)
-        self.assertIn("/requests/submit/data_scouting returned 501 Not Implemented", stderr)
+        # First submit some data to be added to the database.
+        json_path = write_json_request({
+            "team": 100,
+            "match": 1,
+            "missed_shots_auto": 10,
+            "upper_goal_auto": 11,
+            "lower_goal_auto": 12,
+            "missed_shots_tele": 13,
+            "upper_goal_tele": 14,
+            "lower_goal_tele": 15,
+            "defense_rating": 3,
+            "climbing": 1,
+        })
+        exit_code, _, stderr = run_debug_cli(["-submitDataScouting", json_path])
+        self.assertEqual(exit_code, 0, stderr)
+
+        # Now request the data back with zero indentation. That let's us
+        # validate the data easily.
+        json_path = write_json_request({})
+        exit_code, stdout, stderr = run_debug_cli(["-requestDataScouting", json_path, "-indent="])
+
+        self.assertEqual(exit_code, 0, stderr)
+        self.assertIn(textwrap.dedent("""\
+            {
+            Team: (int32) 100,
+            Match: (int32) 1,
+            MissedShotsAuto: (int32) 10,
+            UpperGoalAuto: (int32) 11,
+            LowerGoalAuto: (int32) 12,
+            MissedShotsTele: (int32) 13,
+            UpperGoalTele: (int32) 14,
+            LowerGoalTele: (int32) 15,
+            DefenseRating: (int32) 3,
+            Climbing: (int32) 1
+            }"""), stdout)
 
     def test_request_all_matches(self):
         self.refresh_match_list()
@@ -147,13 +172,5 @@
         self.assertEqual(stdout.count("MatchNumber:"), 12)
         self.assertEqual(len(re.findall(r": \(int32\) 4856[,\n]", stdout)), 12)
 
-    def test_request_data_scouting(self):
-        json_path = write_json_request({})
-        exit_code, stdout, stderr = run_debug_cli(["-requestDataScouting", json_path])
-
-        # TODO(phil): Actually add data here before querying it.
-        self.assertEqual(exit_code, 0, stderr)
-        self.assertIn("(request_data_scouting_response.RequestDataScoutingResponseT)", stdout)
-
 if __name__ == "__main__":
     unittest.main()
diff --git a/scouting/webserver/requests/debug/cli/main.go b/scouting/webserver/requests/debug/cli/main.go
index 03032be..6f2de1d 100644
--- a/scouting/webserver/requests/debug/cli/main.go
+++ b/scouting/webserver/requests/debug/cli/main.go
@@ -67,6 +67,8 @@
 
 func main() {
 	// Parse command line arguments.
+	indentPtr := flag.String("indent", " ",
+		"The indentation to use for the result dumping. Default is a space.")
 	addressPtr := flag.String("address", "http://localhost:8080",
 		"The end point where the server is listening.")
 	submitDataScoutingPtr := flag.String("submitDataScouting", "",
@@ -81,6 +83,8 @@
 		"If specified, parse the file as a RefreshMatchList JSON request.")
 	flag.Parse()
 
+	spew.Config.Indent = *indentPtr
+
 	// Handle the actual arguments.
 	if *submitDataScoutingPtr != "" {
 		log.Printf("Sending SubmitDataScouting to %s", *addressPtr)
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 93ee128..6c4bdd1 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -20,12 +20,13 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
-	_ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
 	flatbuffers "github.com/google/flatbuffers/go"
 )
 
 type SubmitDataScouting = submit_data_scouting.SubmitDataScouting
+type SubmitDataScoutingResponseT = submit_data_scouting_response.SubmitDataScoutingResponseT
 type RequestAllMatches = request_all_matches.RequestAllMatches
 type RequestAllMatchesResponseT = request_all_matches_response.RequestAllMatchesResponseT
 type RequestMatchesForTeam = request_matches_for_team.RequestMatchesForTeam
@@ -91,16 +92,32 @@
 		return
 	}
 
-	_, success := parseSubmitDataScouting(w, requestBytes)
+	request, success := parseSubmitDataScouting(w, requestBytes)
 	if !success {
 		return
 	}
 
-	// TODO(phil): Actually handle the request.
-	// We have access to the database via "handler.db" here. For example:
-	// stats := handler.db.ReturnStats()
+	stats := db.Stats{
+		TeamNumber:      request.Team(),
+		MatchNumber:     request.Match(),
+		ShotsMissedAuto: request.MissedShotsAuto(),
+		UpperGoalAuto:   request.UpperGoalAuto(),
+		LowerGoalAuto:   request.LowerGoalAuto(),
+		ShotsMissed:     request.MissedShotsTele(),
+		UpperGoalShots:  request.UpperGoalTele(),
+		LowerGoalShots:  request.LowerGoalTele(),
+		PlayedDefense:   request.DefenseRating(),
+		Climbing:        request.Climbing(),
+	}
 
-	respondNotImplemented(w)
+	err = handler.db.AddToStats(stats)
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit datascouting data: ", err))
+	}
+
+	builder := flatbuffers.NewBuilder(50 * 1024)
+	builder.Finish((&SubmitDataScoutingResponseT{}).Pack(builder))
+	w.Write(builder.FinishedBytes())
 }
 
 // TODO(phil): Can we turn this into a generic?
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index 999e955..60bee0e 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -20,7 +20,7 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
-	_ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
 	flatbuffers "github.com/google/flatbuffers/go"
 )
@@ -92,15 +92,16 @@
 		Climbing:        9971,
 	}).Pack(builder))
 
-	resp, err := http.Post("http://localhost:8080/requests/submit/data_scouting", "application/octet-stream", bytes.NewReader(builder.FinishedBytes()))
+	response, err := debug.SubmitDataScouting("http://localhost:8080", builder.FinishedBytes())
 	if err != nil {
-		t.Fatalf("Failed to send request: %v", err)
+		t.Fatal("Failed to submit data scouting: ", err)
 	}
-	if resp.StatusCode != http.StatusNotImplemented {
-		t.Fatal("Unexpected status code. Got", resp.Status)
+
+	// We get an empty response back. Validate that.
+	expected := submit_data_scouting_response.SubmitDataScoutingResponseT{}
+	if !reflect.DeepEqual(expected, *response) {
+		t.Fatal("Expected ", expected, ", but got:", *response)
 	}
-	// TODO(phil): We have nothing to validate yet. Fix that.
-	// TODO(phil): Can we use scouting/webserver/requests/debug here?
 }
 
 // Validates that we can request the full match list.
@@ -160,6 +161,7 @@
 			t.Fatal("Expected for match", i, ":", *match, ", but got:", *response.MatchList[i])
 		}
 	}
+
 }
 
 // Validates that we can request the full match list.
@@ -358,7 +360,8 @@
 	return nil
 }
 
-func (database *MockDatabase) AddToStats(db.Stats) error {
+func (database *MockDatabase) AddToStats(stats db.Stats) error {
+	database.stats = append(database.stats, stats)
 	return nil
 }