package services import ( "encoding/json" "net/http" "net/http/httptest" "testing" "nearle/models" "nearle/repositories" ) /* Whether a shop shows a product's health score. The score comes from a third party matching a reference product by name, often at under 60% confidence, and the figures it is derived from are sometimes wrong in ways a shopkeeper can see at a glance — one live record reports under 1mg of sodium per 100g for salted crisps. So a merchant can take the rating off their own shelf. Two rules run through all of this: - it hides the SCORE and never the nutrition. The figures are what the label states; the score is somebody's judgement of them, and a merchant disputing the judgement is not disputing the grams. - silence means yes. Every product imported before this existed, and every caller that does not know about it, must keep showing what they show today. */ func withScore(t *testing.T) *httptest.Server { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/brands" { _, _ = w.Write([]byte(`{"brands":["Balaji"]}`)) return } _, _ = w.Write([]byte(balajiJSON)) })) t.Cleanup(server.Close) return server } func TestAShopThatShowsTheScoreGetsBoth(t *testing.T) { rows := []models.Products{{ Productid: 7101, Productbrand: "balaji", Imageid: "balaji_wafers_135g", Showhealthscore: true, }} decorateNutrition(NewNutritionService(withScore(t).URL), rows) if rows[0].Healthscore == nil { t.Fatal("the score was withheld from a shop that shows it") } if !rows[0].Nutrition.HasValues() { t.Fatal("the figures went missing") } } func TestTurningTheScoreOffLeavesTheFiguresAlone(t *testing.T) { // The whole shape of this feature. A merchant saying "that rating does not // describe what I sell" has said nothing about the protein content. rows := []models.Products{{ Productid: 7101, Productbrand: "balaji", Imageid: "balaji_wafers_135g", Showhealthscore: false, }} decorateNutrition(NewNutritionService(withScore(t).URL), rows) if rows[0].Healthscore != nil { t.Fatalf("the score was shown by a shop that turned it off: %+v", rows[0].Healthscore) } if !rows[0].Nutrition.HasValues() { t.Fatal("turning off the score also removed the nutrition figures") } } func TestTheHiddenScoreIsAbsentAndNotNull(t *testing.T) { // Same contract as the rest of this endpoint: a missing key means "nothing // to show", and the app already reads it that way. `"healthscore": null` // would be a new shape for a client that is already written. row := models.Products{Productid: 7101, Showhealthscore: false} 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) } if _, present := out["healthscore"]; present { t.Fatalf("a hidden score was sent as a key: %s", encoded) } } func TestTwoShopsSellingTheSameProductDecideSeparately(t *testing.T) { // The flag is on the tenant's product row, not the catalogue, so one shop // hiding a rating cannot take it off anybody else's shelf. rows := []models.Products{ {Productid: 7101, Productbrand: "balaji", Imageid: "balaji_wafers_135g", Showhealthscore: true}, {Productid: 8202, Productbrand: "balaji", Imageid: "balaji_wafers_135g", Showhealthscore: false}, } decorateNutrition(NewNutritionService(withScore(t).URL), rows) if rows[0].Healthscore == nil { t.Error("the shop that shows it lost its score") } if rows[1].Healthscore != nil { t.Error("the shop that hid it got one anyway") } } /* ── Silence means yes ───────────────────────────────────────────────────── */ func TestAnImportThatSaysNothingKeepsTheScore(t *testing.T) { // Every console that predates this field omits it. Reading that as a // deliberate "no" would strip the score from every import made by an older // build — a change nobody asked for, arriving as a deploy. var req models.ImportCatalogueProductRequest if err := json.Unmarshal([]byte(`{"tenantid":1147,"productid":7101}`), &req); err != nil { t.Fatalf("unmarshal: %v", err) } if req.Showhealthscore != nil { t.Fatalf("an absent field was read as a value: %v", *req.Showhealthscore) } // This is the expression the import uses. if show := req.Showhealthscore == nil || *req.Showhealthscore; !show { t.Fatal("an import that said nothing turned the score off") } } func TestAnImportCanSayNoOutright(t *testing.T) { // And the other half: `false` has to survive. A bool that cannot be set to // false is a toggle that only switches on. var req models.ImportCatalogueProductRequest if err := json.Unmarshal([]byte(`{"showhealthscore":false}`), &req); err != nil { t.Fatalf("unmarshal: %v", err) } if req.Showhealthscore == nil || *req.Showhealthscore { t.Fatalf("an explicit no was lost: %v", req.Showhealthscore) } if show := req.Showhealthscore == nil || *req.Showhealthscore; show { t.Fatal("an explicit no was read as yes") } } func TestAnImportCanSayYesOutright(t *testing.T) { var req models.ImportCatalogueProductRequest if err := json.Unmarshal([]byte(`{"showhealthscore":true}`), &req); err != nil { t.Fatalf("unmarshal: %v", err) } if req.Showhealthscore == nil || !*req.Showhealthscore { t.Fatalf("an explicit yes was lost: %v", req.Showhealthscore) } } /* ── The write ───────────────────────────────────────────────────────────── */ type showScoreRepo struct { repositories.ProductRepository tenantID, productID int show bool calls int } func (r *showScoreRepo) SetShowHealthScore(tenantID, productID int, show bool) error { r.calls++ r.tenantID, r.productID, r.show = tenantID, productID, show return nil } func TestTurningItOffReachesTheRepositoryAsFalse(t *testing.T) { // GORM's struct update skips zero values, so `false` is the value most // likely to be silently dropped on its way to the database — which would // read to a merchant as "it does not save". repo := &showScoreRepo{} service := &productService{repo: repo} if err := service.SetShowHealthScore(1147, 7101, false); err != nil { t.Fatalf("SetShowHealthScore: %v", err) } if repo.calls != 1 || repo.tenantID != 1147 || repo.productID != 7101 { t.Fatalf("wrong call: %+v", repo) } if repo.show { t.Fatal("false arrived as true") } } func TestAWriteWithNoTenantIsRefusedBeforeItReachesTheDatabase(t *testing.T) { // A tenantid of 0 in an UPDATE's WHERE clause matches nothing here, but the // habit of letting one through is how an unscoped write eventually ships. repo := &showScoreRepo{} service := &productService{repo: repo} if err := service.SetShowHealthScore(0, 7101, false); err == nil { t.Fatal("an unscoped write was accepted") } if repo.calls != 0 { t.Fatal("it reached the repository anyway") } }