Port pytoyoda's authentication and read-only data flow to a Go builtin plugin behind the existing plugin contract. Implements the three-legged ForgeRock/OAuth2 login (authenticate callback loop, authorize, token exchange), silent refresh with full re-auth fallback, and the full Toyota gateway header set with backoff on 429/5xx. Exposes read-only capabilities: vehicles, telemetry, location, health, status, electric, notifications, and service-history. Config takes a MyToyota email/password and a Toyota/Lexus brand select. Europe-only and read-only, matching pytoyoda's limitations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
125 lines
3.3 KiB
Go
125 lines
3.3 KiB
Go
package toyota
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"drivervault/apiserver/internal/plugins"
|
|
)
|
|
|
|
func TestDescriptor(t *testing.T) {
|
|
d := (&Plugin{}).Descriptor()
|
|
if d.Name != "toyota" {
|
|
t.Fatalf("name = %q, want toyota", d.Name)
|
|
}
|
|
if d.Kind != plugins.KindBuiltin {
|
|
t.Fatalf("kind = %q, want builtin", d.Kind)
|
|
}
|
|
if len(d.Capabilities) == 0 {
|
|
t.Fatal("expected capabilities")
|
|
}
|
|
// Required credential fields must be present.
|
|
req := map[string]bool{}
|
|
for _, f := range d.ConfigFields {
|
|
if f.Required {
|
|
req[f.Key] = true
|
|
}
|
|
}
|
|
for _, k := range []string{"username", "password"} {
|
|
if !req[k] {
|
|
t.Errorf("config field %q should be required", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegistered(t *testing.T) {
|
|
// The plugin must self-register via init() so the manager can construct it.
|
|
var found bool
|
|
for _, v := range plugins.NewManager(t.TempDir()+"/plugins.json").List() {
|
|
if v.Name == "toyota" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("toyota not registered with the plugin manager")
|
|
}
|
|
}
|
|
|
|
func TestHMACSHA256(t *testing.T) {
|
|
// Mirrors pytoyoda: hmac.new(b"2.14.0", b"abc", sha256).hexdigest().
|
|
got := hmacSHA256("2.14.0", "abc")
|
|
if len(got) != 64 {
|
|
t.Fatalf("hex digest length = %d, want 64", len(got))
|
|
}
|
|
// Deterministic: same key/message → same digest.
|
|
if got != hmacSHA256("2.14.0", "abc") {
|
|
t.Fatal("hmac not deterministic")
|
|
}
|
|
// Key matters: a different client version changes the digest.
|
|
if got == hmacSHA256("9.9.9", "abc") {
|
|
t.Fatal("hmac ignored the key")
|
|
}
|
|
}
|
|
|
|
func TestJWTUUID(t *testing.T) {
|
|
payload := base64.RawURLEncoding.EncodeToString([]byte(`{"uuid":"abc-123","aud":"oneappsdkclient"}`))
|
|
token := "header." + payload + ".sig"
|
|
uuid, err := jwtUUID(token)
|
|
if err != nil {
|
|
t.Fatalf("jwtUUID: %v", err)
|
|
}
|
|
if uuid != "abc-123" {
|
|
t.Fatalf("uuid = %q, want abc-123", uuid)
|
|
}
|
|
|
|
if _, err := jwtUUID("not-a-jwt"); err == nil {
|
|
t.Error("expected error for malformed token")
|
|
}
|
|
}
|
|
|
|
func TestExtractCode(t *testing.T) {
|
|
loc := "com.toyota.oneapp:/oauth2Callback?code=AUTHCODE123&state=xyz"
|
|
if got := extractCode(loc); got != "AUTHCODE123" {
|
|
t.Fatalf("code = %q, want AUTHCODE123", got)
|
|
}
|
|
if got := extractCode("com.toyota.oneapp:/oauth2Callback"); got != "" {
|
|
t.Fatalf("expected empty code, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCallbackHelpers(t *testing.T) {
|
|
// A NameCallback shaped like ForgeRock's response.
|
|
var cb map[string]any
|
|
_ = json.Unmarshal([]byte(`{
|
|
"type":"NameCallback",
|
|
"output":[{"name":"prompt","value":"User Name"}],
|
|
"input":[{"name":"IDToken1","value":""}]
|
|
}`), &cb)
|
|
|
|
if output0(cb) != "User Name" {
|
|
t.Fatalf("output0 = %q", output0(cb))
|
|
}
|
|
setInput0(cb, "driver@example.com")
|
|
in := cb["input"].([]any)[0].(map[string]any)
|
|
if in["value"] != "driver@example.com" {
|
|
t.Fatalf("input value = %v, want driver@example.com", in["value"])
|
|
}
|
|
}
|
|
|
|
func TestInvokeRequiresVIN(t *testing.T) {
|
|
p := &Plugin{}
|
|
_ = p.Init(context.Background(), map[string]string{"username": "u", "password": "p"})
|
|
|
|
if _, err := p.Invoke(context.Background(), "telemetry", nil); err == nil ||
|
|
!strings.Contains(err.Error(), "requires a vin") {
|
|
t.Fatalf("expected vin-required error, got %v", err)
|
|
}
|
|
if _, err := p.Invoke(context.Background(), "bogus", nil); err == nil ||
|
|
!strings.Contains(err.Error(), "unknown action") {
|
|
t.Fatalf("expected unknown-action error, got %v", err)
|
|
}
|
|
}
|