Add bettervent.me provider with lazy-cached device list (~6,700 Eurovent-certified heat pumps), search API endpoint, device search UI with auto-populate, BTU/kW unit switcher for European users, and extended AC fields (SEER, SCOP, COP, TOL, Tbiv, refrigerant). Closes #2.
179 lines
4.4 KiB
Go
179 lines
4.4 KiB
Go
package bettervent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
var testDevices = []apiDevice{
|
|
{
|
|
ID: 1, TradeName: "MITSUBISHI ELECTRIC", ModelName: "MSZ-AP25VGK",
|
|
Range: "MSZ-AP series", Pc: 2.5, Ph: 3.2, EER: 4.0, COP: 4.6,
|
|
SEER: 8.7, SEERClass: "A+++", SCOP: 4.6, SCOPClass: "A++",
|
|
TOL: -25, Tbiv: -15, Mounting: "Wall mounted", Refrigerant: "R32",
|
|
},
|
|
{
|
|
ID: 2, TradeName: "DAIKIN", ModelName: "FTXM25R",
|
|
Range: "Perfera", Pc: 2.5, Ph: 3.0, EER: 3.8, COP: 4.5,
|
|
SEER: 8.73, SEERClass: "A+++", SCOP: 5.1, SCOPClass: "A+++",
|
|
TOL: -25, Tbiv: -15, Mounting: "Wall mounted", Refrigerant: "R32",
|
|
},
|
|
{
|
|
ID: 3, TradeName: "MITSUBISHI ELECTRIC", ModelName: "MFZ-KT50VG",
|
|
Range: "Floor standing", Pc: 5.0, Ph: 5.8, EER: 3.5, COP: 4.2,
|
|
SEER: 7.0, SEERClass: "A++", SCOP: 4.3, SCOPClass: "A++",
|
|
TOL: -15, Tbiv: -7, Mounting: "Floor standing", Refrigerant: "R410A",
|
|
},
|
|
}
|
|
|
|
func newTestServer(t *testing.T) *httptest.Server {
|
|
t.Helper()
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/devices" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(testDevices)
|
|
}))
|
|
}
|
|
|
|
func TestSearch_MatchesByTradeName(t *testing.T) {
|
|
ts := newTestServer(t)
|
|
defer ts.Close()
|
|
|
|
p := NewWithBaseURL(nil, ts.URL)
|
|
resp, err := p.Search(context.Background(), "daikin")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.Total != 1 {
|
|
t.Fatalf("got %d results, want 1", resp.Total)
|
|
}
|
|
if resp.Devices[0].TradeName != "DAIKIN" {
|
|
t.Errorf("got trade name %q, want DAIKIN", resp.Devices[0].TradeName)
|
|
}
|
|
if resp.Devices[0].CoolingKW != 2.5 {
|
|
t.Errorf("got cooling %v kW, want 2.5", resp.Devices[0].CoolingKW)
|
|
}
|
|
}
|
|
|
|
func TestSearch_MatchesByModelName(t *testing.T) {
|
|
ts := newTestServer(t)
|
|
defer ts.Close()
|
|
|
|
p := NewWithBaseURL(nil, ts.URL)
|
|
resp, err := p.Search(context.Background(), "MSZ-AP25")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.Total != 1 {
|
|
t.Fatalf("got %d results, want 1", resp.Total)
|
|
}
|
|
if resp.Devices[0].ID != 1 {
|
|
t.Errorf("got ID %d, want 1", resp.Devices[0].ID)
|
|
}
|
|
}
|
|
|
|
func TestSearch_MultipleMatches(t *testing.T) {
|
|
ts := newTestServer(t)
|
|
defer ts.Close()
|
|
|
|
p := NewWithBaseURL(nil, ts.URL)
|
|
resp, err := p.Search(context.Background(), "mitsubishi")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.Total != 2 {
|
|
t.Fatalf("got %d results, want 2", resp.Total)
|
|
}
|
|
}
|
|
|
|
func TestSearch_EmptyQuery(t *testing.T) {
|
|
ts := newTestServer(t)
|
|
defer ts.Close()
|
|
|
|
p := NewWithBaseURL(nil, ts.URL)
|
|
resp, err := p.Search(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.Total != 0 {
|
|
t.Errorf("got %d results for empty query, want 0", resp.Total)
|
|
}
|
|
}
|
|
|
|
func TestSearch_NoResults(t *testing.T) {
|
|
ts := newTestServer(t)
|
|
defer ts.Close()
|
|
|
|
p := NewWithBaseURL(nil, ts.URL)
|
|
resp, err := p.Search(context.Background(), "nonexistent-brand-xyz")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.Total != 0 {
|
|
t.Errorf("got %d results, want 0", resp.Total)
|
|
}
|
|
}
|
|
|
|
func TestSearch_CacheReuse(t *testing.T) {
|
|
callCount := 0
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(testDevices)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
p := NewWithBaseURL(nil, ts.URL)
|
|
ctx := context.Background()
|
|
|
|
_, _ = p.Search(ctx, "daikin")
|
|
_, _ = p.Search(ctx, "mitsubishi")
|
|
|
|
if callCount != 1 {
|
|
t.Errorf("expected 1 API call (cached), got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestSearch_FieldMapping(t *testing.T) {
|
|
ts := newTestServer(t)
|
|
defer ts.Close()
|
|
|
|
p := NewWithBaseURL(nil, ts.URL)
|
|
resp, err := p.Search(context.Background(), "FTXM25R")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.Total != 1 {
|
|
t.Fatalf("got %d results, want 1", resp.Total)
|
|
}
|
|
|
|
d := resp.Devices[0]
|
|
if d.SEER != 8.73 {
|
|
t.Errorf("SEER = %v, want 8.73", d.SEER)
|
|
}
|
|
if d.SEERClass != "A+++" {
|
|
t.Errorf("SEERClass = %q, want A+++", d.SEERClass)
|
|
}
|
|
if d.SCOP != 5.1 {
|
|
t.Errorf("SCOP = %v, want 5.1", d.SCOP)
|
|
}
|
|
if d.TOL != -25 {
|
|
t.Errorf("TOL = %v, want -25", d.TOL)
|
|
}
|
|
if d.Tbiv != -15 {
|
|
t.Errorf("Tbiv = %v, want -15", d.Tbiv)
|
|
}
|
|
if d.Refrigerant != "R32" {
|
|
t.Errorf("Refrigerant = %q, want R32", d.Refrigerant)
|
|
}
|
|
if d.Mounting != "Wall mounted" {
|
|
t.Errorf("Mounting = %q, want Wall mounted", d.Mounting)
|
|
}
|
|
}
|