package handler import ( "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/wmms/wmms/internal/model" ) // 本文件验收 Task 5C 的申请/审批接口隔离:列表、详情、取消、驳回、审批都带请求头公司, // 在 A 上下文操作 B 的申请读取阶段就命中空行,回 404,且不落任何状态改动。 // 中间件链:AuthMiddleware → OrganizationMiddleware → Handler,所有 SQL 期望都必须带对组织参数。 const ( appListCountPattern = `SELECT COUNT\(\*\) FROM material_applications a WHERE a.organization_id = \? AND a.user_id = \?` appListPattern = `SELECT a.id, a.organization_id, a.user_id` appUpdatePattern = `UPDATE material_applications SET status` ) func applicationHeadRows(t *testing.T, organizationID, id, userID int64, status string) *sqlmock.Rows { t.Helper() now := time.Now() return sqlmock.NewRows([]string{"id", "organization_id", "user_id", "purpose", "status", "rejection_reason", "approved_by", "created_at", "updated_at", "username"}). AddRow(id, organizationID, userID, "领用", status, "", 0, now, now, "shared") } func emptyApplicationHeadRows() *sqlmock.Rows { return sqlmock.NewRows([]string{"id", "organization_id", "user_id", "purpose", "status", "rejection_reason", "approved_by", "created_at", "updated_at", "username"}) } func emptyApplicationItemRows() *sqlmock.Rows { return sqlmock.NewRows([]string{"id", "application_id", "material_id", "quantity", "material_code", "name"}) } // TestApplicationListScopesToOrganization:我的申请列表按请求公司 + 当前用户查询, // 计数与数据查询都带组织参数。 func TestApplicationListScopesToOrganization(t *testing.T) { mock := contextMock(t) expectContextPrincipal(mock, "warehouse_admin") expectMaterialOrgAccess(mock, 1) mock.ExpectQuery(appListCountPattern).WithArgs(int64(1), int64(7)).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) mock.ExpectQuery(appListPattern).WithArgs(int64(1), int64(7), 20, 0).WillReturnRows(emptyApplicationHeadRows()) w := newStockRequest(t, "GET", "/api/applications/my", "", 1) if w.Code != 200 { t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestApplicationCancelScopesToOrganization:本人取消待审申请时,条件更新带请求公司、 // 申请 ID 和原状态 pending。 func TestApplicationCancelScopesToOrganization(t *testing.T) { mock := contextMock(t) expectContextPrincipal(mock, "warehouse_admin") expectMaterialOrgAccess(mock, 1) mock.ExpectQuery(appListPattern).WithArgs(int64(9), int64(1)).WillReturnRows(applicationHeadRows(t, 1, 9, 7, model.ApplicationStatusPending)) mock.ExpectQuery("SELECT ai.id, ai.application_id").WithArgs(int64(1), int64(9)).WillReturnRows(emptyApplicationItemRows()) mock.ExpectExec(appUpdatePattern).WithArgs(model.ApplicationStatusCancelled, int64(1), int64(9), model.ApplicationStatusPending).WillReturnResult(sqlmock.NewResult(0, 1)) w := newStockRequest(t, "POST", "/api/applications/9/cancel", "", 1) if w.Code != 200 { t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestApplicationCancelCrossOrganizationReturnsNotFound:A 上下文取消 B 的申请,读取阶段 // 命中空行,回 404,不执行任何条件更新。 func TestApplicationCancelCrossOrganizationReturnsNotFound(t *testing.T) { mock := contextMock(t) expectContextPrincipal(mock, "warehouse_admin") expectMaterialOrgAccess(mock, 1) mock.ExpectQuery(appListPattern).WithArgs(int64(9), int64(1)).WillReturnRows(emptyApplicationHeadRows()) w := newStockRequest(t, "POST", "/api/applications/9/cancel", "", 1) if w.Code != 404 { t.Fatalf("status=%d body=%s, want 404", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestApplicationRejectCrossOrganizationReturnsNotFound:审批人驳回别家公司的申请回 404。 func TestApplicationRejectCrossOrganizationReturnsNotFound(t *testing.T) { mock := contextMock(t) expectContextPrincipal(mock, "super_admin") expectMaterialOrgAccess(mock, 1) mock.ExpectQuery(appListPattern).WithArgs(int64(9), int64(1)).WillReturnRows(emptyApplicationHeadRows()) w := newStockRequest(t, "POST", "/api/applications/9/reject", `{"reason":"no"}`, 1) if w.Code != 404 { t.Fatalf("status=%d body=%s, want 404", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestApplicationApproveCrossOrganizationReturnsNotFound:自动出库审批在事务内锁定别家 // 公司的申请命中空行,回 404,整体回滚,不解析本公司 scope。 func TestApplicationApproveCrossOrganizationReturnsNotFound(t *testing.T) { mock := contextMock(t) expectContextPrincipal(mock, "super_admin") expectMaterialOrgAccess(mock, 1) mock.ExpectBegin() mock.ExpectQuery(appListPattern).WithArgs(int64(9), int64(1)).WillReturnRows(emptyApplicationHeadRows()) mock.ExpectRollback() w := newStockRequest(t, "POST", "/api/applications/9/approve", `{"auto_outbound":true}`, 1) if w.Code != 404 { t.Fatalf("status=%d body=%s, want 404", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }