370 lines
12 KiB
Go
370 lines
12 KiB
Go
package services
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
|
||
"git.beifan.cn/trace-system/backend-go/database"
|
||
"git.beifan.cn/trace-system/backend-go/models"
|
||
)
|
||
|
||
// validSignatureFixture 构造一个合法的签名 dataURL(>200 字节)
|
||
func validSignatureFixture() string {
|
||
payload := strings.Repeat("a", 400)
|
||
return "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte(payload))
|
||
}
|
||
|
||
// confirmTest 为客户确认相关测试做准备:清空限流计数(多个测试可能共享同一序列号)
|
||
func confirmTest(t *testing.T) {
|
||
t.Helper()
|
||
ResetConfirmRateLimit()
|
||
}
|
||
|
||
func seedTechnician(t *testing.T, username string) models.User {
|
||
t.Helper()
|
||
user := models.User{
|
||
Username: username,
|
||
Password: "hashed",
|
||
Name: "技术员-" + username,
|
||
Email: username + "@example.com",
|
||
Role: "technician",
|
||
}
|
||
if err := database.DB.Create(&user).Error; err != nil {
|
||
t.Fatalf("seed technician failed: %v", err)
|
||
}
|
||
return user
|
||
}
|
||
|
||
func createOrderFor(t *testing.T, user models.User, phone string) *models.AftersalesOrder {
|
||
t.Helper()
|
||
svc := AftersalesService{}
|
||
order, err := svc.Create(models.CreateAftersalesOrderDTO{
|
||
CompanyName: "TestCo_" + user.Username,
|
||
CompanyAddress: "测试地址",
|
||
ContactName: "张三",
|
||
ContactPhone: phone,
|
||
ServiceType: "software",
|
||
IssueDescription: "系统无法启动",
|
||
}, user.ID)
|
||
if err != nil {
|
||
t.Fatalf("create order failed: %v", err)
|
||
}
|
||
return order
|
||
}
|
||
|
||
func TestAftersalesService_Create_GeneratesYYMMDDNNSerial(t *testing.T) {
|
||
user := seedTechnician(t, "aftersales_create_tech")
|
||
defer database.DB.Unscoped().Delete(&user)
|
||
|
||
svc := AftersalesService{}
|
||
order, err := svc.Create(models.CreateAftersalesOrderDTO{
|
||
CompanyName: "AftersalesSerialCo",
|
||
CompanyAddress: "杭州市西湖区",
|
||
ContactName: "李四",
|
||
ContactPhone: "13800001234",
|
||
ServiceType: "hardware",
|
||
IssueDescription: "硬盘故障",
|
||
}, user.ID)
|
||
|
||
assert.NoError(t, err)
|
||
assert.NotNil(t, order)
|
||
assert.True(t, strings.HasPrefix(order.SerialNumber, "zjbf-sh-"))
|
||
assert.Len(t, order.SerialNumber, len("zjbf-sh-")+8, "default serial should be 8 digits (YYMMDDNN)")
|
||
assert.Equal(t, WorkOrderStatusCreated, order.WorkOrderStatus)
|
||
assert.Equal(t, AuthorizationStatusPending, order.AuthorizationStatus)
|
||
assert.NotNil(t, order.TechnicianID)
|
||
assert.Equal(t, user.ID, *order.TechnicianID)
|
||
|
||
database.DB.Unscoped().Delete(order)
|
||
}
|
||
|
||
func TestAftersalesService_Create_DoesNotCreateManagedCompany(t *testing.T) {
|
||
user := seedTechnician(t, "aftersales_no_company_tech")
|
||
defer database.DB.Unscoped().Delete(&user)
|
||
|
||
companyName := "AftersalesCustomerOnlyCo"
|
||
database.DB.Unscoped().Where("company_name = ?", companyName).Delete(&models.Company{})
|
||
|
||
svc := AftersalesService{}
|
||
order, err := svc.Create(models.CreateAftersalesOrderDTO{
|
||
CompanyName: companyName,
|
||
CompanyAddress: "客户现场地址",
|
||
ContactName: "王五",
|
||
ContactPhone: "13800001235",
|
||
ServiceType: "software",
|
||
IssueDescription: "售后客户问题",
|
||
}, user.ID)
|
||
|
||
assert.NoError(t, err)
|
||
assert.NotNil(t, order)
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
var count int64
|
||
database.DB.Model(&models.Company{}).Where("company_name = ?", companyName).Count(&count)
|
||
assert.Equal(t, int64(0), count)
|
||
}
|
||
|
||
func TestAftersalesService_Create_SerialIncrementsWithinDay(t *testing.T) {
|
||
user := seedTechnician(t, "aftersales_seq_tech")
|
||
defer database.DB.Unscoped().Delete(&user)
|
||
|
||
svc := AftersalesService{}
|
||
first, err := svc.Create(models.CreateAftersalesOrderDTO{
|
||
CompanyName: "SeqCo", CompanyAddress: "addr", ContactName: "A", ContactPhone: "13800002000",
|
||
ServiceType: "maintenance", IssueDescription: "issue 1",
|
||
}, user.ID)
|
||
assert.NoError(t, err)
|
||
|
||
second, err := svc.Create(models.CreateAftersalesOrderDTO{
|
||
CompanyName: "SeqCo", CompanyAddress: "addr", ContactName: "A", ContactPhone: "13800002000",
|
||
ServiceType: "maintenance", IssueDescription: "issue 2",
|
||
}, user.ID)
|
||
assert.NoError(t, err)
|
||
|
||
assert.NotEqual(t, first.SerialNumber, second.SerialNumber)
|
||
// 第二单的序号应大于第一单(按天递增)
|
||
assert.True(t, second.SerialNumber > first.SerialNumber)
|
||
|
||
database.DB.Unscoped().Delete(first)
|
||
database.DB.Unscoped().Delete(second)
|
||
}
|
||
|
||
func TestAftersalesService_SubmitForConfirmation_OwnerOnly(t *testing.T) {
|
||
owner := seedTechnician(t, "aftersales_owner")
|
||
intruder := seedTechnician(t, "aftersales_intruder")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
defer database.DB.Unscoped().Delete(&intruder)
|
||
|
||
order := createOrderFor(t, owner, "13800003000")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
|
||
// 非负责人技术员不能提交
|
||
_, err := svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "已重装系统",
|
||
}, intruder)
|
||
assert.Error(t, err)
|
||
|
||
// 负责人可以提交
|
||
updated, err := svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "已重装系统",
|
||
}, owner)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, WorkOrderStatusPendingConfirmation, updated.WorkOrderStatus)
|
||
assert.Equal(t, "已重装系统", updated.ResolutionNote)
|
||
}
|
||
|
||
func TestAftersalesService_SubmitForConfirmation_RejectsClosed(t *testing.T) {
|
||
owner := seedTechnician(t, "aftersales_closed_owner")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
|
||
order := createOrderFor(t, owner, "13800004000")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
|
||
_, err := svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "first submit",
|
||
}, owner)
|
||
assert.NoError(t, err)
|
||
|
||
// 已经 pending_confirmation 状态,不能再提交
|
||
_, err = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "second submit",
|
||
}, owner)
|
||
assert.Error(t, err)
|
||
}
|
||
|
||
func TestAftersalesService_PublicQuery_MasksPhoneAndSetsScannedAt(t *testing.T) {
|
||
owner := seedTechnician(t, "aftersales_public_owner")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
|
||
order := createOrderFor(t, owner, "13800005000")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
view, err := svc.PublicQuery(order.SerialNumber)
|
||
assert.NoError(t, err)
|
||
assert.NotNil(t, view)
|
||
assert.Equal(t, order.CompanyName, view.CompanyName)
|
||
assert.Equal(t, order.ContactName, view.ContactName)
|
||
|
||
// PublicView 不应该带电话字段(只有以下字段,通过类型保证)
|
||
// 验证 ScannedAt 已设置
|
||
var refreshed models.AftersalesOrder
|
||
database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed)
|
||
assert.NotNil(t, refreshed.ScannedAt)
|
||
}
|
||
|
||
func TestAftersalesService_CustomerConfirm_Authorize(t *testing.T) {
|
||
confirmTest(t)
|
||
owner := seedTechnician(t, "aftersales_auth_owner")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
|
||
order := createOrderFor(t, owner, "13800006789")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
_, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "done",
|
||
}, owner)
|
||
|
||
sig := validSignatureFixture()
|
||
view, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||
Action: "authorize",
|
||
Signature: sig,
|
||
})
|
||
assert.NoError(t, err)
|
||
assert.NotNil(t, view)
|
||
assert.Equal(t, WorkOrderStatusClosed, view.WorkOrderStatus)
|
||
assert.Equal(t, AuthorizationStatusAuthorized, view.AuthorizationStatus)
|
||
assert.NotNil(t, view.ConfirmedAt)
|
||
assert.Equal(t, sig, view.Signature)
|
||
|
||
var refreshed models.AftersalesOrder
|
||
database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed)
|
||
assert.Equal(t, sig, refreshed.Signature)
|
||
}
|
||
|
||
func TestAftersalesService_CustomerConfirm_AuthorizeRejectsEmptySignature(t *testing.T) {
|
||
confirmTest(t)
|
||
owner := seedTechnician(t, "aftersales_emptysig_owner")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
|
||
order := createOrderFor(t, owner, "13800007777")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
_, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "done",
|
||
}, owner)
|
||
|
||
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||
Action: "authorize",
|
||
Signature: "",
|
||
})
|
||
assert.Error(t, err)
|
||
assert.Contains(t, err.Error(), "签名")
|
||
}
|
||
|
||
func TestAftersalesService_CustomerConfirm_AuthorizeRejectsInvalidSignature(t *testing.T) {
|
||
confirmTest(t)
|
||
owner := seedTechnician(t, "aftersales_badsig_owner")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
|
||
order := createOrderFor(t, owner, "13800007788")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
_, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "done",
|
||
}, owner)
|
||
|
||
// 非 dataURL 格式
|
||
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||
Action: "authorize",
|
||
Signature: "not-a-data-url",
|
||
})
|
||
assert.Error(t, err)
|
||
assert.Contains(t, err.Error(), "签名格式")
|
||
|
||
// 太短
|
||
tiny := "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte("xx"))
|
||
_, err = svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||
Action: "authorize",
|
||
Signature: tiny,
|
||
})
|
||
assert.Error(t, err)
|
||
assert.Contains(t, err.Error(), "过短")
|
||
}
|
||
|
||
func TestAftersalesService_CustomerConfirm_RejectRequiresReason(t *testing.T) {
|
||
confirmTest(t)
|
||
owner := seedTechnician(t, "aftersales_rejnoreason_owner")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
|
||
order := createOrderFor(t, owner, "13800008877")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
_, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "done",
|
||
}, owner)
|
||
|
||
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||
Action: "reject",
|
||
RejectReason: "",
|
||
})
|
||
assert.Error(t, err)
|
||
assert.Contains(t, err.Error(), "退回原因")
|
||
}
|
||
|
||
func TestAftersalesService_CustomerConfirm_RejectIncrementsCount(t *testing.T) {
|
||
confirmTest(t)
|
||
owner := seedTechnician(t, "aftersales_reject_owner")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
|
||
order := createOrderFor(t, owner, "13800008888")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
_, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "done",
|
||
}, owner)
|
||
|
||
view, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||
Action: "reject",
|
||
RejectReason: "没修好",
|
||
})
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, WorkOrderStatusRejected, view.WorkOrderStatus)
|
||
assert.Equal(t, AuthorizationStatusUnauthorized, view.AuthorizationStatus)
|
||
assert.Empty(t, view.Signature, "reject 不应该写入签名")
|
||
|
||
var refreshed models.AftersalesOrder
|
||
database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed)
|
||
assert.Equal(t, 1, refreshed.RejectCount)
|
||
assert.Contains(t, refreshed.ResolutionNote, "没修好")
|
||
|
||
// 退回后技术员可以再次提交
|
||
_, err = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{
|
||
ResolutionNote: "re-do",
|
||
}, owner)
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
func TestAftersalesService_CustomerConfirm_RejectsWrongStatus(t *testing.T) {
|
||
confirmTest(t)
|
||
owner := seedTechnician(t, "aftersales_wrongstatus_owner")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
|
||
order := createOrderFor(t, owner, "13800009999")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
// 未提交客户确认,工单仍是 created,应该拒绝
|
||
_, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{
|
||
Action: "authorize",
|
||
Signature: validSignatureFixture(),
|
||
})
|
||
assert.Error(t, err)
|
||
}
|
||
|
||
func TestAftersalesService_ForceClose_AdminOverride(t *testing.T) {
|
||
owner := seedTechnician(t, "aftersales_force_owner")
|
||
defer database.DB.Unscoped().Delete(&owner)
|
||
|
||
order := createOrderFor(t, owner, "13800001111")
|
||
defer database.DB.Unscoped().Delete(order)
|
||
|
||
svc := AftersalesService{}
|
||
updated, err := svc.ForceClose(order.SerialNumber)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, WorkOrderStatusClosed, updated.WorkOrderStatus)
|
||
assert.Equal(t, AuthorizationStatusAuthorized, updated.AuthorizationStatus)
|
||
}
|