149 lines
5.0 KiB
Go
149 lines
5.0 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"nearle/models"
|
|
)
|
|
|
|
/*
|
|
What the product screen receives.
|
|
|
|
The app renders a nutrition panel off `getproductbyvariant`. These are about the
|
|
contract that screen is built on — the exact field names, and what happens for
|
|
the overwhelming majority of products, which have no structured nutrition at all
|
|
and are not going to for a while.
|
|
*/
|
|
|
|
func TestTheStructuredPanelIsUsedWhereItExists(t *testing.T) {
|
|
facts := `{"nutrition":{"per":"100g","servingsize":"30g","items":[
|
|
{"name":"Energy","value":520,"unit":"kcal"},
|
|
{"name":"Protein","value":6.5,"unit":"g"}]}}`
|
|
|
|
panel := nutritionFromFacts(facts)
|
|
if !panel.HasValues() {
|
|
t.Fatal("no panel")
|
|
}
|
|
if panel.Per != "100g" || panel.Servingsize != "30g" {
|
|
t.Fatalf("basis lost: per=%q servingsize=%q", panel.Per, panel.Servingsize)
|
|
}
|
|
if len(panel.Items) != 2 || panel.Items[1].Value != 6.5 {
|
|
t.Fatalf("items wrong: %+v", panel.Items)
|
|
}
|
|
}
|
|
|
|
func TestTheOldNutrientLinesStillProduceAPanel(t *testing.T) {
|
|
// This is the case that actually matters on day one: every product on the
|
|
// platform carries display lines and nothing else. Without this the app
|
|
// ships a nutrition screen that is empty for every product in every shop.
|
|
panel := nutritionFromFacts(`{"nutrients":["Energy 350kcal","Protein 7g"]}`)
|
|
if !panel.HasValues() {
|
|
t.Fatal("the lines were not used")
|
|
}
|
|
if panel.Items[0].Name != "Energy" || panel.Items[0].Value != 350 {
|
|
t.Fatalf("got %+v", panel.Items[0])
|
|
}
|
|
}
|
|
|
|
func TestStructuredBeatsLinesWhenBothArePresent(t *testing.T) {
|
|
// The import keeps both — the console shows the lines, the app shows the
|
|
// panel. The structured one is the agent team's work and carries a basis,
|
|
// so it wins.
|
|
facts := `{"nutrients":["Energy 350kcal"],
|
|
"nutrition":{"per":"100g","items":[{"name":"Energy","value":520,"unit":"kcal"}]}}`
|
|
|
|
panel := nutritionFromFacts(facts)
|
|
if panel.Per != "100g" || panel.Items[0].Value != 520 {
|
|
t.Fatalf("the derived panel won: %+v", panel)
|
|
}
|
|
}
|
|
|
|
func TestNothingKnownMeansNoPanel(t *testing.T) {
|
|
for _, facts := range []string{"", " ", "{}", `{"highlights":["Crunchy"]}`} {
|
|
if p := nutritionFromFacts(facts); p != nil {
|
|
t.Errorf("%q produced a panel: %+v", facts, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAMalformedSnapshotCostsThePanelAndNothingElse(t *testing.T) {
|
|
// One bad blob must not fail the response for the variant group it is in —
|
|
// the shopper tapped a product and is owed the screen.
|
|
if p := nutritionFromFacts(`{"nutrition":{"items":`); p != nil {
|
|
t.Fatalf("read a panel out of broken JSON: %+v", p)
|
|
}
|
|
}
|
|
|
|
func TestAPanelWithNoRowsIsNotSent(t *testing.T) {
|
|
// `items: []` renders as an empty box, which a shopper reads as "this food
|
|
// has no nutrition" rather than "we do not know".
|
|
if p := nutritionFromFacts(`{"nutrition":{"per":"100g","items":[]}}`); p != nil {
|
|
t.Fatalf("sent an empty panel: %+v", p)
|
|
}
|
|
}
|
|
|
|
func TestEveryRowInAVariantGroupIsDecorated(t *testing.T) {
|
|
// A shopper switching from 500ml to 1L must not watch the nutrition vanish.
|
|
rows := []models.Products{
|
|
{Productid: 1, Cataloguefacts: `{"nutrients":["Energy 350kcal"]}`},
|
|
{Productid: 2, Cataloguefacts: `{"nutrition":{"per":"100g","items":[{"name":"Energy","value":700,"unit":"kcal"}]}}`},
|
|
{Productid: 3},
|
|
}
|
|
decorateNutrition(rows)
|
|
|
|
if !rows[0].Nutrition.HasValues() || !rows[1].Nutrition.HasValues() {
|
|
t.Fatal("a sibling was left without its panel")
|
|
}
|
|
if rows[2].Nutrition != nil {
|
|
t.Fatal("invented a panel for a product with no facts")
|
|
}
|
|
}
|
|
|
|
func TestTheWireNamesAreTheOnesTheAppAskedFor(t *testing.T) {
|
|
// An API is a promise to a client already written against it. `servingsize`
|
|
// is not how the rest of this file would spell it, and it is what was asked
|
|
// for, which outranks house style.
|
|
rows := []models.Products{{
|
|
Productid: 7093,
|
|
Cataloguefacts: `{"nutrition":{"per":"100g","servingsize":"30g","items":[{"name":"Energy","value":520,"unit":"kcal"}]}}`,
|
|
}}
|
|
decorateNutrition(rows)
|
|
|
|
encoded, err := json.Marshal(rows[0])
|
|
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)
|
|
}
|
|
|
|
nutrition, ok := out["nutrition"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("no `nutrition` key on the product: %s", encoded)
|
|
}
|
|
for _, key := range []string{"per", "servingsize", "items"} {
|
|
if _, ok := nutrition[key]; !ok {
|
|
t.Errorf("missing %q: %v", key, nutrition)
|
|
}
|
|
}
|
|
item := nutrition["items"].([]any)[0].(map[string]any)
|
|
for _, key := range []string{"name", "value", "unit"} {
|
|
if _, ok := item[key]; !ok {
|
|
t.Errorf("item missing %q: %v", key, item)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAProductWithNoNutritionHasNoKeyAtAll(t *testing.T) {
|
|
// Not `"nutrition": null`. Most products have none, and a null on every row
|
|
// of a mobile response is payload spent saying nothing.
|
|
encoded, _ := json.Marshal(models.Products{Productid: 1})
|
|
var out map[string]any
|
|
_ = json.Unmarshal(encoded, &out)
|
|
if _, present := out["nutrition"]; present {
|
|
t.Fatalf("an absent panel was sent as a key: %s", encoded)
|
|
}
|
|
}
|