101 lines
3.3 KiB
Go
101 lines
3.3 KiB
Go
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)
|
|
}
|
|
}
|