From 1f6a4195441cde435bb5a08b8502f7e2ecfcd8e2 Mon Sep 17 00:00:00 2001 From: awei Date: Tue, 28 Jul 2026 08:52:44 +0800 Subject: [PATCH] =?UTF-8?q?test(repository):=20=E6=95=91=E5=9B=9E=20dropof?= =?UTF-8?q?f=20=E5=BA=A7=E6=A8=99=E7=9A=84=20repository=20=E5=B1=A4?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6=EF=BC=88=E4=BE=86=E8=87=AA=E6=9C=AA=E5=90=88?= =?UTF-8?q?=E4=BD=B5=E7=9A=84=E5=88=86=E6=94=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 清理殘留分支時發現 `claude/determined-shannon-17f4ac`(從未開過 PR)帶了一份 `ride_dropoff_integration_test.go`,內容從未進 main。原檔測的 `GetDropoffCoords` 在 main 已不存在,故改寫成對現有 API(Create → GetByID)的等價覆蓋。 兩案的價值不同: - 正向 round-trip:service 層 ride_create_customer_test 已間接覆蓋,此處補 repository 層直接覆蓋——Create 有/無 dropoff 是兩條不同的 SQL,走錯會被上層邏輯掩蓋。 - **未指定目的地 → DropoffPoint 必須是 nil**:main 完全沒有這條。這守的是踩過的 GeoPoint.Scan 坑(NULL 被當成掃描成功而留下零值座標,導航與計費會把 (0,0) 當真目的地且不報錯)。正向案例校準了同一條取值路徑,故此負向斷言不會永遠 PASS。 驗證:真 PostGIS(testcontainers)實跑 2/2 PASS;gofmt/go vet 乾淨。 Co-Authored-By: Claude Opus 5 --- .../ride_dropoff_integration_test.go | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 internal/repository/ride_dropoff_integration_test.go diff --git a/internal/repository/ride_dropoff_integration_test.go b/internal/repository/ride_dropoff_integration_test.go new file mode 100644 index 0000000..c15efd3 --- /dev/null +++ b/internal/repository/ride_dropoff_integration_test.go @@ -0,0 +1,90 @@ +package repository + +import ( + "math" + "testing" + "time" + + "line-fleet-dispatch/internal/constants" + "line-fleet-dispatch/internal/model" +) + +// TestRideDropoff_寫入讀回 建立帶目的地的訂單後,dropoff_point 座標與 dropoff_address 應原樣讀回。 +// +// 這是 repository 層的直接覆蓋:service 層測試(ride_create_customer_test)走的是 +// 建單流程,若 Create 的兩條 SQL 分支(有/無 dropoff)走錯,錯誤會被上層邏輯掩蓋。 +func TestRideDropoff_寫入讀回(t *testing.T) { + db := newMigratedTestDB(t) // Docker 不可用時內部 t.Skip + rides := NewRideRepository(db) + cust, err := NewCustomerRepository(db).FindOrCreateByLineUserID("U_dropoff_rt", "測試乘客") + if err != nil { + t.Fatalf("建立乘客失敗:%v", err) + } + + now := time.Now() + ride := &model.Ride{ + CustomerID: cust.ID, + Status: constants.RideStatusRequested, + PickupPoint: model.GeoPoint{Lat: 25.03, Lng: 121.56}, + PickupAddress: "台北車站", + DropoffPoint: &model.GeoPoint{Lat: 25.08, Lng: 121.57}, + DropoffAddress: "松山機場", + RequestedAt: now, + CreatedAt: now, + UpdatedAt: now, + } + if err := rides.Create(ride); err != nil { + t.Fatalf("建立訂單失敗:%v", err) + } + + got, err := rides.GetByID(ride.ID) + if err != nil { + t.Fatalf("讀取訂單失敗:%v", err) + } + if got.DropoffPoint == nil { + t.Fatal("預期有目的地座標,卻讀回 nil") + } + if math.Abs(got.DropoffPoint.Lat-25.08) > 1e-6 || math.Abs(got.DropoffPoint.Lng-121.57) > 1e-6 { + t.Fatalf("目的地座標不符:得到 (%f, %f)", got.DropoffPoint.Lat, got.DropoffPoint.Lng) + } + if got.DropoffAddress != "松山機場" { + t.Fatalf("dropoff_address 不符:得到 %q", got.DropoffAddress) + } +} + +// TestRideDropoff_未指定時為NULL 未指定目的地的訂單,DropoffPoint 必須讀回 nil 而非零值座標。 +// +// 這條守的是 GeoPoint.Scan 曾經的坑:NULL 被當成「掃描成功」而留下 (0, 0), +// 導航與計費就會把非洲外海的那個點當成真目的地,且**不會有任何錯誤**。 +// 判斷「有沒有目的地」的唯一依據是指標是否為 nil,故此斷言不可弱化為座標比較。 +func TestRideDropoff_未指定時為NULL(t *testing.T) { + db := newMigratedTestDB(t) + rides := NewRideRepository(db) + cust, err := NewCustomerRepository(db).FindOrCreateByLineUserID("U_dropoff_null", "測試乘客") + if err != nil { + t.Fatalf("建立乘客失敗:%v", err) + } + + now := time.Now() + ride := &model.Ride{ + CustomerID: cust.ID, + Status: constants.RideStatusRequested, + PickupPoint: model.GeoPoint{Lat: 25.03, Lng: 121.56}, + PickupAddress: "台北車站", + RequestedAt: now, + CreatedAt: now, + UpdatedAt: now, + } + if err := rides.Create(ride); err != nil { + t.Fatalf("建立訂單失敗:%v", err) + } + + got, err := rides.GetByID(ride.ID) + if err != nil { + t.Fatalf("讀取訂單失敗:%v", err) + } + if got.DropoffPoint != nil { + t.Fatalf("未指定目的地時 DropoffPoint 應為 nil,卻得到 (%f, %f)", + got.DropoffPoint.Lat, got.DropoffPoint.Lng) + } +}