package services import "testing" /* The tenants table had no write path at all, so a merchant could never change their own shop's photo, licence number or contact — measured across 200 tenants: 18 had an image, none had a licence. Adding one is where the risk is. `tenants` carries platform-controlled columns next to merchant-owned ones — `approved`, `status`, `partnerid`, `partneruserid`, `moduleid`, `configid`, `tenanttoken` — and the obvious implementation, handing the parsed body to GORM, would let anyone reaching the endpoint approve themselves onto the platform or move themselves under a different partner. A UI would never send those fields, which is what makes the hole easy to leave open. */ func TestAMerchantCannotApproveThemselves(t *testing.T) { clean, err := TenantProfileUpdate(map[string]any{ "tenantimage": "https://example.com/shop.jpg", "approved": 1, "status": "Active", }) if err != nil { t.Fatalf("TenantProfileUpdate: %v", err) } for _, forbidden := range []string{"approved", "status"} { if _, present := clean[forbidden]; present { t.Errorf("%q survived the allowlist", forbidden) } } if clean["tenantimage"] != "https://example.com/shop.jpg" { t.Errorf("the legitimate field was dropped: %+v", clean) } } func TestOwnershipAndBillingColumnsAreNotEditable(t *testing.T) { _, err := TenantProfileUpdate(map[string]any{ "partnerid": 9, "partneruserid": 9, "moduleid": 3, "configid": 2, "tenanttoken": "stolen", "tenantid": 1, }) // Every field was refused, so nothing is left to write — and that must be // an error, not a silent success. if err == nil { t.Fatal("a request of nothing but forbidden fields was accepted") } } /* A profile form sends every field it renders on every save. If a blank meant "erase", opening the form and saving one change would wipe everything the form does not show — an address typed last month, a licence added by somebody else. */ func TestABlankFieldIsNotAnInstructionToErase(t *testing.T) { clean, err := TenantProfileUpdate(map[string]any{ "licenseno": "12345678901234", "address": " ", "city": "", }) if err != nil { t.Fatalf("TenantProfileUpdate: %v", err) } if _, present := clean["address"]; present { t.Error("a whitespace-only value was treated as a change") } if _, present := clean["city"]; present { t.Error("an empty value was treated as a change") } if clean["licenseno"] != "12345678901234" { t.Errorf("the real change was lost: %+v", clean) } } // Zero is a real minimum order and a real subcategory id is not a string, so // the blank rule must apply to text only. func TestANumericZeroIsStillAChange(t *testing.T) { clean, err := TenantProfileUpdate(map[string]any{"minorder": 0}) if err != nil { t.Fatalf("TenantProfileUpdate: %v", err) } if value, present := clean["minorder"]; !present || value != 0 { t.Errorf("a minimum order of zero was dropped: %+v", clean) } } // Case and stray whitespace in a key are a client's problem, not a reason to // silently ignore a field the merchant filled in. func TestFieldNamesAreMatchedLeniently(t *testing.T) { clean, err := TenantProfileUpdate(map[string]any{" TenantImage ": "x.jpg"}) if err != nil { t.Fatalf("TenantProfileUpdate: %v", err) } if clean["tenantimage"] != "x.jpg" { t.Errorf("want the field normalised onto its column, got %+v", clean) } } // Store setup marks Store type REQUIRED and sends it on save. It was missing // from the allowlist, so the merchant answered a required question, saw the // step succeed, and the answer was dropped on the floor — measured on local // tenant 9002, which reached the end of setup with `tenanttype` still empty. func TestTheKindOfShopIsTheMerchantsToSet(t *testing.T) { clean, err := TenantProfileUpdate(map[string]any{"tenanttype": "grocery"}) if err != nil { t.Fatalf("TenantProfileUpdate: %v", err) } if clean["tenanttype"] != "grocery" { t.Errorf("tenanttype did not survive the allowlist: %#v", clean) } } // Letting the shop describe itself must not have opened a door next to it. func TestAllowingTheShopTypeDidNotAllowItsStanding(t *testing.T) { clean, err := TenantProfileUpdate(map[string]any{ "tenanttype": "pharmacy", "tenanttoken": "stolen", "partnerid": 99, "configid": 7, }) if err != nil { t.Fatalf("TenantProfileUpdate: %v", err) } for _, forbidden := range []string{"tenanttoken", "partnerid", "configid"} { if _, present := clean[forbidden]; present { t.Errorf("%q survived the allowlist", forbidden) } } } // The person who runs the shop. // // `tenants.firstname` was collected by no form and permitted by no allowlist, // so every merchant read came back with it empty and the store profile printed // "—" for Store admin. It is the business's own contact person, not its // standing on the platform. func TestTenantProfileUpdateAllowsStoreAdmin(t *testing.T) { clean, err := TenantProfileUpdate(map[string]any{ "tenantid": 1147, "firstname": "Ravi Kumar", }) if err != nil { t.Fatalf("TenantProfileUpdate: %v", err) } if clean["firstname"] != "Ravi Kumar" { t.Fatalf("firstname should survive the allowlist, got %v", clean["firstname"]) } if _, ok := clean["tenantid"]; ok { t.Fatal("tenantid names the row and is never a value to write") } } // The guard that makes the allowlist worth having: a merchant must not be able // to approve themselves or move under another partner by sending the field. func TestTenantProfileUpdateStillRefusesPlatformFields(t *testing.T) { _, err := TenantProfileUpdate(map[string]any{ "tenantid": 1147, "approved": 1, "partnerid": 9, "status": "Active", }) if err == nil { t.Fatal("nothing in that request is editable, so it must not report success") } }