material_manage_system/internal/handler/stock_organization_test.go
2026-10-09 11:10:13 +08:00

132 lines
5.2 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 handler
import (
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
)
// 本文件验收 Task 5A 的库存接口隔离:
// 1) 库存列表、详情的 SQL 都带请求头公司,公司 A 凭物料 ID 读不到公司 B 的库存(404);
// 2) 改变库存域配置的写请求(预警调整)在公司未配置启用仓库、库位时回 409。
// 中间件链:AuthMiddleware(按 user_id 现查有效角色)→ OrganizationMiddleware(现查公司授权)
// → 业务 Handler,所有 SQL 期望都必须带对组织参数。
// 与 repository/stock.go 的查询字面量对得上的稳定子串。
const (
stockByMaterialPattern = `FROM materials m.*WHERE m\.organization_id = \? AND m\.id = \?`
stockCountPattern = `SELECT COUNT\(\*\) FROM materials m.*WHERE m\.organization_id = \?`
stockListPattern = `SELECT m\.id, m\.material_code.*ORDER BY m\.id LIMIT \? OFFSET \?`
stockWarehousePattern = `SELECT w.id, w.organization_id`
)
func stockMaterialColumns() []string {
return []string{"id", "material_code", "name", "spec", "unit", "on_hand", "minimum_available", "enabled", "warning_configured", "major_category", "minor_category"}
}
func warehouseSummaryColumns() []string {
return []string{"id", "organization_id", "code", "name", "status", "location_id", "location_code", "location_name", "location_status"}
}
func newStockRequest(t *testing.T, method, path, body string, organizationID int64) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(method, path, strings.NewReader(body))
req.Header.Set("Authorization", "Bearer "+contextToken(t))
req.Header.Set("X-Organization-ID", strconv.FormatInt(organizationID, 10))
if body != "" {
req.Header.Set("Content-Type", "application/json")
}
w := httptest.NewRecorder()
gin.SetMode(gin.TestMode)
router := gin.New()
NewHandler().RegisterRoutes(router)
router.ServeHTTP(w, req)
return w
}
// TestStockDetailScopesToOrganization:库存详情按请求公司过滤,只有本公司物料能读到;
// 传别家公司的物料 ID 在读取阶段就查不到,接口回 404,而不是泄露别家公司的余额。
func TestStockDetailScopesToOrganization(t *testing.T) {
for _, tc := range []struct {
name string
material int64
found bool
wantCode int
}{
{"own material", 5, true, 200},
{"another company's material", 7, false, 404},
} {
t.Run(tc.name, func(t *testing.T) {
mock := contextMock(t)
expectContextPrincipal(mock, "warehouse_admin")
expectMaterialOrgAccess(mock, 1)
rows := sqlmock.NewRows(stockMaterialColumns())
if tc.found {
rows.AddRow(5, "MAT-005", "轴承", "6204", "个", "12.000000", "3.000000", true, true, "A01", "A01-01")
}
mock.ExpectQuery(`(?s)`+stockByMaterialPattern).WithArgs(int64(1), tc.material).
WillReturnRows(rows)
w := newStockRequest(t, "GET", "/api/stock/"+strconv.FormatInt(tc.material, 10), "", 1)
if w.Code != tc.wantCode {
t.Fatalf("status=%d body=%s, want %d", w.Code, w.Body.String(), tc.wantCode)
}
if tc.found && !strings.Contains(w.Body.String(), "MAT-005") {
t.Fatalf("expected own material in body, got %s", w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
})
}
}
// TestStockListScopesToOrganization:列表的 COUNT 与主查询都带请求公司,分页总数和
// 实际行不会跨公司汇总。
func TestStockListScopesToOrganization(t *testing.T) {
mock := contextMock(t)
expectContextPrincipal(mock, "warehouse_admin")
expectMaterialOrgAccess(mock, 2)
mock.ExpectQuery(`(?s)` + stockCountPattern).WithArgs(int64(2)).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
mock.ExpectQuery(`(?s)`+stockListPattern).WithArgs(int64(2), 20, 0).
WillReturnRows(sqlmock.NewRows(stockMaterialColumns()).
AddRow(9, "ORG2-009", "电机", "80W", "台", "4.000000", "0.000000", true, true, "B01", "B01-01"))
w := newStockRequest(t, "GET", "/api/stock", "", 2)
if w.Code != 200 || !strings.Contains(w.Body.String(), "ORG2-009") {
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
// TestStockWarningWriteReturnsConflictWhenScopeUnavailable:预警调整是改变库存域的写操作,
// 公司没有启用仓库时必须回 409 并给出计划指定的配置提示,不能写进默认公司。
func TestStockWarningWriteReturnsConflictWhenScopeUnavailable(t *testing.T) {
mock := contextMock(t)
expectContextPrincipal(mock, "warehouse_admin")
expectMaterialOrgAccess(mock, 1)
// 该公司没有仓库:Summary 返回 nil。
mock.ExpectQuery(stockWarehousePattern).WithArgs(int64(1)).
WillReturnRows(sqlmock.NewRows(warehouseSummaryColumns()))
w := newStockRequest(t, "PUT", "/api/stock/5/warning", `{"warning_stock":10}`, 1)
if w.Code != 409 {
t.Fatalf("未配置仓库的写操作应当返回 409,实际 status=%d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "请先配置本公司仓库") {
t.Fatalf("应回具体配置提示,实际 body=%s", w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}