Files
doormile_backend/models/customer_app_test.go

176 lines
6.2 KiB
Go

package models
import (
"reflect"
"strings"
"testing"
)
// Table names and column tags are strings the compiler never checks. A typo in
// either produces a binary that builds cleanly and then fails against the
// database at runtime — which, for the customer app, means the first real
// booking. These are cheap tests for a class of bug nothing else catches.
// Every customer-app model maps to the table AutoMigrate creates and the seed
// fills. A mismatch here means seed_customer_app.sql populates one table while
// the code reads another.
func TestCustomerAppTableNames(t *testing.T) {
cases := []struct {
model interface{ TableName() string }
want string
}{
{ServiceableState{}, "serviceablestates"},
{ServiceableDistrict{}, "serviceabledistricts"},
{PickupSlotTemplate{}, "pickupslottemplates"},
{CustomerBookingLimit{}, "customerbookinglimits"},
{BookingDestination{}, "bookingdestinations"},
{BookingParcelPhoto{}, "bookingparcelphotos"},
{BookingStageEvent{}, "bookingstageevents"},
{CustomerRefreshToken{}, "customerrefreshtokens"},
{CustomerDevice{}, "customerdevices"},
}
for _, tc := range cases {
if got := tc.model.TableName(); got != tc.want {
t.Errorf("%T.TableName() = %q, want %q",
tc.model, got, tc.want)
}
}
}
// columnOf reads the gorm column name off a struct field, the way the driver
// does.
func columnOf(t *testing.T, model interface{}, field string) string {
t.Helper()
f, ok := reflect.TypeOf(model).FieldByName(field)
if !ok {
t.Fatalf("%T has no field %s", model, field)
}
for _, part := range strings.Split(f.Tag.Get("gorm"), ";") {
if strings.HasPrefix(part, "column:") {
return strings.TrimPrefix(part, "column:")
}
}
return ""
}
// The columns the fan-out is built on. bookingdestinations.seq in particular is
// addressed by position in PATCH .../destinations/{index} and is what stop
// ordering sorts by — a renamed column there silently reorders a rider's route.
func TestBookingDestinationColumns(t *testing.T) {
want := map[string]string{
"Bookingdestinationid": "bookingdestinationid",
"Bookingid": "bookingid",
"Seq": "seq",
"Statecode": "statecode",
"Districtcode": "districtcode",
"Packagecount": "packagecount",
"Recipientname": "recipientname",
"Recipientphone": "recipientphone",
"Codamount": "codamount",
"Consignmentid": "consignmentid",
"Trackingno": "trackingno",
"Stage": "stage",
"Verifiedweightkg": "verifiedweightkg",
}
for field, col := range want {
if got := columnOf(t, BookingDestination{}, field); got != col {
t.Errorf("BookingDestination.%s column = %q, want %q", field, got, col)
}
}
}
// The columns added to the live pickupbookings table. These land on 512
// existing rows, so the names have to match what migrate.go creates.
func TestPickupBookingCustomerColumns(t *testing.T) {
want := map[string]string{
"Slotid": "slotid",
"Customerstage": "customerstage",
"Customerstatus": "customerstatus",
"Estimateminrupees": "estimateminrupees",
"Estimatemaxrupees": "estimatemaxrupees",
"Routekm": "routekm",
"Pickuptitle": "pickuptitle",
"Pickupsub": "pickupsub",
"Cancelreason": "cancelreason",
}
for field, col := range want {
if got := columnOf(t, PickupBooking{}, field); got != col {
t.Errorf("PickupBooking.%s column = %q, want %q", field, got, col)
}
}
// The link that lets a parcel know which order it belongs to.
if got := columnOf(t, BookingParcel{}, "Bookingdestinationid"); got != "bookingdestinationid" {
t.Errorf("BookingParcel.Bookingdestinationid column = %q, want %q",
got, "bookingdestinationid")
}
}
// A refresh token is a bearer credential valid for sixty days. It must never be
// serialised outward — a JSON tag of anything but "-" would put it in a
// response body.
func TestRefreshTokenHashIsNeverSerialised(t *testing.T) {
f, ok := reflect.TypeOf(CustomerRefreshToken{}).FieldByName("Tokenhash")
if !ok {
t.Fatal("CustomerRefreshToken has no Tokenhash field")
}
if got := f.Tag.Get("json"); got != "-" {
t.Errorf("Tokenhash json tag = %q, want \"-\" — a stored credential must "+
"never leave the server", got)
}
}
// The delivery OTP is the receiver's proof, not the rider's. Returning it in a
// response would hand the rider the code they are supposed to be told at the
// door.
func TestDeliveryOtpIsNeverSerialised(t *testing.T) {
f, ok := reflect.TypeOf(Consignment{}).FieldByName("Deliveryotp")
if !ok {
t.Fatal("Consignment has no Deliveryotp field")
}
if got := f.Tag.Get("json"); got != "-" {
t.Errorf("Deliveryotp json tag = %q, want \"-\"", got)
}
}
// The customer PIN hash predates the OTP flow and is now unused, but the column
// still exists. It must stay unserialised.
func TestCustomerPinHashIsNeverSerialised(t *testing.T) {
f, ok := reflect.TypeOf(AppCustomer{}).FieldByName("Loginpinhash")
if !ok {
t.Fatal("AppCustomer has no Loginpinhash field")
}
if got := f.Tag.Get("json"); got != "-" {
t.Errorf("Loginpinhash json tag = %q, want \"-\"", got)
}
}
// Two columns on pickupbookings that are computed, not stored. A missing
// gorm:"-" would make AutoMigrate create real columns for them and the driver
// would try to write there.
func TestComputedBookingFieldsAreNotColumns(t *testing.T) {
for _, field := range []string{"Consignmentstatus", "Trackingno"} {
f, ok := reflect.TypeOf(PickupBooking{}).FieldByName(field)
if !ok {
t.Fatalf("PickupBooking has no field %s", field)
}
if got := f.Tag.Get("gorm"); got != "-" {
t.Errorf("PickupBooking.%s gorm tag = %q, want \"-\" — it is filled in "+
"by handlers, not stored", field, got)
}
}
}
// The device token is unique across the whole table, not per customer: a shared
// handset must be REASSIGNED rather than duplicated, or one person's parcel
// updates reach another person's phone.
func TestCustomerDeviceTokenIsGloballyUnique(t *testing.T) {
f, _ := reflect.TypeOf(CustomerDevice{}).FieldByName("Token")
if tag := f.Tag.Get("gorm"); !strings.Contains(tag, "uniqueIndex") {
t.Errorf("CustomerDevice.Token gorm tag = %q, want a uniqueIndex — the "+
"upsert on conflict depends on it", tag)
}
}