176 lines
6.2 KiB
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)
|
|
}
|
|
}
|