package services import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "nearle/models" ) /* The health score on the product screen. Most of this file is about what the score REFUSES to say. The number itself is the service's; the judgements around it — which band, whether the match is sure enough to state plainly, whether the product is even food — are decisions this code makes on a shopper's behalf, and each one has a way of being wrong that matters more than being absent. The fixture is the service's own record for Balaji Wafers Simply Salted, read from mcp.nearle.ai.in on 29 Sep 2026: a real score of 65.3 on a real product in a real tenant's catalogue, matched at 0.607 confidence — below the line. */ const balajiJSON = `{ "data_status": "verified", "category": "Snacks", "health_score": 65.3, "nutrition_score": 61.0, "match_confidence": 0.607, "positive_insights": ["Good source of fibre (9.3 g per 100 g)."], "nutritional_cautions": ["High in saturated fat (14.4 g per 100 g)."], "diet_tags": ["Vegetarian"], "allergens": [], "data_source": "openfoodfacts", "source_url": "https://world.openfoodfacts.org/product/1", "calories_kcal": 545, "protein_g": 7.5, "total_fat_g": 30.7 }` func healthOf(t *testing.T, raw string) *models.HealthScore { t.Helper() var source NutritionSource if err := json.Unmarshal([]byte(raw), &source); err != nil { t.Fatalf("fixture: %v", err) } return source.Health() } func TestARealScoreReachesTheApp(t *testing.T) { score := healthOf(t, balajiJSON) if score == nil { t.Fatal("no score from a verified, edible, scored product") } if score.Score != 65 { // Rounded: the service sends 65.3 and nobody reads the decimal. t.Errorf("score = %d, want 65", score.Score) } if score.Band != "good" || score.Label != "Healthy" { t.Errorf("band = %q / %q", score.Band, score.Label) } if len(score.Positives) != 1 || len(score.Cautions) != 1 { t.Errorf("insights lost: %+v / %+v", score.Positives, score.Cautions) } if score.Source == nil || score.Source.Label != "openfoodfacts" { t.Errorf("source lost: %+v", score.Source) } } /* ── What it refuses to say ──────────────────────────────────────────────── */ func TestSomethingThatIsNotFoodGetsNoScore(t *testing.T) { // The reason this guard exists. Measured 4 Sep 2026, the upstream // per-product endpoint rated Godrej Hit insecticide 80/100 with // data_status "verified"; soap and shampoo both scored 37.5 the same way. // Those records now read "unavailable", and the guard stays: this tenant // sells soap and toothpaste next to its biscuits. for _, category := range []string{"Household", "Personal Care", "Insecticide", "Cosmetics"} { raw := strings.Replace(balajiJSON, `"category": "Snacks"`, `"category": "`+category+`"`, 1) if score := healthOf(t, raw); score != nil { t.Errorf("%s was rated %d/100", category, score.Score) } } } func TestAnUnknownCategoryIsTreatedAsNotFood(t *testing.T) { // An allowlist, because the two failure modes are not symmetric. Withholding // a badge from real food costs a shopper something they never had; putting // one on bleach is a different order of mistake. for _, category := range []string{`null`, `""`, `"General"`, `"Miscellaneous"`} { raw := strings.Replace(balajiJSON, `"category": "Snacks"`, `"category": `+category, 1) if score := healthOf(t, raw); score != nil { t.Errorf("category %s was scored %d/100", category, score.Score) } } } func TestAnUnscoredProductGetsNoScore(t *testing.T) { // The common case: the service knows the product and has not rated it. raw := strings.Replace(balajiJSON, `"health_score": 65.3`, `"health_score": null`, 1) if score := healthOf(t, raw); score != nil { t.Fatalf("invented a score: %+v", score) } } /* ── Saying how sure it is ───────────────────────────────────────────────── */ func TestAWeakMatchIsDeclared(t *testing.T) { // 0.607 is below the 0.7 line. A nutrition table presented as fact on a 61% // match is a claim the data does not support. score := healthOf(t, balajiJSON) if score.Caveat == "" { t.Fatal("a 0.607 match was presented as fact") } if !strings.Contains(score.Caveat, "61%") { t.Errorf("the caveat does not say how sure: %q", score.Caveat) } } func TestAStrongMatchNeedsNoApology(t *testing.T) { raw := strings.Replace(balajiJSON, `"match_confidence": 0.607`, `"match_confidence": 0.94`, 1) if score := healthOf(t, raw); score.Caveat != "" { t.Errorf("apologised for a 94%% match: %q", score.Caveat) } } func TestAnEmptyAllergenListOnAWeakMatchIsFlagged(t *testing.T) { // The one that could actually hurt somebody. The service matches sources // down to 0.32 confidence, and "verified" speaks to the numbers being real, // not to the record being THIS product. Silence must not stand in for // "contains none" — the app has to say "not confirmed" rather than draw // nothing. score := healthOf(t, balajiJSON) if !score.Allergensunconfirmed { t.Fatal("an empty allergen list on a 61% match was left to speak for itself") } } func TestADeclaredAllergenIsAlwaysShown(t *testing.T) { // A false positive sends somebody to read the packet. A false negative sends // them to hospital. So a declared allergen survives a weak match. raw := strings.Replace(balajiJSON, `"allergens": []`, `"allergens": ["Milk", "Soy"]`, 1) score := healthOf(t, raw) if len(score.Allergens) != 2 { t.Fatalf("a declared allergen was dropped: %+v", score.Allergens) } if score.Allergensunconfirmed { t.Error("a list with entries in it was flagged as unconfirmed") } } func TestAnEmptyListOnAStrongMatchIsNotFlagged(t *testing.T) { raw := strings.Replace(balajiJSON, `"match_confidence": 0.607`, `"match_confidence": 0.94`, 1) if healthOf(t, raw).Allergensunconfirmed { t.Error("flagged an empty list that the match actually supports") } } /* ── Bands ───────────────────────────────────────────────────────────────── */ func TestTheBandThresholdsAreTheServices(t *testing.T) { // Confirmed by that team in writing. The per-product endpoint sends no // band, so these are the only path — picking our own would be one API // disagreeing with itself depending which endpoint a screen called. for _, tc := range []struct { score float64 band string label string }{ {92, "excellent", "Very healthy"}, {80, "excellent", "Very healthy"}, {79.9, "good", "Healthy"}, {60, "good", "Healthy"}, {59.9, "fair", "Okay"}, {40, "fair", "Okay"}, {39.9, "poor", "Less healthy"}, {0, "poor", "Less healthy"}, } { band := models.BandFor(tc.score, "") if band != tc.band || models.BandLabel(band) != tc.label { t.Errorf("%v → %q/%q, want %q/%q", tc.score, band, models.BandLabel(band), tc.band, tc.label) } } } func TestTheServicesOwnBandWinsWhenItSendsOne(t *testing.T) { // It does not today, from this endpoint. When it starts, its answer is the // authority and the thresholds above become the fallback they were meant to // be. if got := models.BandFor(20, "excellent"); got != "excellent" { t.Errorf("ignored the service's own band: %q", got) } if got := models.BandFor(20, " GOOD "); got != "good" { t.Errorf("did not normalise the service's band: %q", got) } if got := models.BandFor(85, "nonsense"); got != "excellent" { t.Errorf("trusted a band it does not recognise: %q", got) } } /* ── On the wire ─────────────────────────────────────────────────────────── */ func TestTheScoreAndThePanelAreIndependent(t *testing.T) { // A product can be scored with no figures published, and carry figures with // no score. Tying them together would blank one because the other was // missing. noFigures := `{"category":"Snacks","health_score":65.3,"match_confidence":0.9}` var source NutritionSource if err := json.Unmarshal([]byte(noFigures), &source); err != nil { t.Fatalf("fixture: %v", err) } if source.Panel() != nil { t.Error("made a panel with no figures") } if source.Health() == nil { t.Error("withheld a score because there were no figures") } } func TestBothHalvesComeFromOneRequest(t *testing.T) { // Splitting a record the service returns whole would double the traffic to // a third party on a screen a shopper is waiting on. var hits int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/brands" { _, _ = w.Write([]byte(`{"brands":["Balaji"]}`)) return } hits++ _, _ = w.Write([]byte(balajiJSON)) })) defer server.Close() found := NewNutritionService(server.URL).ForProduct("balaji", "balaji_wafers_135g") if hits != 1 { t.Fatalf("made %d requests for one product", hits) } if !found.Panel.HasValues() || found.Health == nil { t.Fatal("one of the two halves was lost") } } func TestTheWireNamesForTheScore(t *testing.T) { row := models.Products{Productid: 7101} row.Healthscore = healthOf(t, balajiJSON) encoded, err := json.Marshal(row) if err != nil { t.Fatalf("marshal: %v", err) } var out map[string]any if err := json.Unmarshal(encoded, &out); err != nil { t.Fatalf("unmarshal: %v", err) } health, ok := out["healthscore"].(map[string]any) if !ok { t.Fatalf("no `healthscore` key: %s", encoded) } for _, key := range []string{"score", "band", "label", "caveat", "allergensunconfirmed"} { if _, ok := health[key]; !ok { t.Errorf("missing %q: %v", key, health) } } } func TestAProductWithNoScoreHasNoKeyAtAll(t *testing.T) { // Not `"healthscore": null`. Most products have none. encoded, _ := json.Marshal(models.Products{Productid: 1}) var out map[string]any _ = json.Unmarshal(encoded, &out) if _, present := out["healthscore"]; present { t.Fatalf("an absent score was sent as a key: %s", encoded) } } func TestSoapOnTheProductScreenCarriesNoScore(t *testing.T) { // End to end, through the decoration the endpoint actually calls. This is // the failure a shopper would see: a health rating on a bar of soap. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/brands" { _, _ = w.Write([]byte(`{"brands":["Godrej"]}`)) return } _, _ = w.Write([]byte(strings.Replace(balajiJSON, `"category": "Snacks"`, `"category": "Personal Care"`, 1))) })) defer server.Close() rows := []models.Products{{Productid: 7121, Productbrand: "godrej", Imageid: "godrej_soap_100g"}} decorateNutrition(NewNutritionService(server.URL), rows) if rows[0].Healthscore != nil { t.Fatalf("a bar of soap was rated %d/100", rows[0].Healthscore.Score) } }