Files
backend-go/services/aftersales_service_test.go
T

356 lines
12 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 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)
database.DB.Unscoped().Where("company_name = ?", "AftersalesSerialCo").Delete(&models.Company{})
}
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)
database.DB.Unscoped().Where("company_name = ?", "SeqCo").Delete(&models.Company{})
}
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
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)
defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{})
svc := AftersalesService{}
updated, err := svc.ForceClose(order.SerialNumber)
assert.NoError(t, err)
assert.Equal(t, WorkOrderStatusClosed, updated.WorkOrderStatus)
assert.Equal(t, AuthorizationStatusAuthorized, updated.AuthorizationStatus)
}