package main import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" ) func startHandoff(t *testing.T, h http.Handler, token string) (deviceCode, userCode string) { t.Helper() rec := do(t, h, "POST", "/v1/settings/start", token, `{"installationId":"`+testInstallation+`"}`) if rec.Code != 201 { t.Fatalf("start: %d %s", rec.Code, rec.Body) } var got map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatal(err) } if got["verificationUriComplete"] != "https://car.example.com/settings?code="+got["userCode"].(string) { t.Fatalf("uri %v", got["verificationUriComplete"]) } return got["deviceCode"].(string), got["userCode"].(string) } func sendSettings(t *testing.T, h http.Handler, form url.Values) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest("POST", "/settings", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } func TestSettingsTravelFromThePhoneToTheCarOnce(t *testing.T) { api, _ := newTestAPI() h := api.Routes() if rec := do(t, h, "POST", "/v1/settings/start", "", `{"installationId":"`+testInstallation+`"}`); rec.Code != 401 { t.Fatalf("start without a token: %d", rec.Code) } deviceCode, userCode := startHandoff(t, h, testToken) poll := func() *httptest.ResponseRecorder { return do(t, h, "POST", "/v1/settings/poll", testToken, `{"deviceCode":"`+deviceCode+`"}`) } if rec := poll(); rec.Code != 202 { t.Fatalf("before the phone: %d", rec.Code) } page := do(t, h, "GET", "/settings?code="+userCode, "", "") if page.Code != 200 || !strings.Contains(page.Body.String(), userCode) { t.Fatalf("page %d", page.Code) } form := url.Values{"code": {userCode}, "abrpApiKey": {"api-key-1234"}, "abrpToken": {"user-token-5678"}, "price": {"0,39"}, "password": {"wrong"}} if rec := sendSettings(t, h, form); rec.Code != 401 || strings.Contains(rec.Body.String(), "user-token-5678") { t.Fatalf("wrong password: %d (credentials must not be echoed)", rec.Code) } form.Set("password", testApprovalPassword) if rec := sendSettings(t, h, form); rec.Code != 200 { t.Fatalf("send: %d %s", rec.Code, rec.Body) } if rec := sendSettings(t, h, form); rec.Code != 409 { t.Fatalf("sent twice: %d", rec.Code) } rec := poll() var got PhoneSettings if rec.Code != 200 || json.Unmarshal(rec.Body.Bytes(), &got) != nil { t.Fatalf("collect: %d %s", rec.Code, rec.Body) } if got.AbrpAPIKey != "api-key-1234" || got.AbrpToken != "user-token-5678" || got.PricePerKwh == nil || *got.PricePerKwh != 0.39 || got.UsableKwh != nil { t.Fatalf("settings %+v", got) } if rec.Header().Get("Cache-Control") != "no-store" { t.Fatal("credentials cacheable") } if rec := poll(); rec.Code != 410 { t.Fatalf("collected twice: %d", rec.Code) } } func TestSettingsAreOnlyForTheCarThatAsked(t *testing.T) { api, _ := newTestAPI() h := api.Routes() deviceCode, userCode := startHandoff(t, h, testToken) _ = api.Handoff.Fill(userCode, PhoneSettings{AbrpToken: "user-token-5678"}) // A car paired as another installation cannot open a request for this one, nor collect it. other := caller{installationID: "7e2b3c4d-5e6f-4a70-9b8c-1d2e3f4a5b6c"} if s, err := api.Handoff.Poll(deviceCode, other.mayUse); s != nil || err == nil { t.Fatal("another car collected the settings") } if s, err := api.Handoff.Poll(deviceCode, caller{}.mayUse); s == nil || err != nil { t.Fatalf("the car itself could not collect: %v", err) } } func TestPhoneSettingsAreChecked(t *testing.T) { for name, form := range map[string]url.Values{ "nothing": {}, "short token": {"abrpToken": {"abc"}}, "spaced key": {"abrpApiKey": {"has a space here"}}, "price absurd": {"price": {"250"}}, "usable too low": {"usable": {"3"}}, "not a number": {"price": {"cheap"}}, } { if _, err := phoneSettingsFromForm(form); err == nil { t.Errorf("%s accepted", name) } } s, err := phoneSettingsFromForm(url.Values{"usable": {" 50,8 "}}) if err != nil || s.UsableKwh == nil || *s.UsableKwh != 50.8 || s.AbrpToken != "" { t.Fatalf("%+v %v", s, err) } } func TestHandoffRequestsExpireAndAreReplaced(t *testing.T) { h := NewHandoff() first, _ := h.Start(testInstallation) second, _ := h.Start(testInstallation) if _, err := h.Poll(first.deviceCode, caller{}.mayUse); err == nil { t.Fatal("the replaced request is still open") } h.Now = func() time.Time { return time.Now().Add(pairingTTL + time.Second) } if err := h.Fill(second.userCode, PhoneSettings{AbrpToken: "user-token-5678"}); err == nil { t.Fatal("an expired request took settings") } } func TestPhoneSettingsNeedPairingSwitchedOn(t *testing.T) { api, _ := newTestAPI() api.Pairing = NewPairing("", nil) h := api.Routes() if rec := do(t, h, "POST", "/v1/settings/start", testToken, `{"installationId":"`+testInstallation+`"}`); rec.Code != 404 { t.Fatalf("start: %d", rec.Code) } if rec := do(t, h, "GET", "/settings", "", ""); rec.Code != 404 { t.Fatalf("page: %d", rec.Code) } }