material_manage_system/internal/service/outbound_test.go
2026-10-09 10:38:02 +08:00

117 lines
5.1 KiB
Go

package service
import (
"errors"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/pkg/database"
)
func outboundMock(t *testing.T) sqlmock.Sqlmock {
t.Helper()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
previous := database.DB
database.DB = db
t.Cleanup(func() { database.DB = previous; db.Close() })
return mock
}
func expectActiveMaterial(mock sqlmock.Sqlmock, id int64) {
for i := 0; i < 2; i++ {
mock.ExpectQuery("SELECT id, organization_id, material_code, name, material_image").WithArgs(int64(1), id).
WillReturnRows(sqlmock.NewRows([]string{"id", "organization_id", "material_code", "name", "material_image", "spec", "manufacturer_model", "category_id", "base_unit_id", "unit", "status", "version", "created_at", "updated_at"}).
AddRow(id, 1, "M-1", "Material", nil, "", nil, 1, 1, "pc", "active", 1, time.Now(), time.Now()))
}
}
func createOutboundRequest() *model.CreateOutboundRequest {
req := &model.CreateOutboundRequest{Receiver: "receiver"}
req.Items = append(req.Items, struct {
MaterialID int64 `json:"material_id" binding:"required"`
Quantity int `json:"quantity" binding:"required,gt=0"`
}{MaterialID: 7, Quantity: 2})
return req
}
func TestOutboundCreateRollsBackWhenLockedStockRunsOut(t *testing.T) {
mock := outboundMock(t)
expectActiveMaterial(mock, 7)
mock.ExpectQuery("SELECT ib.on_hand").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"on_hand"}).AddRow("2"))
mock.ExpectBegin()
mock.ExpectQuery("SELECT ib.on_hand").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"on_hand"}).AddRow("1"))
mock.ExpectRollback()
_, err := NewOutboundService().Create(1, createOutboundRequest(), "operator")
if !errors.Is(err, ErrInsufficientStock) {
t.Fatalf("got %v, want insufficient stock", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestOutboundCreateChecksDefaultLocationBeforeTransaction(t *testing.T) {
mock := outboundMock(t)
expectActiveMaterial(mock, 7)
mock.ExpectQuery("SELECT ib.on_hand.*FROM inventory_balances").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"on_hand"}).AddRow("1"))
_, err := NewOutboundService().Create(1, createOutboundRequest(), "operator")
if !errors.Is(err, ErrInsufficientStock) {
t.Fatalf("got %v, want insufficient stock", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestOutboundCreateRollsBackWhenMaterialReadFailsAfterOrderInsert(t *testing.T) {
mock := outboundMock(t)
expectActiveMaterial(mock, 7)
mock.ExpectQuery("SELECT ib.on_hand").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"on_hand"}).AddRow("2"))
mock.ExpectBegin()
mock.ExpectQuery("SELECT ib.on_hand").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"on_hand"}).AddRow("2"))
mock.ExpectExec("INSERT INTO outbound_orders").WillReturnResult(sqlmock.NewResult(11, 1))
mock.ExpectExec("UPDATE outbound_orders").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO outbound_items").WillReturnResult(sqlmock.NewResult(12, 1))
mock.ExpectQuery("SELECT id, organization_id, material_code, name, material_image").WithArgs(int64(1), int64(7)).WillReturnError(errors.New("material read failed"))
mock.ExpectRollback()
_, err := NewOutboundService().Create(1, createOutboundRequest(), "operator")
if err == nil || err.Error() != "material read failed" {
t.Fatalf("got %v, want material read failure", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestOutboundUpdateDoesNotCountRestoredStockTwice(t *testing.T) {
mock := outboundMock(t)
now := time.Now()
mock.ExpectQuery("SELECT id, COALESCE\\(outbound_no").WithArgs(int64(11)).WillReturnRows(
sqlmock.NewRows([]string{"id", "outbound_no", "operator", "outbound_date", "handler", "receiver", "remark", "created_at", "updated_at"}).AddRow(11, "OUT-11", "operator", now, "operator", "receiver", "", now, now))
mock.ExpectQuery("SELECT oi.id, oi.outbound_order_id").WithArgs(int64(11)).WillReturnRows(
sqlmock.NewRows([]string{"id", "outbound_order_id", "material_id", "quantity", "material_code", "name", "status"}).AddRow(12, 11, 7, 2, "M-1", "Material", "active"))
expectActiveMaterial(mock, 7)
mock.ExpectQuery("SELECT ib.on_hand").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"on_hand"}).AddRow("2"))
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO inventory_balances").WithArgs(int64(7), "2").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery("SELECT ib.on_hand").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"on_hand"}).AddRow("3"))
mock.ExpectRollback()
req := &model.UpdateOutboundRequest{Receiver: "receiver"}
req.Items = append(req.Items, struct {
MaterialID int64 `json:"material_id" binding:"required"`
Quantity int `json:"quantity" binding:"required,gt=0"`
}{MaterialID: 7, Quantity: 4})
_, err := NewOutboundService().Update(1, 11, req, "operator")
if !errors.Is(err, ErrInsufficientStock) {
t.Fatalf("got %v, want insufficient stock", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}