diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 834cab3..d0740dc 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -26,6 +26,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.21.x' + + - name: Test + run: go test -v ./... + - name: Log in to the container registry uses: docker/login-action@v2 with: diff --git a/README.md b/README.md index c10b527..5e303ed 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,39 @@ +# PrioBike Shlink Guard + +This is a proxy that can be used in front of the Shlink to make sure that only valid requests and espacially valid long links are sent to the Shlink. + ## Quickstart +Run locally: ```bash docker-compose up ``` +Run tests: ```bash -curl -X POST --header "X-Api-Key: secret" -H "Content-Type: application/json" -d @example_long_link.json http://localhost/rest/v3/short-urls +go test ``` -Then +### POST + +Should not work: +```bash +curl -X POST --header "X-Api-Key: secret" -H "Content-Type: application/json" -d @example_long_link_base64_invalid.json http://localhost/rest/v3/short-urls +``` +Should work: ```bash -curl -X GET --header "X-Api-Key: secret" -H "Content-Type: application/json" -d @example_long_link.json http://localhost/rest/v3/short-urls/ +curl -X POST --header "X-Api-Key: secret" -H "Content-Type: application/json" -d @example_long_link_shortcut_location.json http://localhost/rest/v3/short-urls ``` + +### GET + +Should not work: +```bash +curl -X GET --header "X-Api-Key: secret" -H "Content-Type: application/json" http://localhost/rest/v3/short-urls +``` + +Should work (if short link exists): +```bash +curl -X GET --header "X-Api-Key: secret" -H "Content-Type: application/json" http://localhost/rest/v3/short-urls/segrs4 +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3f79c88..7f88e03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: environment: # Http cannot be omitted here - PROXY_TARGET=http://shlink:8080 - - LOGGING_LEVEL=debug + - LOG_LEVEL=debug networks: - test-network labels: diff --git a/example_long_link_bad_json.json b/example_long_link_base64_invalid.json similarity index 100% rename from example_long_link_bad_json.json rename to example_long_link_base64_invalid.json diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..29800a8 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module shlink-guard + +go 1.21.3 diff --git a/main.go b/main.go index e1c488f..43293e8 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "log" "net/http" "os" + "reflect" "strings" ) @@ -15,145 +16,283 @@ var ( logLevel = os.Getenv("LOG_LEVEL") ) -func checkAndProxy(w http.ResponseWriter, r *http.Request) { - if logLevel == "debug" { - log.Printf("Request: %s %s\n", r.Method, r.URL.Path) - } - +func checkRequest(w http.ResponseWriter, r *http.Request) bool { // Check if the request is to /rest/v3/short-urls if !strings.Contains(r.URL.Path, "/rest/v3/short-urls") { - http.Error(w, "Invalid URL", http.StatusBadRequest) - return + if logLevel == "debug" { + log.Printf("Invalid URL, end point not supported: %s\n", r.URL.Path) + } + return false } - // If this is a GET request, we only want to allow /rest/v3/short-urls/{code} - if r.Method == http.MethodGet { - // Check if the URL contains a code - code := strings.Split(r.URL.Path, "/rest/v3/short-urls/") - if len(code) < 2 { - http.Error(w, "Invalid URL", http.StatusBadRequest) - return + return true +} + +func checkGetRequest(w http.ResponseWriter, r *http.Request) bool { + // Check if the URL contains a code + code := strings.Split(r.URL.Path, "/rest/v3/short-urls/") + if len(code) < 2 { + if logLevel == "debug" { + log.Printf("Invalid URL, missing short link, : %s\n", r.URL.Path) } + return false + } - shortlink := code[len(code)-1] - if shortlink == "" { - http.Error(w, "Invalid URL", http.StatusBadRequest) - return + // Check if the code is empty + shortlink := code[len(code)-1] + if shortlink == "" { + if logLevel == "debug" { + log.Printf("Invalid URL, short link is empty: %s\n", r.URL.Path) } - proxy(w, r, nil) + return false } - // If this is a POST request, we need to check the content type - if r.Method == http.MethodPost { - // Check if the content type is application/json - if r.Header.Get("Content-Type") != "application/json" { - http.Error(w, "Invalid content type", http.StatusBadRequest) - return + return true +} + +func checkBody(r *http.Request) (bool, []byte, string) { + // Check if the content type is application/json + if r.Header.Get("Content-Type") != "application/json" { + if logLevel == "debug" { + log.Printf("Invalid content type: %s\n", r.Header.Get("Content-Type")) } - if r.Body == nil { - http.Error(w, "Empty body", http.StatusBadRequest) - return + return false, nil, "" + } + if r.Body == nil { + if logLevel == "debug" { + log.Printf("Empty body\n") } - // Read the body - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Error reading body", http.StatusInternalServerError) - return + return false, nil, "" + } + // Read the body + body, err := io.ReadAll(r.Body) + if err != nil { + if logLevel == "debug" { + log.Printf("Error reading body\n") } - // Check if the body contains the key "longUrl" - if !strings.Contains(string(body), "longUrl") { - http.Error(w, "Invalid body", http.StatusBadRequest) - return + return false, nil, "" + } + + var parsedBody map[string]interface{} + json.Unmarshal([]byte(body), &parsedBody) + + if parsedBody == nil { + if logLevel == "debug" { + log.Printf("Invalid body, JSON could not be parsed\n") + } + return false, nil, "" + } + + // Check if the body contains the key "longUrl" + if _, ok := parsedBody["longUrl"]; !ok { + if logLevel == "debug" { + log.Printf("Invalid body, missing longUrl key\n") } + return false, nil, "" + } - // Check if the longUrl is a base64 encoded shorcut. - // Therefore get the base64 encoded string under longUrl: "https...import/{base64 encoded string}" - str := strings.Split(string(body), "import/") - if len(str) != 2 { - http.Error(w, "Invalid longUrl", http.StatusBadRequest) - return + // Check if the longUrl is a string. + longUrl := parsedBody["longUrl"] + if longUrl == nil { + if logLevel == "debug" { + log.Printf("Invalid body, longUrl is nil\n") } + return false, nil, "" + } + if reflect.TypeOf(longUrl).Kind() != reflect.String { + if logLevel == "debug" { + log.Printf("Invalid body, longUrl is not a string\n") + } + return false, nil, "" + } - str = strings.Split(str[1], "\"") - if len(str) < 2 { - http.Error(w, "Invalid longUrl", http.StatusBadRequest) - return + longUrlS := longUrl.(string) + + return true, body, longUrlS +} + +func checkLongUrl(longUrl string) (bool, map[string]interface{}) { + // Check if the longUrl is valid. + if !strings.Contains(longUrl, "import/") { + if logLevel == "debug" { + log.Printf("Invalid body, longUrl does not contain 'import/'\n") } + return false, nil + } - shortcut, err := base64.StdEncoding.DecodeString(str[0]) - if err != nil { - http.Error(w, "Invalid longUrl", http.StatusBadRequest) - return + urlParts := strings.Split(longUrl, "import/") + if len(urlParts) < 2 { + if logLevel == "debug" { + log.Printf("Invalid body, longUrl does not contain part after import/\n") } + return false, nil + } - // Make more checks on the body, e.g. validate the JSON structure - // and whether the contained code can be parsed as a valid shortcut. + base64Str := urlParts[len(urlParts)-1] - var jsonMap map[string]interface{} - json.Unmarshal([]byte(shortcut), &jsonMap) + shortcut, err := base64.StdEncoding.DecodeString(base64Str) + if err != nil { + if logLevel == "debug" { + log.Printf("Invalid body, base64 decode failed\n") + } + return false, nil + } - if jsonMap == nil { - http.Error(w, "Invalid shortcut", http.StatusBadRequest) - return + var jsonMap map[string]interface{} + unmarshErr := json.Unmarshal([]byte(shortcut), &jsonMap) + + if unmarshErr != nil { + if logLevel == "debug" { + log.Printf("Invalid body, base64v JSON could not be parsed\n") + } + return false, nil + } + + return true, jsonMap +} + +func checkShortcut(jsonMap map[string]interface{}) (bool, string) { + if jsonMap == nil { + if logLevel == "debug" { + log.Printf("Invalid body, JSON could not be parsed\n") + } + return false, "" + } + + // Check the type of the shortcut (e.g. "ShortcutLocation" or "ShortcutRoute") + shortcutType := jsonMap["type"] + + if shortcutType == nil { + if logLevel == "debug" { + log.Printf("Invalid body, missing type key\n") + } + return false, "" + } + + if shortcutType != "ShortcutLocation" && shortcutType != "ShortcutRoute" { + if logLevel == "debug" { + log.Printf("Invalid body, invalid type key\n") + } + return false, "" + } + + if _, ok := jsonMap["id"]; !ok { + if logLevel == "debug" { + log.Printf("Invalid body, missing id key\n") + } + return false, "" + } + + if _, ok := jsonMap["name"]; !ok { + if logLevel == "debug" { + log.Printf("Invalid body, missing name key\n") + } + return false, "" + } + + return true, shortcutType.(string) +} + +func checkLocationShortcut(shortcut map[string]interface{}) bool { + if len(shortcut) > 4 { + if logLevel == "debug" { + log.Printf("Invalid body, too many keys\n") + } + return false + } + + if _, ok := shortcut["waypoint"]; !ok { + if logLevel == "debug" { + log.Printf("Invalid body, missing waypoint key\n") + } + return false + } + + return true +} + +func checkRouteShortcut(shortcut map[string]interface{}) bool { + if len(shortcut) > 6 { + if logLevel == "debug" { + log.Printf("Invalid body, too many keys\n") + } + return false + } + + if _, ok := shortcut["waypoints"]; !ok { + if logLevel == "debug" { + log.Printf("Invalid body, missing waypoints key\n") + } + return false + } + + if _, ok := shortcut["routeTimeText"]; !ok { + if logLevel == "debug" { + log.Printf("Invalid body, missing routeTimeText key\n") + } + return false + } + + if _, ok := shortcut["routeLengthText"]; !ok { + if logLevel == "debug" { + log.Printf("Invalid body, missing routeLengthText key\n") } + return false + } + + return true +} - // Check the type of the shortcut (e.g. "ShortcutLocation" or "ShortcutRoute") - shortcutType := jsonMap["type"] +func checkAndProxy(w http.ResponseWriter, r *http.Request) { + if logLevel == "debug" { + log.Printf("Request: %s %s\n", r.Method, r.URL.Path) + } - if shortcutType == nil { - http.Error(w, "Invalid shortcut", http.StatusBadRequest) + if !checkRequest(w, r) { + http.Error(w, "Invalid", http.StatusBadRequest) + return + } + + // If this is a GET request, we only want to allow /rest/v3/short-urls/{code} + if r.Method == http.MethodGet { + if !checkGetRequest(w, r) { + http.Error(w, "Invalid", http.StatusBadRequest) return } + proxy(w, r, nil) + return + } - if shortcutType != "ShortcutLocation" && shortcutType != "ShortcutRoute" { - http.Error(w, "Invalid shortcut type", http.StatusBadRequest) + // If this is a POST request, we need to check the content + if r.Method == http.MethodPost { + ok, body, longUrl := checkBody(r) + if !ok { + http.Error(w, "Invalid", http.StatusBadRequest) return } - // Check the common keys for both types - if _, ok := jsonMap["id"]; !ok { - http.Error(w, "Invalid shortcut", http.StatusBadRequest) + ok, jsonMap := checkLongUrl(longUrl) + if !ok { + http.Error(w, "Invalid", http.StatusBadRequest) return } - if _, ok := jsonMap["name"]; !ok { - http.Error(w, "Invalid shortcut", http.StatusBadRequest) + ok, shortcutType := checkShortcut(jsonMap) + if !ok { + http.Error(w, "Invalid", http.StatusBadRequest) return } - // Check Shortcut Location JSON individual keys and length if shortcutType == "ShortcutLocation" { - print(len(jsonMap)) - if len(jsonMap) > 4 { - http.Error(w, "Invalid shortcut attributes", http.StatusBadRequest) - return - } - - if _, ok := jsonMap["waypoint"]; !ok { - http.Error(w, "Invalid shortcut", http.StatusBadRequest) + if !checkLocationShortcut(jsonMap) { + http.Error(w, "Invalid", http.StatusBadRequest) return } } if shortcutType == "ShortcutRoute" { - print(len(jsonMap)) - if len(jsonMap) > 6 { - http.Error(w, "Invalid shortcut attributes", http.StatusBadRequest) - return - } - - if _, ok := jsonMap["waypoints"]; !ok { - http.Error(w, "Invalid shortcut", http.StatusBadRequest) - return - } - - if _, ok := jsonMap["routeTimeText"]; !ok { - http.Error(w, "Invalid shortcut", http.StatusBadRequest) - return - } - - if _, ok := jsonMap["routeLengthText"]; !ok { - http.Error(w, "Invalid shortcut", http.StatusBadRequest) + if !checkRouteShortcut(jsonMap) { + http.Error(w, "Invalid", http.StatusBadRequest) return } } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..0b9404d --- /dev/null +++ b/main_test.go @@ -0,0 +1,440 @@ +package main + +import ( + "fmt" + "io" + "net/http/httptest" + "strings" + "testing" +) + +func TestCheckRequestValid(t *testing.T) { + req := httptest.NewRequest("GET", "/rest/v3/short-urls", nil) + w := httptest.NewRecorder() + if !checkRequest(w, req) { + t.Errorf("Expected true, got false") + } +} + +func TestCheckRequestInvalidEndpoint(t *testing.T) { + req := httptest.NewRequest("GET", "/rest/v3/short-url", nil) + w := httptest.NewRecorder() + if checkRequest(w, req) { + t.Errorf("Expected false, got true") + } +} + +func TestCheckGetRequestValid(t *testing.T) { + req := httptest.NewRequest("GET", "/rest/v3/short-urls/123", nil) + w := httptest.NewRecorder() + if !checkGetRequest(w, req) { + t.Errorf("Expected true, got false") + } +} + +func TestCheckGetRequestMissingShortlink(t *testing.T) { + req := httptest.NewRequest("GET", "/rest/v3/short-urls", nil) + w := httptest.NewRecorder() + if checkGetRequest(w, req) { + t.Errorf("Expected false, got true") + } +} + +func TestCheckGetRequestEmptyShortlink(t *testing.T) { + req := httptest.NewRequest("GET", "/rest/v3/short-urls/", nil) + w := httptest.NewRecorder() + if checkGetRequest(w, req) { + t.Errorf("Expected false, got true") + } +} + +func TestCheckBodyValid(t *testing.T) { + req := httptest.NewRequest("POST", "/rest/v3/short-urls", nil) + longUrl := "http://example.com/drth5" + bodyJson := fmt.Sprintf(`{"longUrl": "%s"}`, longUrl) + req.Header.Set("Content-Type", "application/json") + req.Body = io.NopCloser(strings.NewReader(bodyJson)) + ok, _, longUrlReturn := checkBody(req) + if !ok { + t.Errorf("Expected true, got false") + } + if longUrl != longUrlReturn { + t.Errorf("Expected %s, got %s", longUrl, longUrl) + } +} + +func TestCheckBodyInvalidContentType(t *testing.T) { + req := httptest.NewRequest("POST", "/rest/v3/short-urls", nil) + body := "aergfwrger" + req.Header.Set("Content-Type", "text/plain") + req.Body = io.NopCloser(strings.NewReader(body)) + ok, _, _ := checkBody(req) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckBodyMissingBody(t *testing.T) { + req := httptest.NewRequest("POST", "/rest/v3/short-urls", nil) + req.Header.Set("Content-Type", "application/json") + ok, _, _ := checkBody(req) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckBodyInvalidJson(t *testing.T) { + req := httptest.NewRequest("POST", "/rest/v3/short-urls", nil) + body := "aergfwrger" + req.Header.Set("Content-Type", "application/json") + req.Body = io.NopCloser(strings.NewReader(body)) + ok, _, _ := checkBody(req) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckBodyMissingLongUrl(t *testing.T) { + req := httptest.NewRequest("POST", "/rest/v3/short-urls", nil) + body := `{longU: "http://example.com/drth5"}` + req.Header.Set("Content-Type", "application/json") + req.Body = io.NopCloser(strings.NewReader(body)) + ok, _, _ := checkBody(req) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckBodyLongUrlNotString(t *testing.T) { + req := httptest.NewRequest("POST", "/rest/v3/short-urls", nil) + body := `{longUrl: 123}` + req.Header.Set("Content-Type", "application/json") + req.Body = io.NopCloser(strings.NewReader(body)) + ok, _, _ := checkBody(req) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckLongUrlValid(t *testing.T) { + longUrl := "http://example.com/import/eyJ0eXBlIjoiU2hvcnRjdXRMb2NhdGlvbiIsImlkIjoiWyM1ZTYzOV0iLCJuYW1lIjoiIiwid2F5cG9pbnQiOnsibGF0Ijo1My41NDE1NzAxMDc3NzY2LCJsb24iOjkuOTg0Mjc1NjA1Nzk0Njg2LCJhZGRyZXNzIjoiRWxicGhpbGhhcm1vbmllIEhhbWJ1cmcsIFBsYXR6IGRlciBEZXV0c2NoZW4gRWluaGVpdCwgSGFtYnVyZyJ9fQ==" + ok, _ := checkLongUrl(longUrl) + if !ok { + t.Errorf("Expected true, got false") + } +} + +func TestCheckLongUrlMissingImport(t *testing.T) { + longUrl := "http://example.com" + ok, _ := checkLongUrl(longUrl) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckLongUrlMissingBase64(t *testing.T) { + longUrl := "http://example.com/import/" + ok, _ := checkLongUrl(longUrl) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckLongUrlInvalidBase64(t *testing.T) { + longUrl := "http://example.com/import/123" + ok, _ := checkLongUrl(longUrl) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckLongUrlInvalidJson(t *testing.T) { + longUrl := "http://example.com/import/aGFsbG8gd2VsdA==" + ok, _ := checkLongUrl(longUrl) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckShortcutValid(t *testing.T) { + shortcutLocation := map[string]interface{}{ + "type": "ShortcutLocation", + "id": "[#5e639]", + "name": "", + "waypoint": map[string]interface{}{ + "lat": 53.5415701077766, + "lon": 9.984275605794686, + "address": "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg", + }, + } + ok, shortcutType := checkShortcut(shortcutLocation) + if !ok { + t.Errorf("Expected true, got false") + } + + if shortcutType != "ShortcutLocation" { + t.Errorf("Expected ShortcutLocation, got %s", shortcutType) + } + + shortcutRoute := map[string]interface{}{ + "type": "ShortcutRoute", + "id": "[#dd9f9]", + "name": "", + "waypoints": [2]map[string]interface{}{ + { + "lat": 53.5522524, + "lon": 9.9313068, + "address": "Altona-Altstadt, 22767, Hamburg, Deutschland", + }, + { + "lat": 53.5536507, + "lon": 9.9893664, + "address": "Jungfernstieg, Altstadt, 20095, Hamburg, Deutschland", + }, + }, + "routeLengthText": "4.8 km", + "routeTimeText": "17 Min.", + } + ok, shortcutType = checkShortcut(shortcutRoute) + if !ok { + t.Errorf("Expected true, got false") + } + + if shortcutType != "ShortcutRoute" { + t.Errorf("Expected ShortcutRoute, got %s", shortcutType) + } +} + +func TestCheckShortcutJsonNil(t *testing.T) { + ok, _ := checkShortcut(nil) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckShortcutTypeKeyMissing(t *testing.T) { + shortcutLocation := map[string]interface{}{ + "id": "[#5e639]", + "name": "", + "waypoint": map[string]interface{}{ + "lat": 53.5415701077766, + "lon": 9.984275605794686, + "address": "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg", + }, + } + ok, _ := checkShortcut(shortcutLocation) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckShortcutTypeKeyInvalid(t *testing.T) { + shortcutLocation := map[string]interface{}{ + "type": "Shortcut", + "id": "[#5e639]", + "name": "", + "waypoint": map[string]interface{}{ + "lat": 53.5415701077766, + "lon": 9.984275605794686, + "address": "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg", + }, + } + ok, _ := checkShortcut(shortcutLocation) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckShortcutIdKeyMissing(t *testing.T) { + shortcutLocation := map[string]interface{}{ + "type": "ShortcutLocation", + "name": "", + "waypoint": map[string]interface{}{ + "lat": 53.5415701077766, + "lon": 9.984275605794686, + "address": "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg", + }, + } + ok, _ := checkShortcut(shortcutLocation) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckShortcutNameKeyMissing(t *testing.T) { + shortcutLocation := map[string]interface{}{ + "type": "ShortcutLocation", + "id": "[#5e639]", + "waypoint": map[string]interface{}{ + "lat": 53.5415701077766, + "lon": 9.984275605794686, + "address": "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg", + }, + } + ok, _ := checkShortcut(shortcutLocation) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckLocationShortcutValid(t *testing.T) { + shortcutLocation := map[string]interface{}{ + "type": "ShortcutLocation", + "id": "[#5e639]", + "name": "", + "waypoint": map[string]interface{}{ + "lat": 53.5415701077766, + "lon": 9.984275605794686, + "address": "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg", + }, + } + ok := checkLocationShortcut(shortcutLocation) + if !ok { + t.Errorf("Expected true, got false") + } +} + +func TestCheckLocationShortcutTooManyKeys(t *testing.T) { + shortcutLocation := map[string]interface{}{ + "type": "ShortcutLocation", + "id": "[#5e639]", + "name": "", + "waypoint": map[string]interface{}{ + "lat": 53.5415701077766, + "lon": 9.984275605794686, + "address": "Elbphilharmonie Hamburg, Platz der Deutschen Einheit, Hamburg", + }, + "extra": "extra", + } + ok := checkLocationShortcut(shortcutLocation) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckLocationShortcutWaypointKeyMissing(t *testing.T) { + shortcutLocation := map[string]interface{}{ + "type": "ShortcutLocation", + "id": "[#5e639]", + "name": "", + } + ok := checkLocationShortcut(shortcutLocation) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckRouteShortcutValid(t *testing.T) { + shortcutRoute := map[string]interface{}{ + "type": "ShortcutRoute", + "id": "[#dd9f9]", + "name": "", + "waypoints": [2]map[string]interface{}{ + { + "lat": 53.5522524, + "lon": 9.9313068, + "address": "Altona-Altstadt, 22767, Hamburg, Deutschland", + }, + { + "lat": 53.5536507, + "lon": 9.9893664, + "address": "Jungfernstieg, Altstadt, 20095, Hamburg, Deutschland", + }, + }, + "routeLengthText": "4.8 km", + "routeTimeText": "17 Min.", + } + ok := checkRouteShortcut(shortcutRoute) + if !ok { + t.Errorf("Expected true, got false") + } +} + +func TestCheckRouteShortcutTooManyKeys(t *testing.T) { + shortcutRoute := map[string]interface{}{ + "type": "ShortcutRoute", + "id": "[#dd9f9]", + "name": "", + "waypoints": [2]map[string]interface{}{ + { + "lat": 53.5522524, + "lon": 9.9313068, + "address": "Altona-Altstadt, 22767, Hamburg, Deutschland", + }, + { + "lat": 53.5536507, + "lon": 9.9893664, + "address": "Jungfernstieg, Altstadt, 20095, Hamburg, Deutschland", + }, + }, + "routeLengthText": "4.8 km", + "routeTimeText": "17 Min.", + "extra": "extra", + } + ok := checkRouteShortcut(shortcutRoute) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckRouteShortcutWaypointsKeyMissing(t *testing.T) { + shortcutRoute := map[string]interface{}{ + "type": "ShortcutRoute", + "id": "[#dd9f9]", + "name": "", + } + ok := checkRouteShortcut(shortcutRoute) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckRouteShortcutRouteTimeTextKeyMissing(t *testing.T) { + shortcutRoute := map[string]interface{}{ + "type": "ShortcutRoute", + "id": "[#dd9f9]", + "name": "", + "waypoints": [2]map[string]interface{}{ + { + "lat": 53.5522524, + "lon": 9.9313068, + "address": "Altona-Altstadt, 22767, Hamburg, Deutschland", + }, + { + "lat": 53.5536507, + "lon": 9.9893664, + "address": "Jungfernstieg, Altstadt, 20095, Hamburg, Deutschland", + }, + }, + "routeLengthText": "4.8 km", + } + ok := checkRouteShortcut(shortcutRoute) + if ok { + t.Errorf("Expected false, got true") + } +} + +func TestCheckRouteShortcutRouteLengthTextKeyMissing(t *testing.T) { + shortcutRoute := map[string]interface{}{ + "type": "ShortcutRoute", + "id": "[#dd9f9]", + "name": "", + "waypoints": [2]map[string]interface{}{ + { + "lat": 53.5522524, + "lon": 9.9313068, + "address": "Altona-Altstadt, 22767, Hamburg, Deutschland", + }, + { + "lat": 53.5536507, + "lon": 9.9893664, + "address": "Jungfernstieg, Altstadt, 20095, Hamburg, Deutschland", + }, + }, + "routeTimeText": "17 Min.", + } + ok := checkRouteShortcut(shortcutRoute) + if ok { + t.Errorf("Expected false, got true") + } +}