| 1 | package billing |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "net/http" |
| 6 | "net/http/httptest" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | // A DeepSeek-shaped response parses, exposes Available, and Display prefers CNY |
| 11 | // with the right symbol; the request carries the bearer key. |
| 12 | func TestFetchDeepSeekShape(t *testing.T) { |
| 13 | const body = `{ |
| 14 | "is_available": true, |
| 15 | "balance_infos": [ |
| 16 | {"currency": "USD", "total_balance": "15.30", "granted_balance": "0.00", "topped_up_balance": "15.30"}, |
| 17 | {"currency": "CNY", "total_balance": "110.00", "granted_balance": "10.00", "topped_up_balance": "100.00"} |
| 18 | ] |
| 19 | }` |
| 20 | var gotAuth string |
| 21 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 22 | gotAuth = r.Header.Get("Authorization") |
| 23 | w.Header().Set("Content-Type", "application/json") |
| 24 | _, _ = w.Write([]byte(body)) |
| 25 | })) |
| 26 | defer srv.Close() |
| 27 | |
| 28 | b, err := Fetch(context.Background(), srv.URL, "secret-key") |
| 29 | if err != nil { |
| 30 | t.Fatalf("Fetch: %v", err) |
| 31 | } |
| 32 | if b == nil || !b.Available { |
| 33 | t.Fatalf("want available balance, got %+v", b) |
| 34 | } |
| 35 | if gotAuth != "Bearer secret-key" { |
| 36 | t.Errorf("Authorization = %q, want %q", gotAuth, "Bearer secret-key") |
| 37 | } |
| 38 | if len(b.Infos) != 2 { |
| 39 | t.Fatalf("want 2 infos, got %d", len(b.Infos)) |
| 40 | } |
| 41 | // Display prefers CNY → "¥110.00", not the first (USD) entry. |
| 42 | if got := b.Display(); got != "¥110.00" { |
| 43 | t.Errorf("Display = %q, want %q", got, "¥110.00") |
| 44 | } |
| 45 | if got := b.DisplayForCurrency("USD"); got != "$15.30" { |
| 46 | t.Errorf("DisplayForCurrency(USD) = %q, want %q", got, "$15.30") |
| 47 | } |
| 48 | if got := b.DisplayForCurrency("¥"); got != "¥110.00" { |
| 49 | t.Errorf("DisplayForCurrency(¥) = %q, want %q", got, "¥110.00") |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | // An empty url is "not configured", not an error: (nil, nil), and Display on a nil |
| 54 | // balance is "". |
| 55 | func TestFetchEmptyURL(t *testing.T) { |
| 56 | b, err := Fetch(context.Background(), "", "key") |
| 57 | if err != nil || b != nil { |
| 58 | t.Fatalf("Fetch(\"\") = (%v, %v), want (nil, nil)", b, err) |
| 59 | } |
| 60 | if got := b.Display(); got != "" { |
| 61 | t.Errorf("nil Display = %q, want empty", got) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | // A non-200 surfaces an error rather than a bogus zero balance. |
| 66 | func TestFetchHTTPError(t *testing.T) { |
| 67 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 68 | w.WriteHeader(http.StatusUnauthorized) |
| 69 | _, _ = w.Write([]byte(`{"error":"invalid key"}`)) |
| 70 | })) |
| 71 | defer srv.Close() |
| 72 | if _, err := Fetch(context.Background(), srv.URL, "bad"); err == nil { |
| 73 | t.Fatal("want error on 401, got nil") |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // Display falls back to the first currency when no CNY entry is present, and maps |
| 78 | // USD to "$". |
| 79 | func TestDisplayUSDOnly(t *testing.T) { |
| 80 | b := &Balance{Available: true, Infos: []Info{{Currency: "USD", TotalBalance: "9.99"}}} |
| 81 | if got := b.Display(); got != "$9.99" { |
| 82 | t.Errorf("Display = %q, want %q", got, "$9.99") |
| 83 | } |
| 84 | if got := b.DisplayForCurrency("CNY"); got != "USD $9.99" { |
| 85 | t.Errorf("DisplayForCurrency(CNY) = %q, want explicit real fallback currency %q", got, "USD $9.99") |
| 86 | } |
| 87 | } |
| 88 |