185 lines
6.7 KiB
Go
185 lines
6.7 KiB
Go
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 的旧库必须报升级错误")
|
||
}
|
||
})
|
||
}
|