material_manage_system/internal/repository/inbound_organization_test.go
2026-10-09 12:21:53 +08:00

176 lines
7.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"database/sql"
"testing"
"time"
"github.com/wmms/wmms/internal/model"
)
// seedOrderIsolation 在共享双公司 MySQL 测试集里播种两套完全独立的入出库单据与库存:
//
// 公司 A=101:物料 101、仓库 101、库位 101、入库单 101、出库单 101、余额 100;
// 公司 B=102:物料 102、仓库 102、库位 102、入库单 102、出库单 102、余额 50。
//
// 两家各自的单据、明细、余额、流水都指向本公司的维度,用来验证单据与库存冲销按公司隔离。
func seedOrderIsolation(t *testing.T, db *sql.DB) {
t.Helper()
mustSQL(t, db, `INSERT INTO organizations(id,code,name) VALUES (101,'OI_A','A'),(102,'OI_B','B');
INSERT INTO material_categories(id,organization_id,code,name) VALUES (101,101,'C','C'),(102,102,'C','C');
INSERT INTO units(id,organization_id,code,name) VALUES (101,101,'U','U'),(102,102,'U','U');
INSERT INTO materials(id,organization_id,material_code,name,category_id,base_unit_id) VALUES (101,101,'M','M',101,101),(102,102,'M','M',102,102);
INSERT INTO warehouses(id,organization_id,code,name,status) VALUES (101,101,'W','W','active'),(102,102,'W','W','active');
INSERT INTO warehouse_locations(id,warehouse_id,code,name,status) VALUES (101,101,'L','L','active'),(102,102,'L','L','active');
INSERT INTO inventory_balances(organization_id,warehouse_id,location_id,material_id,on_hand) VALUES (101,101,101,101,100),(102,102,102,102,50);
INSERT INTO inbound_orders(id,organization_id,warehouse_id,location_id,inbound_date,handler,supplier) VALUES (101,101,101,101,NOW(),'A-in','supA'),(102,102,102,102,NOW(),'B-in','supB');
INSERT INTO inbound_items(inbound_order_id,material_id,quantity) VALUES (101,101,5),(102,102,7);
INSERT INTO outbound_orders(id,organization_id,warehouse_id,location_id,outbound_date,handler,receiver) VALUES (101,101,101,101,NOW(),'A-out','recA'),(102,102,102,102,NOW(),'B-out','recB');
INSERT INTO outbound_items(outbound_order_id,material_id,quantity) VALUES (101,101,3),(102,102,4);
INSERT INTO stock_logs(organization_id,warehouse_id,location_id,material_id,type,quantity,related_order_type,related_order_id) VALUES (101,101,101,101,'in',5,'inbound',101),(102,102,102,102,'in',7,'inbound',102);`)
}
func TestInboundRepoOrganizationIsolationReads(t *testing.T) {
db := isolationDB(t, false)
seedOrderIsolation(t, db)
repo := NewInboundRepo()
ownOrder, err := repo.GetByID(101, 101)
if err != nil || ownOrder.ID != 101 || ownOrder.OrganizationID != 101 {
t.Fatalf("A 读自己的单据失败: %+v err=%v", ownOrder, err)
}
if _, err := repo.GetByID(101, 102); err != sql.ErrNoRows {
t.Fatalf("A 读 B 的单据必须 ErrNoRows, got %v", err)
}
// 列表与分页总数只含本公司。
ordersA, totalA, err := repo.List(101, "", 20, 0)
if err != nil {
t.Fatal(err)
}
if totalA != 1 || len(ordersA) != 1 || ordersA[0].ID != 101 {
t.Fatalf("A 列表=%+v total=%d", ordersA, totalA)
}
ordersB, totalB, err := repo.List(102, "", 20, 0)
if err != nil {
t.Fatal(err)
}
if totalB != 1 || len(ordersB) != 1 || ordersB[0].ID != 102 {
t.Fatalf("B 列表=%+v total=%d", ordersB, totalB)
}
// 按物料历史隔离:A 查到自己的 1 条;A 上下文读 B 的物料为空。
recordsA, err := repo.GetByMaterialID(101, 101)
if err != nil || len(recordsA) != 1 {
t.Fatalf("A 物料历史=%+v err=%v", recordsA, err)
}
recordsCross, err := repo.GetByMaterialID(101, 102)
if err != nil || len(recordsCross) != 0 {
t.Fatalf("A 读 B 物料历史必须为空, got %+v err=%v", recordsCross, err)
}
}
// 头表 INSERT 显式保存三个维度;明细经已授权头表关联并限制同组织物料,跨公司物料的
// 明细读不出来。
func TestInboundRepoCreatePersistsDimensionsAndRestrictsItems(t *testing.T) {
db := isolationDB(t, false)
seedOrderIsolation(t, db)
repo := NewInboundRepo()
order := &model.InboundOrder{
OrganizationID: 101,
WarehouseID: 101,
LocationID: 101,
InboundDate: time.Now(),
Operator: "op",
Handler: "h",
Supplier: "s",
Items: []model.InboundItem{{MaterialID: 101, Quantity: 2}},
}
if err := repo.Create(order); err != nil {
t.Fatal(err)
}
var org, warehouse, location int64
if err := db.QueryRow(`SELECT organization_id, warehouse_id, location_id FROM inbound_orders WHERE id = ?`, order.ID).Scan(&org, &warehouse, &location); err != nil {
t.Fatal(err)
}
if org != 101 || warehouse != 101 || location != 101 {
t.Fatalf("头表维度=%d/%d/%d, want 101/101/101", org, warehouse, location)
}
// 给 A 的单据塞一条 B 的物料明细:物料外键合法,但读取时的同组织关联会把它挡掉。
mustSQL(t, db, `INSERT INTO inbound_items(inbound_order_id, material_id, quantity) VALUES (?, ?, ?)`, order.ID, 102, 9)
loaded, err := repo.GetByID(101, order.ID)
if err != nil {
t.Fatal(err)
}
if len(loaded.Items) != 1 || loaded.Items[0].MaterialID != 101 {
t.Fatalf("跨公司物料明细必须被关联条件挡掉, got %+v", loaded.Items)
}
}
// A 改/删 B 的单据:A 上下文命中 ErrNoRows 或影响 0 行,B 的头表与明细都不变。
func TestInboundRepoUpdateAndDeleteScopeToOrganization(t *testing.T) {
db := isolationDB(t, false)
seedOrderIsolation(t, db)
repo := NewInboundRepo()
if err := repo.Update(101, 102, &model.UpdateInboundRequest{Handler: "hacked", Supplier: "hacked"}); err != sql.ErrNoRows {
t.Fatalf("A 改 B 的单据必须 ErrNoRows, got %v", err)
}
var handler, supplier string
if err := db.QueryRow(`SELECT handler, supplier FROM inbound_orders WHERE id = 102`).Scan(&handler, &supplier); err != nil {
t.Fatal(err)
}
if handler != "B-in" || supplier != "supB" {
t.Fatalf("B 的单据头被改动: %s/%s", handler, supplier)
}
var itemCount int
if err := db.QueryRow(`SELECT COUNT(*) FROM inbound_items WHERE inbound_order_id = 102`).Scan(&itemCount); err != nil {
t.Fatal(err)
}
if itemCount != 1 {
t.Fatalf("B 的明细被改动: %d 行", itemCount)
}
if err := repo.Delete(101, 102); err != nil {
t.Fatal(err)
}
var orderCount int
if err := db.QueryRow(`SELECT COUNT(*) FROM inbound_orders WHERE id = 102`).Scan(&orderCount); err != nil {
t.Fatal(err)
}
if orderCount != 1 {
t.Fatal("A 删掉了 B 的单据")
}
}
// 库存流水删除同时约束组织、关联类型和单据 ID:凭别家公司的单据 ID 删不掉它的流水,
// 本公司相同单据 ID 的流水仍能正常删除。
func TestStockRepoDeleteLogsScopesToOrganization(t *testing.T) {
db := isolationDB(t, false)
seedOrderIsolation(t, db)
repo := NewStockRepo()
if err := repo.DeleteLogs(101, "inbound", 102); err != nil {
t.Fatal(err)
}
var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM stock_logs WHERE organization_id = 102 AND related_order_type = 'inbound' AND related_order_id = 102`).Scan(&count); err != nil {
t.Fatal(err)
}
if count != 1 {
t.Fatal("凭 B 的单据 ID 删掉了它的库存流水")
}
if err := repo.DeleteLogs(101, "inbound", 101); err != nil {
t.Fatal(err)
}
if err := db.QueryRow(`SELECT COUNT(*) FROM stock_logs WHERE organization_id = 101 AND related_order_type = 'inbound' AND related_order_id = 101`).Scan(&count); err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("本公司流水未被删除: %d", count)
}
}