material_manage_system/internal/repository/application_organization_test.go

185 lines
6.7 KiB
Go
Raw 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"
"github.com/wmms/wmms/internal/model"
)
// seedApplicationIsolation 复用两公司物料/仓库基础数据,并补一个跨两公司都授权、可用同一
// 用户在 A/B 各自建申请的用户。company 101=公司A,102=公司B。
func seedApplicationIsolation(t *testing.T, db *sql.DB) {
t.Helper()
seedOrderIsolation(t, db)
mustSQL(t, db, `INSERT INTO users(id,username,password) VALUES (7,'shared','x')`)
}
func newIsolationApplication(organizationID, materialID int64) *model.MaterialApplication {
return &model.MaterialApplication{
OrganizationID: organizationID,
UserID: 7,
Purpose: "领用",
Status: model.ApplicationStatusPending,
Items: []model.ApplicationItem{{MaterialID: materialID, Quantity: 2}},
}
}
// 同一用户在 A/B 各自创建的申请在对应公司可见,跨公司读不到。
func TestApplicationRepoOrganizationIsolationReads(t *testing.T) {
db := isolationDB(t, false)
seedApplicationIsolation(t, db)
repo := NewApplicationRepository()
appA := newIsolationApplication(101, 101)
appB := newIsolationApplication(102, 102)
if err := repo.Create(appA); err != nil {
t.Fatal(err)
}
if err := repo.Create(appB); err != nil {
t.Fatal(err)
}
own, err := repo.GetByID(101, appA.ID)
if err != nil || own.OrganizationID != 101 || len(own.Items) != 1 || own.Items[0].MaterialID != 101 {
t.Fatalf("A 读自己的申请失败: %+v err=%v", own, err)
}
if _, err := repo.GetByID(101, appB.ID); err != sql.ErrNoRows {
t.Fatalf("A 读 B 的申请必须 ErrNoRows, got %v", err)
}
if _, err := repo.GetByID(102, appA.ID); err != sql.ErrNoRows {
t.Fatalf("B 读 A 的申请必须 ErrNoRows, got %v", err)
}
appsA, totalA, err := repo.ListAllApplications(101, "", 20, 0)
if err != nil || totalA != 1 || len(appsA) != 1 || appsA[0].ID != appA.ID {
t.Fatalf("A 审批列表=%+v total=%d err=%v", appsA, totalA, err)
}
appsB, totalB, err := repo.ListAllApplications(102, "", 20, 0)
if err != nil || totalB != 1 || len(appsB) != 1 || appsB[0].ID != appB.ID {
t.Fatalf("B 审批列表=%+v total=%d err=%v", appsB, totalB, err)
}
mineA, mineTotalA, err := repo.ListByUserID(101, 7, "", 20, 0)
if err != nil || mineTotalA != 1 || mineA[0].ID != appA.ID {
t.Fatalf("A 我的申请=%+v total=%d err=%v", mineA, mineTotalA, err)
}
mineB, mineTotalB, err := repo.ListByUserID(102, 7, "", 20, 0)
if err != nil || mineTotalB != 1 || mineB[0].ID != appB.ID {
t.Fatalf("B 我的申请=%+v total=%d err=%v", mineB, mineTotalB, err)
}
}
// 明细经组织化申请头表关联物料:头表属于 A,混入的 B 公司物料会被关联条件挡掉。
func TestApplicationRepoCrossCompanyItemHidden(t *testing.T) {
db := isolationDB(t, false)
seedApplicationIsolation(t, db)
repo := NewApplicationRepository()
appA := newIsolationApplication(101, 101)
if err := repo.Create(appA); err != nil {
t.Fatal(err)
}
// 物料外键合法(102 存在),但属于公司 B,读取时的同组织关联会把它挡掉。
mustSQL(t, db, `INSERT INTO application_items(application_id, material_id, quantity) VALUES (?, ?, ?)`, appA.ID, 102, 9)
loaded, err := repo.GetByID(101, appA.ID)
if err != nil {
t.Fatal(err)
}
if len(loaded.Items) != 1 || loaded.Items[0].MaterialID != 101 {
t.Fatalf("跨公司物料明细必须被关联条件挡掉, got %+v", loaded.Items)
}
}
// 状态更新带公司、ID 和原状态条件:别家公司改不动;本公司重复审批/取消非待审也会被条件挡下。
func TestApplicationRepoStatusUpdatesScopedAndConditional(t *testing.T) {
db := isolationDB(t, false)
seedApplicationIsolation(t, db)
repo := NewApplicationRepository()
appA := newIsolationApplication(101, 101)
if err := repo.Create(appA); err != nil {
t.Fatal(err)
}
// 别家公司操作 A 的申请:公司条件不匹配,一律冲突。
if err := repo.Approve(102, appA.ID, 5); err != ErrApplicationStatusConflict {
t.Fatalf("B 审批 A 的申请必须冲突, got %v", err)
}
if err := repo.Reject(102, appA.ID, "x"); err != ErrApplicationStatusConflict {
t.Fatalf("B 驳回 A 的申请必须冲突, got %v", err)
}
if err := repo.Cancel(102, appA.ID); err != ErrApplicationStatusConflict {
t.Fatalf("B 取消 A 的申请必须冲突, got %v", err)
}
if status := applicationStatus(t, db, appA.ID); status != model.ApplicationStatusPending {
t.Fatalf("跨公司操作改变了状态: %s", status)
}
// 本公司首次审批成功;重复审批因原状态已变(非 pending)被条件更新挡下。
if err := repo.Approve(101, appA.ID, 5); err != nil {
t.Fatal(err)
}
if status := applicationStatus(t, db, appA.ID); status != model.ApplicationStatusApproved {
t.Fatalf("审批后状态=%s, want approved", status)
}
if err := repo.Approve(101, appA.ID, 5); err != ErrApplicationStatusConflict {
t.Fatalf("重复审批必须被条件挡下, got %v", err)
}
if err := repo.Cancel(101, appA.ID); err != ErrApplicationStatusConflict {
t.Fatalf("取消非待审申请必须冲突, got %v", err)
}
}
// 审批事务锁定当前公司的申请:锁别家公司的申请命中空行。
func TestApplicationRepoLockForUpdateScopesToOrganization(t *testing.T) {
db := isolationDB(t, false)
seedApplicationIsolation(t, db)
repo := NewApplicationRepository()
appA := newIsolationApplication(101, 101)
if err := repo.Create(appA); err != nil {
t.Fatal(err)
}
tx, err := BeginTx()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
if _, err := repo.GetByIDForUpdateTx(tx, 102, appA.ID); err != sql.ErrNoRows {
t.Fatalf("锁别家公司申请必须 ErrNoRows, got %v", err)
}
locked, err := repo.GetByIDForUpdateTx(tx, 101, appA.ID)
if err != nil || locked.ID != appA.ID {
t.Fatalf("锁本公司申请失败: %+v err=%v", locked, err)
}
}
func applicationStatus(t *testing.T, db *sql.DB, id int64) string {
t.Helper()
var status string
if err := db.QueryRow(`SELECT status FROM material_applications WHERE id = ?`, id).Scan(&status); err != nil {
t.Fatal(err)
}
return status
}
// 服务启动只检查所需列:新库通过;缺少 organization_id 的旧库必须报明确的升级错误,
// 而不是在新环境按旧定义建一张缺列的申请表。
func TestCheckApplicationSchema(t *testing.T) {
t.Run("fresh schema passes", func(t *testing.T) {
isolationDB(t, false)
if err := NewApplicationRepository().CheckApplicationSchema(); err != nil {
t.Fatalf("新库不应报缺列: %v", err)
}
})
t.Run("legacy schema is rejected", func(t *testing.T) {
isolationDB(t, true)
if err := NewApplicationRepository().CheckApplicationSchema(); err == nil {
t.Fatal("缺少 organization_id 的旧库必须报升级错误")
}
})
}