package api import "testing" // What a car's page shows — which tabs, which rows of the Information tab, which // columns of the Service history table, and the order each of those is laid out // in — is stored on the car as key lists, so the validation has to keep them to // keys the page actually renders. Two keys stay out of their hideable set: // Information, because a car with no tabs left would be a dead end, and the // service Date, because a history with the day taken out is not one. func TestNormalizeHiddenTabs(t *testing.T) { got, err := normalizeKeys([]string{" fuel ", "parts", "fuel", ""}, hideableCarTabs, "tab") if err != nil { t.Fatalf("normalizeKeys: %v", err) } want := []string{"fuel", "parts"} // trimmed, blanks dropped, deduped assertKeys(t, got, want) // Clearing the list is how a car goes back to showing everything. if empty, err := normalizeKeys(nil, hideableCarTabs, "tab"); err != nil || len(empty) != 0 { t.Errorf("normalizeKeys(nil) = %v, %v; want empty and no error", empty, err) } // Information is the car itself; hiding it would leave a page with no tabs. if _, err := normalizeKeys([]string{"info"}, hideableCarTabs, "tab"); err == nil { t.Error("normalizeKeys allowed hiding the info tab, want an error") } // A key from a stale or wrong client is an error, not something to drop // quietly while the tab stays visible. if _, err := normalizeKeys([]string{"fuel", "nonsense"}, hideableCarTabs, "tab"); err == nil { t.Error("normalizeKeys accepted an unknown tab, want an error") } // The hideable set is the contract the web app's HIDEABLE_TABS mirrors: // every tab the car page renders beside Information. for _, key := range []string{ "provider", "services", "technical", "maintenance", "fuel", "charging", "documents", "parts", "reminders", } { if !hideableCarTabs[key] { t.Errorf("tab %q should be hideable", key) } } if len(hideableCarTabs) != 9 { t.Errorf("hideableCarTabs has %d entries, want the 9 tabs beside Information", len(hideableCarTabs)) } } func TestNormalizeHiddenFields(t *testing.T) { got, err := normalizeKeys([]string{"vin", " differentialOil ", "vin"}, hideableCarFields, "field") if err != nil { t.Fatalf("normalizeKeys: %v", err) } assertKeys(t, got, []string{"vin", "differentialOil"}) if _, err := normalizeKeys([]string{"oilSpec", "nonsense"}, hideableCarFields, "field"); err == nil { t.Error("normalizeKeys accepted an unknown field, want an error") } // A tab key is not a field key — the two sets are validated separately. if _, err := normalizeKeys([]string{"fuel"}, hideableCarFields, "field"); err == nil { t.Error("normalizeKeys accepted a tab key as a field, want an error") } // Every Information row the web app renders must be hideable; unlike the // tabs there is no row the page has to keep. for _, key := range []string{ "oilSpec", "transmissionOil", "differentialOil", "brakeFluid", "coolant", "odometer", "serviceInterval", "nextDue", "registrationPlate", "registrationCountry", "vin", "fuelType", "buildDate", "firstRegistration", } { if !hideableCarFields[key] { t.Errorf("field %q should be hideable", key) } } if len(hideableCarFields) != 14 { t.Errorf("hideableCarFields has %d entries, want the 14 Information rows", len(hideableCarFields)) } } // The tabs arrange against a wider set than they hide against: Information // cannot be switched off, but it can be moved off the front of the bar. func TestNormalizeTabOrder(t *testing.T) { got, err := normalizeKeys([]string{"reminders", "info", "fuel"}, arrangeableCarTabs, "tab") if err != nil { t.Fatalf("normalizeKeys: %v", err) } assertKeys(t, got, []string{"reminders", "info", "fuel"}) // Everything the bar renders has to be arrangeable — the hideable tabs plus // Information, and nothing else. for key := range hideableCarTabs { if !arrangeableCarTabs[key] { t.Errorf("tab %q should be arrangeable", key) } } if !arrangeableCarTabs["info"] { t.Error("the info tab should be arrangeable even though it cannot be hidden") } if len(arrangeableCarTabs) != len(hideableCarTabs)+1 { t.Errorf("arrangeableCarTabs has %d entries, want the hideable tabs plus Information", len(arrangeableCarTabs)) } // A field key is not a tab key, and an invented tab is still an error. if _, err := normalizeKeys([]string{"vin"}, arrangeableCarTabs, "tab"); err == nil { t.Error("normalizeKeys accepted a field key as a tab, want an error") } if _, err := normalizeKeys([]string{"info", "nonsense"}, arrangeableCarTabs, "tab"); err == nil { t.Error("normalizeKeys accepted an unknown tab in an arrangement, want an error") } } // The arrangement of the Information rows shares the field key set — every row // can be moved — but not the meaning: here the order of the list is the point, // so it has to survive validation exactly as it was sent. func TestNormalizeFieldOrder(t *testing.T) { got, err := normalizeKeys([]string{"vin", "odometer", "oilSpec"}, hideableCarFields, "field") if err != nil { t.Fatalf("normalizeKeys: %v", err) } assertKeys(t, got, []string{"vin", "odometer", "oilSpec"}) // A key repeated by a client that lost track keeps its first position; a // second entry for the same row would put it in two places at once. got, err = normalizeKeys([]string{"vin", "odometer", "vin"}, hideableCarFields, "field") if err != nil { t.Fatalf("normalizeKeys: %v", err) } assertKeys(t, got, []string{"vin", "odometer"}) // A partial arrangement is fine — the rows it leaves out follow the arranged // ones — but an invented row is still an error. if _, err := normalizeKeys([]string{"vin", "nonsense"}, hideableCarFields, "field"); err == nil { t.Error("normalizeKeys accepted an unknown field in an arrangement, want an error") } } // The connected service's headline readings arrange the same way, against the // key set derived from what that panel renders. func TestNormalizeMetricOrder(t *testing.T) { got, err := normalizeKeys([]string{"evRangeWithAc", "odometer"}, arrangeableCarMetrics, "reading") if err != nil { t.Fatalf("normalizeKeys: %v", err) } assertKeys(t, got, []string{"evRangeWithAc", "odometer"}) // Every reading the panel can show has to be arrangeable, the two that are // not measures included. for _, key := range []string{ "odometer", "fuelLevel", "fuelRange", "batteryLevel", "evRange", "evRangeWithAc", "chargingStatus", "location", } { if !arrangeableCarMetrics[key] { t.Errorf("reading %q should be arrangeable", key) } } // A field key is not a reading key — a car's two arrangements are separate. if _, err := normalizeKeys([]string{"vin"}, arrangeableCarMetrics, "reading"); err == nil { t.Error("normalizeKeys accepted a field key as a reading, want an error") } } func TestNormalizeHiddenServiceColumns(t *testing.T) { got, err := normalizeKeys([]string{" parts ", "notes", "parts", ""}, hideableServiceColumns, "service column") if err != nil { t.Fatalf("normalizeKeys: %v", err) } assertKeys(t, got, []string{"parts", "notes"}) // trimmed, blanks dropped, deduped // The date is what a service record is; a table of them without it would be // a list of unattributed work. if _, err := normalizeKeys([]string{"date"}, hideableServiceColumns, "service column"); err == nil { t.Error("normalizeKeys allowed hiding the date column, want an error") } // Neither a field key nor an invented one passes: each set is its own. if _, err := normalizeKeys([]string{"vin"}, hideableServiceColumns, "service column"); err == nil { t.Error("normalizeKeys accepted a field key as a service column, want an error") } if _, err := normalizeKeys([]string{"parts", "nonsense"}, hideableServiceColumns, "service column"); err == nil { t.Error("normalizeKeys accepted an unknown service column, want an error") } // The parts are one column, not one each: a key per part would put the table // back where it started, and these three were columns of their own once. for _, key := range []string{"oil", "engineFilter", "cabinFilter"} { if hideableServiceColumns[key] { t.Errorf("%q should not be a column of its own — the parts share one", key) } } // The hideable set is the contract the web app's HIDEABLE_SERVICE_COLUMNS // mirrors: every column that table renders beside the date. for _, key := range []string{"km", "nextDate", "nextKm", "parts", "notes", "file"} { if !hideableServiceColumns[key] { t.Errorf("service column %q should be hideable", key) } } if len(hideableServiceColumns) != 6 { t.Errorf("hideableServiceColumns has %d entries, want the 6 columns beside the date", len(hideableServiceColumns)) } } // The parts a service can change hide against their own set — one key per // boolean on the service_records collection, and no order beside it, because // they are a checkbox list inside a single column. func TestNormalizeHiddenServiceParts(t *testing.T) { got, err := normalizeKeys([]string{" oil ", "cabinFilter", "oil", ""}, hideableServiceParts, "service part") if err != nil { t.Fatalf("normalizeKeys: %v", err) } assertKeys(t, got, []string{"oil", "cabinFilter"}) // trimmed, blanks dropped, deduped // Every part is hideable, unlike the tabs and the columns: a service that // changed nothing is a real service, so there is no part the form must keep. for _, key := range []string{"oil", "engineFilter", "cabinFilter"} { if !hideableServiceParts[key] { t.Errorf("service part %q should be hideable", key) } } if len(hideableServiceParts) != 3 { t.Errorf("hideableServiceParts has %d entries, want the 3 booleans on a service record", len(hideableServiceParts)) } // "parts" is the column those three share; it is not one of them. Sending // the column key here would hide nothing and quietly succeed if the two sets // were not kept apart. if _, err := normalizeKeys([]string{"parts"}, hideableServiceParts, "service part"); err == nil { t.Error("normalizeKeys accepted the parts column as a part, want an error") } if _, err := normalizeKeys([]string{"notes"}, hideableServiceParts, "service part"); err == nil { t.Error("normalizeKeys accepted a column key as a service part, want an error") } if _, err := normalizeKeys([]string{"oil", "nonsense"}, hideableServiceParts, "service part"); err == nil { t.Error("normalizeKeys accepted an unknown service part, want an error") } // And no part is a column, which is what keeps the table from widening as // parts are added. for key := range hideableServiceParts { if arrangeableServiceColumns[key] { t.Errorf("service part %q must not also be a column", key) } } } // The columns arrange against a wider set than they hide against, the way the // tabs do: the date cannot be switched off, but it can be moved off the left. func TestNormalizeServiceColumnOrder(t *testing.T) { got, err := normalizeKeys([]string{"notes", "date", "parts"}, arrangeableServiceColumns, "service column") if err != nil { t.Fatalf("normalizeKeys: %v", err) } assertKeys(t, got, []string{"notes", "date", "parts"}) for key := range hideableServiceColumns { if !arrangeableServiceColumns[key] { t.Errorf("service column %q should be arrangeable", key) } } if !arrangeableServiceColumns["date"] { t.Error("the date column should be arrangeable even though it cannot be hidden") } if len(arrangeableServiceColumns) != len(hideableServiceColumns)+1 { t.Errorf("arrangeableServiceColumns has %d entries, want the hideable columns plus the date", len(arrangeableServiceColumns)) } // A partial arrangement is fine — the columns it leaves out follow the // arranged ones — but an invented column is still an error. if _, err := normalizeKeys([]string{"date", "nonsense"}, arrangeableServiceColumns, "service column"); err == nil { t.Error("normalizeKeys accepted an unknown column in an arrangement, want an error") } } func assertKeys(t *testing.T, got, want []string) { t.Helper() if len(got) != len(want) { t.Fatalf("keys = %v, want %v", got, want) } for i := range got { if got[i] != want[i] { t.Fatalf("keys = %v, want %v", got, want) } } }