mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-10-31 02:01:44 +08:00
fix: support integer widths in website Excel imports
This commit is contained in:
+37
-221
@@ -1,14 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"SamWaf/common/uuid"
|
||||
"SamWaf/customtype"
|
||||
"SamWaf/global"
|
||||
"SamWaf/model"
|
||||
"SamWaf/model/baseorm"
|
||||
"SamWaf/model/common/response"
|
||||
"SamWaf/model/request"
|
||||
"errors"
|
||||
"SamWaf/service/waf_service"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -23,11 +19,6 @@ import (
|
||||
|
||||
type WafCommonApi struct {
|
||||
}
|
||||
type ReturnImportData struct {
|
||||
SuccessInt int
|
||||
FailInt int
|
||||
Msg string
|
||||
}
|
||||
|
||||
// 导出excel
|
||||
func (w *WafCommonApi) ExportExcelApi(c *gin.Context) {
|
||||
@@ -67,7 +58,19 @@ func (w *WafCommonApi) ExportExcelApi(c *gin.Context) {
|
||||
if field.Name == "BaseOrm" {
|
||||
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", w.GetColumnName(j), rowNum), "")
|
||||
} else {
|
||||
colValue := rowValue.Field(j).Interface()
|
||||
value := rowValue.Field(j)
|
||||
colValue := value.Interface()
|
||||
// Excel 数值仅保留 15 位有效数字,大整数使用文本单元格。
|
||||
switch value.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if n := value.Int(); n > 999999999999999 || n < -999999999999999 {
|
||||
colValue = strconv.FormatInt(n, 10)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if n := value.Uint(); n > 999999999999999 {
|
||||
colValue = strconv.FormatUint(n, 10)
|
||||
}
|
||||
}
|
||||
f.SetCellValue(sheetName, fmt.Sprintf("%s%d", w.GetColumnName(j), rowNum), colValue)
|
||||
}
|
||||
}
|
||||
@@ -93,45 +96,39 @@ func (w *WafCommonApi) ExportExcelApi(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
func (w *WafCommonApi) ImportExcelApi(c *gin.Context) {
|
||||
file, err := c.FormFile("file") // "excelFile" 对应前端上传的文件字段名
|
||||
importTable, ok := c.GetPostForm("import_table")
|
||||
if !ok || importTable != "hosts" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "import_table 仅支持 hosts"})
|
||||
return
|
||||
}
|
||||
importCodeStrategy, ok := c.GetPostForm("import_code_strategy")
|
||||
if !ok || (importCodeStrategy != "0" && importCodeStrategy != "1") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "import_code_strategy 仅支持 0 或 1"})
|
||||
return
|
||||
}
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取上传文件失败"})
|
||||
return
|
||||
}
|
||||
// 生成文件名
|
||||
fileName := time.Now().Format("20060102150405") + "import.xlsx"
|
||||
|
||||
if err := c.SaveUploadedFile(file, fileName); // 保存文件到指定路径
|
||||
err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
importTable, succResult := c.GetPostForm("import_table")
|
||||
if !succResult {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
}
|
||||
importCodeStrategy, succResult := c.GetPostForm("import_code_strategy")
|
||||
if !succResult {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
}
|
||||
// 创建 Excel 文件
|
||||
f, err := excelize.OpenFile(fileName)
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "打开上传文件失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get all the rows in the Sheet1.
|
||||
rows, err := f.GetRows("Sheet1")
|
||||
defer reader.Close()
|
||||
f, err := excelize.OpenReader(reader)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "解析 Excel 文件失败"})
|
||||
return
|
||||
}
|
||||
ret := saveDataToDatabase(importTable, rows, importCodeStrategy)
|
||||
// 删除临时文件
|
||||
if err := os.Remove(fileName); err != nil {
|
||||
log.Println("无法删除临时文件:", err)
|
||||
defer f.Close()
|
||||
rows, err := f.GetRows("Sheet1", excelize.Options{RawCellValue: true})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取 Sheet1 失败"})
|
||||
return
|
||||
}
|
||||
ret := waf_service.ImportHostsExcel(rows, importCodeStrategy)
|
||||
response.OkWithDetailed(ret, "上传完成", c)
|
||||
}
|
||||
|
||||
@@ -160,187 +157,6 @@ func (w *WafCommonApi) GetColumnName(colIdx int) string {
|
||||
return colName
|
||||
}
|
||||
|
||||
// 通用数据插入函数
|
||||
func saveDataToDatabase(tableName string, rows [][]string, importCodeStrategy string) ReturnImportData {
|
||||
successInt := 0
|
||||
failInt := 0
|
||||
msg := ""
|
||||
|
||||
processImportData(&model.Hosts{}, tableName, rows, &successInt, &failInt, &msg, importCodeStrategy)
|
||||
|
||||
return ReturnImportData{
|
||||
SuccessInt: successInt,
|
||||
FailInt: failInt,
|
||||
Msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// processImportData 是通用的导入数据函数
|
||||
func processImportData(dataType interface{}, tableName string, rows [][]string, successInt, failInt *int, msg *string, importCodeStrategy string) {
|
||||
var header []string
|
||||
needJumpFristCol := false
|
||||
rowNumber := 0
|
||||
var dataMap map[string]string
|
||||
|
||||
// 获取结构体类型和字段信息
|
||||
dataValue := reflect.ValueOf(dataType).Elem()
|
||||
dataTypeFields := dataValue.Type()
|
||||
|
||||
// 获取表头
|
||||
for _, row := range rows {
|
||||
for _, colCell := range row {
|
||||
if colCell == " - " {
|
||||
needJumpFristCol = true
|
||||
continue
|
||||
}
|
||||
header = append(header, colCell)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
isBadData := false
|
||||
// 处理数据 获取数据并插入数据库
|
||||
for _, row := range rows {
|
||||
isBadData = false
|
||||
if rowNumber == 0 && needJumpFristCol {
|
||||
rowNumber++
|
||||
continue
|
||||
}
|
||||
|
||||
// 建立一个 map 来存储数据
|
||||
dataMap = make(map[string]string)
|
||||
colNumber := 0
|
||||
for _, colCell := range row {
|
||||
if colNumber == 0 && needJumpFristCol {
|
||||
colNumber++
|
||||
continue
|
||||
}
|
||||
headerNumber := colNumber
|
||||
if needJumpFristCol {
|
||||
headerNumber = headerNumber - 1
|
||||
}
|
||||
dataMap[header[headerNumber]] = colCell
|
||||
colNumber++
|
||||
}
|
||||
|
||||
// 动态创建结构体实例,并映射数据
|
||||
newInstance := reflect.New(dataValue.Type()).Elem()
|
||||
|
||||
for fieldIdx := 0; fieldIdx < dataTypeFields.NumField(); fieldIdx++ {
|
||||
field := dataTypeFields.Field(fieldIdx)
|
||||
fieldName := field.Name
|
||||
jsonTag := field.Tag.Get("json")
|
||||
|
||||
// 如果 dataMap 中有匹配的字段
|
||||
if val, exists := dataMap[jsonTag]; exists {
|
||||
//排除一些特定数据
|
||||
if tableName == "hosts" && fieldName == "Host" && (val == "全局网站" || val == "") {
|
||||
isBadData = true
|
||||
break
|
||||
}
|
||||
//检查数据是否已经存在
|
||||
if tableName == "hosts" && fieldName == "Host" {
|
||||
if importCodeStrategy == "1" {
|
||||
errMsg, err := checkHostCodeData(dataMap["code"])
|
||||
if err != nil {
|
||||
*msg += fmt.Sprintf("行 %d, 检测数据合法性时候 出错: %v |", rowNumber, errMsg)
|
||||
*failInt++
|
||||
isBadData = true
|
||||
break
|
||||
}
|
||||
}
|
||||
errMsg, err := checkHostPortData(dataMap["host"], dataMap["port"])
|
||||
if err != nil {
|
||||
*msg += fmt.Sprintf("行 %d, 检测数据合法性时候 出错: %v |", rowNumber, errMsg)
|
||||
*failInt++
|
||||
isBadData = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// 将字段值设置到结构体中
|
||||
fieldVal := newInstance.Field(fieldIdx)
|
||||
// 转换并设置字段的值
|
||||
switch fieldVal.Kind() {
|
||||
case reflect.String:
|
||||
if tableName == "hosts" {
|
||||
if importCodeStrategy == "0" && fieldName == "Code" {
|
||||
fieldVal.SetString(uuid.GenUUID())
|
||||
} else {
|
||||
fieldVal.SetString(val)
|
||||
}
|
||||
} else {
|
||||
fieldVal.SetString(val)
|
||||
}
|
||||
case reflect.Int:
|
||||
intVal, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
*msg += fmt.Sprintf("行 %d, 字段 %s 转换为 int 错误: %v |", rowNumber, fieldName, err)
|
||||
*failInt++
|
||||
isBadData = true
|
||||
break
|
||||
}
|
||||
fieldVal.SetInt(int64(intVal))
|
||||
default:
|
||||
*msg += fmt.Sprintf("不支持的字段类型: %s |", fieldVal.Kind())
|
||||
*failInt++
|
||||
isBadData = true
|
||||
break
|
||||
}
|
||||
} else if fieldName == "BaseOrm" {
|
||||
fieldVal := newInstance.Field(fieldIdx)
|
||||
// 给 BaseOrm 赋值
|
||||
baseOrm := baseorm.BaseOrm{
|
||||
Id: uuid.GenUUID(), // 新生成的 ID
|
||||
USER_CODE: global.GWAF_USER_CODE,
|
||||
Tenant_ID: global.GWAF_TENANT_ID,
|
||||
CREATE_TIME: customtype.JsonTime(time.Now()),
|
||||
UPDATE_TIME: customtype.JsonTime(time.Now()),
|
||||
}
|
||||
// 将 BaseOrm 设置到结构体字段
|
||||
fieldVal.Set(reflect.ValueOf(baseOrm))
|
||||
} else {
|
||||
*msg += fmt.Sprintf("行 %d, 缺少字段 %s 数据 |", rowNumber, fieldName)
|
||||
*failInt++
|
||||
isBadData = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isBadData == false {
|
||||
if err := global.GWAF_LOCAL_DB.Create(newInstance.Interface()); err != nil {
|
||||
errGorm := err.Error
|
||||
if errGorm != nil {
|
||||
*msg += fmt.Sprintf("行 %d 插入失败: %v |", rowNumber, err.Error)
|
||||
*failInt++
|
||||
} else {
|
||||
*successInt++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rowNumber++
|
||||
}
|
||||
}
|
||||
|
||||
// checkHostData 检查host信息是否合法
|
||||
func checkHostCodeData(code string) (string, error) {
|
||||
// 唯一性校验:检查 `Code` 是否已存在
|
||||
if wafHostService.GetDetailByCodeApi(code).Code != "" {
|
||||
errorMsg := "Code 数据已存在不进行插入"
|
||||
// 数据已存在,不插入
|
||||
return errorMsg, errors.New(errorMsg)
|
||||
}
|
||||
return "数据正常", nil
|
||||
}
|
||||
func checkHostPortData(host string, port string) (string, error) {
|
||||
//唯一性校验:检查 `Host` 和 `Port` 的组合是否已存在
|
||||
if err := wafHostService.CheckIsExist(host, port); err == nil {
|
||||
errorMsg := "Host+Port 数据已存在不进行插入"
|
||||
// 数据已存在,不插入
|
||||
return errorMsg, errors.New(errorMsg)
|
||||
}
|
||||
return "数据正常", nil
|
||||
}
|
||||
|
||||
// HeartbeatApi 心跳数据
|
||||
func (w *WafCommonApi) HeartbeatApi(c *gin.Context) {
|
||||
response.Ok(c)
|
||||
|
||||
+407
-30
@@ -2,49 +2,426 @@ package api
|
||||
|
||||
import (
|
||||
"SamWaf/global"
|
||||
"SamWaf/model"
|
||||
"SamWaf/model/baseorm"
|
||||
"SamWaf/service/waf_service"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// 测试用例 ExportExcelApi
|
||||
func TestExportExcelApi(t *testing.T) {
|
||||
// 创建一个基于 Gin 的引擎
|
||||
r := gin.Default()
|
||||
|
||||
global.GWAF_RELEASE_VERSION = "111"
|
||||
r.GET("/api/v1/export", new(WafCommonApi).ExportExcelApi)
|
||||
// 创建一个模拟的 HTTP 请求 WafCommonReq 对象
|
||||
// 创建一个模拟的 HTTP 请求
|
||||
queryParams := url.Values{}
|
||||
queryParams.Set("table_name", "hosts")
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/api/v1/export?"+queryParams.Encode(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("无法创建请求:%v", err)
|
||||
// 每次从当前 Hosts 生成样本;新增类型必须明确提供非零测试值。
|
||||
func excelHostFixture(t *testing.T) model.Hosts {
|
||||
t.Helper()
|
||||
host := model.Hosts{}
|
||||
v := reflect.ValueOf(&host).Elem()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Type().Field(i)
|
||||
if field.Name == "BaseOrm" {
|
||||
continue
|
||||
}
|
||||
if tag := field.Tag.Get("json"); tag == "" || tag == "-" || strings.Contains(tag, ",") {
|
||||
t.Fatalf("Hosts.%s needs an explicit Excel field contract: %q", field.Name, tag)
|
||||
}
|
||||
switch value := v.Field(i); value.Kind() {
|
||||
case reflect.String:
|
||||
value.SetString("测试-" + field.Name)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
|
||||
value.SetInt(1)
|
||||
case reflect.Int64:
|
||||
value.SetInt(9223372036854775807)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
value.SetUint(1)
|
||||
case reflect.Bool:
|
||||
value.SetBool(true)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
value.SetFloat(1.25)
|
||||
default:
|
||||
t.Fatalf("add Excel round-trip coverage for Hosts.%s (%s)", field.Name, field.Type)
|
||||
}
|
||||
}
|
||||
host.BaseOrm = baseorm.BaseOrm{Id: "export-source", USER_CODE: global.GWAF_USER_CODE, Tenant_ID: global.GWAF_TENANT_ID}
|
||||
host.Host, host.Port, host.Code = "import.example", 9112, "export-source-code"
|
||||
host.GLOBAL_HOST = 0
|
||||
return host
|
||||
}
|
||||
|
||||
// 创建一个响应记录器
|
||||
func excelHostRows(host model.Hosts) [][]string {
|
||||
v := reflect.ValueOf(host)
|
||||
header, row := make([]string, v.NumField()), make([]string, v.NumField())
|
||||
for i := range header {
|
||||
if v.Type().Field(i).Name == "BaseOrm" {
|
||||
header[i] = " - "
|
||||
continue
|
||||
}
|
||||
header[i] = v.Type().Field(i).Tag.Get("json")
|
||||
row[i] = fmt.Sprint(v.Field(i).Interface())
|
||||
}
|
||||
return [][]string{header, row}
|
||||
}
|
||||
|
||||
func setExcelHostCell(t *testing.T, rows [][]string, field, value string) {
|
||||
t.Helper()
|
||||
for i, header := range rows[0] {
|
||||
if header == field {
|
||||
rows[1][i] = value
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("missing fixture column %s", field)
|
||||
}
|
||||
|
||||
func assertExcelImportedHost(t *testing.T, want model.Hosts, strategy string) {
|
||||
t.Helper()
|
||||
var hosts []model.Hosts
|
||||
if err := global.GWAF_LOCAL_DB.Find(&hosts).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(hosts) != 1 {
|
||||
t.Fatalf("want exactly one imported host, got %d", len(hosts))
|
||||
}
|
||||
got := hosts[0]
|
||||
if got.Id == "" || got.Id == want.Id || got.USER_CODE != global.GWAF_USER_CODE || got.Tenant_ID != global.GWAF_TENANT_ID || time.Time(got.CREATE_TIME).IsZero() || time.Time(got.UPDATE_TIME).IsZero() {
|
||||
t.Fatalf("imported BaseOrm metadata was not generated: %+v", got.BaseOrm)
|
||||
}
|
||||
gv, wv := reflect.ValueOf(got), reflect.ValueOf(want)
|
||||
for i := 0; i < wv.NumField(); i++ {
|
||||
name := wv.Type().Field(i).Name
|
||||
if name == "BaseOrm" {
|
||||
continue
|
||||
}
|
||||
if name == "Code" && strategy == "0" {
|
||||
if got.Code == "" || got.Code == want.Code {
|
||||
t.Fatal("strategy 0 must generate a new host code")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(gv.Field(i).Interface(), wv.Field(i).Interface()) {
|
||||
t.Errorf("Hosts.%s changed during import: want %v, got %v", name, wv.Field(i), gv.Field(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exportExcelHost(t *testing.T, host model.Hosts) []byte {
|
||||
t.Helper()
|
||||
if err := global.GWAF_LOCAL_DB.Create(&host).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := gin.New()
|
||||
r.GET("/export", new(WafCommonApi).ExportExcelApi)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// 将模拟的请求发送到测试的 API 路由
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
// 检查响应状态码是否为 200
|
||||
r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/export?table_name=hosts", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("期望的状态码:%d,实际状态码:%d", http.StatusOK, rec.Code)
|
||||
t.Fatalf("export HTTP %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
f, err := excelize.OpenReader(bytes.NewReader(rec.Body.Bytes()))
|
||||
if err != nil {
|
||||
t.Fatalf("response is not an Excel workbook: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
rows, err := f.GetRows("Sheet1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := excelHostRows(host); !reflect.DeepEqual(rows, want) {
|
||||
t.Fatalf("exported columns or cell values differ from current Hosts fields\nwant: %v\ngot: %v", want, rows)
|
||||
}
|
||||
for i, field := range rows[0] {
|
||||
if field == "emergency_until" && host.EmergencyUntil > 999999999999999 {
|
||||
cell, err := excelize.CoordinatesToCellName(i+1, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kind, err := f.GetCellType("Sheet1", cell)
|
||||
if err != nil || (kind != excelize.CellTypeSharedString && kind != excelize.CellTypeInlineString) {
|
||||
t.Fatalf("large integer must be exported as text: %v %v", kind, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return append([]byte(nil), rec.Body.Bytes()...)
|
||||
}
|
||||
|
||||
func importExcelRequest(t *testing.T, workbook []byte, table, strategy string, includeFile bool) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
if includeFile {
|
||||
part, err := writer.CreateFormFile("file", "hosts.xlsx")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := part.Write(workbook); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for key, value := range map[string]string{"import_table": table, "import_code_strategy": strategy} {
|
||||
if value != "" {
|
||||
if err := writer.WriteField(key, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := gin.New()
|
||||
r.POST("/import", func(c *gin.Context) {
|
||||
c.Set("is_openapi", true)
|
||||
new(WafCommonApi).ImportExcelApi(c)
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/import", &body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
func TestExportExcelApi(t *testing.T) {
|
||||
setupNotifyTestEnv(t, &model.Hosts{})
|
||||
t.Chdir(t.TempDir())
|
||||
exportExcelHost(t, excelHostFixture(t))
|
||||
}
|
||||
|
||||
func TestHostsExcelRoundTripAllFields(t *testing.T) {
|
||||
for _, strategy := range []string{"0", "1"} {
|
||||
t.Run("strategy_"+strategy, func(t *testing.T) {
|
||||
setupNotifyTestEnv(t, &model.Hosts{})
|
||||
t.Chdir(t.TempDir())
|
||||
host := excelHostFixture(t)
|
||||
workbook := exportExcelHost(t, host)
|
||||
if err := global.GWAF_LOCAL_DB.Where("id = ?", host.Id).Delete(&model.Hosts{}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := importExcelRequest(t, workbook, "hosts", strategy, true)
|
||||
var response struct {
|
||||
Code int
|
||||
Data waf_service.ReturnImportData
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("decode import result: %v: %s", err, rec.Body.String())
|
||||
}
|
||||
if rec.Code != http.StatusOK || response.Code != 0 || response.Data.SuccessInt != 1 || response.Data.FailInt != 0 || response.Data.Msg != "" {
|
||||
t.Fatalf("round-trip failed: HTTP %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
assertExcelImportedHost(t, host, strategy)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostsExcelImportRawNumericCell(t *testing.T) {
|
||||
setupNotifyTestEnv(t, &model.Hosts{})
|
||||
t.Chdir(t.TempDir())
|
||||
host := excelHostFixture(t)
|
||||
workbook := exportExcelHost(t, host)
|
||||
f, err := excelize.OpenReader(bytes.NewReader(workbook))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
rows, err := f.GetRows("Sheet1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, field := range rows[0] {
|
||||
if field == "emergency_until" {
|
||||
cell, err := excelize.CoordinatesToCellName(i+1, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.SetCellValue("Sheet1", cell, host.EmergencyUntil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := global.GWAF_LOCAL_DB.Where("id = ?", host.Id).Delete(&model.Hosts{}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := importExcelRequest(t, buffer.Bytes(), "hosts", "1", true)
|
||||
var response struct {
|
||||
Code int
|
||||
Data waf_service.ReturnImportData
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.Code != http.StatusOK || response.Code != 0 || response.Data.SuccessInt != 1 || response.Data.FailInt != 0 || response.Data.Msg != "" {
|
||||
t.Fatalf("raw numeric cell import failed: HTTP %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
assertExcelImportedHost(t, host, "1")
|
||||
}
|
||||
|
||||
func TestHostsExcelImportRows(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
edit func(*testing.T, [][]string) [][]string
|
||||
fail bool
|
||||
message string
|
||||
}{
|
||||
{"int64_zero", func(t *testing.T, r [][]string) [][]string { setExcelHostCell(t, r, "emergency_until", "0"); return r }, false, ""},
|
||||
{"int64_max", func(t *testing.T, r [][]string) [][]string {
|
||||
setExcelHostCell(t, r, "emergency_until", "9223372036854775807")
|
||||
return r
|
||||
}, false, ""},
|
||||
{"int64_invalid", func(t *testing.T, r [][]string) [][]string {
|
||||
setExcelHostCell(t, r, "emergency_until", "bad")
|
||||
return r
|
||||
}, true, "EmergencyUntil"},
|
||||
{"int64_overflow", func(t *testing.T, r [][]string) [][]string {
|
||||
setExcelHostCell(t, r, "emergency_until", "9223372036854775808")
|
||||
return r
|
||||
}, true, "EmergencyUntil"},
|
||||
{"two_invalid_cells_one_failure", func(t *testing.T, r [][]string) [][]string {
|
||||
setExcelHostCell(t, r, "ssl", "bad")
|
||||
setExcelHostCell(t, r, "remote_port", "bad")
|
||||
return r
|
||||
}, true, "Ssl"},
|
||||
{"without_base_column", func(t *testing.T, r [][]string) [][]string { return [][]string{r[0][1:], r[1][1:]} }, false, ""},
|
||||
{"missing_model_column", func(t *testing.T, r [][]string) [][]string {
|
||||
for i, field := range r[0] {
|
||||
if field == "emergency_until" {
|
||||
return [][]string{append(r[0][:i], r[0][i+1:]...), append(r[1][:i], r[1][i+1:]...)}
|
||||
}
|
||||
}
|
||||
t.Fatal("emergency_until column missing")
|
||||
return nil
|
||||
}, true, "EmergencyUntil"},
|
||||
{"extra_nonempty_cell", func(t *testing.T, r [][]string) [][]string { r[1] = append(r[1], "extra"); return r }, true, ""},
|
||||
{"duplicate_header", func(t *testing.T, r [][]string) [][]string {
|
||||
r[0] = append(r[0], "emergency_until")
|
||||
r[1] = append(r[1], "0")
|
||||
return r
|
||||
}, true, "EmergencyUntil"},
|
||||
{"reordered_columns", func(t *testing.T, r [][]string) [][]string {
|
||||
for i, j := 0, len(r[0])-1; i < j; i, j = i+1, j-1 {
|
||||
r[0][i], r[0][j] = r[0][j], r[0][i]
|
||||
r[1][i], r[1][j] = r[1][j], r[1][i]
|
||||
}
|
||||
return r
|
||||
}, false, ""},
|
||||
{"trailing_empty_string", func(t *testing.T, r [][]string) [][]string {
|
||||
for i, field := range r[0] {
|
||||
if field == "remarks" {
|
||||
r[0][i], r[0][len(r[0])-1] = r[0][len(r[0])-1], r[0][i]
|
||||
r[1][i] = r[1][len(r[1])-1]
|
||||
r[1] = r[1][:len(r[1])-1]
|
||||
return r
|
||||
}
|
||||
}
|
||||
t.Fatal("remarks column missing")
|
||||
return nil
|
||||
}, false, ""},
|
||||
{"empty_workbook", func(t *testing.T, r [][]string) [][]string { return nil }, true, ""},
|
||||
{"blank_row", func(t *testing.T, r [][]string) [][]string { return [][]string{r[0], {}, r[1]} }, false, ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
setupNotifyTestEnv(t, &model.Hosts{})
|
||||
host := excelHostFixture(t)
|
||||
rows := tc.edit(t, excelHostRows(host))
|
||||
result := waf_service.ImportHostsExcel(rows, "1")
|
||||
var count int64
|
||||
if err := global.GWAF_LOCAL_DB.Model(&model.Hosts{}).Count(&count).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tc.fail {
|
||||
if result.SuccessInt != 0 || result.FailInt != 1 || count != 0 || !strings.Contains(result.Msg, tc.message) {
|
||||
t.Fatalf("invalid import must fail once without insertion: %+v, count=%d", result, count)
|
||||
}
|
||||
return
|
||||
}
|
||||
if result.SuccessInt != 1 || result.FailInt != 0 || result.Msg != "" || count != 1 {
|
||||
t.Fatalf("valid import failed: %+v count=%d", result, count)
|
||||
}
|
||||
if tc.name == "int64_zero" {
|
||||
host.EmergencyUntil = 0
|
||||
}
|
||||
if tc.name == "int64_max" {
|
||||
host.EmergencyUntil = 9223372036854775807
|
||||
}
|
||||
if tc.name == "trailing_empty_string" {
|
||||
host.REMARKS = ""
|
||||
}
|
||||
assertExcelImportedHost(t, host, "1")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostsExcelImportDuplicatesAndBatch(t *testing.T) {
|
||||
for _, duplicate := range []string{"code", "host_port"} {
|
||||
t.Run(duplicate, func(t *testing.T) {
|
||||
setupNotifyTestEnv(t, &model.Hosts{})
|
||||
host := excelHostFixture(t)
|
||||
existing := host
|
||||
if duplicate == "code" {
|
||||
existing.Host = "other.example"
|
||||
} else {
|
||||
existing.Code = "other-code"
|
||||
}
|
||||
if err := global.GWAF_LOCAL_DB.Create(&existing).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := waf_service.ImportHostsExcel(excelHostRows(host), "1")
|
||||
if result.FailInt != 1 || result.SuccessInt != 0 {
|
||||
t.Fatalf("duplicate accepted: %+v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("invalid_row_then_valid", func(t *testing.T) {
|
||||
setupNotifyTestEnv(t, &model.Hosts{})
|
||||
host := excelHostFixture(t)
|
||||
rows := excelHostRows(host)
|
||||
valid := append([]string(nil), rows[1]...)
|
||||
setExcelHostCell(t, rows, "emergency_until", "invalid")
|
||||
rows = append(rows, valid)
|
||||
result := waf_service.ImportHostsExcel(rows, "1")
|
||||
if result.SuccessInt != 1 || result.FailInt != 1 || !strings.Contains(result.Msg, "2") {
|
||||
t.Fatalf("batch accounting incorrect: %+v", result)
|
||||
}
|
||||
assertExcelImportedHost(t, host, "1")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHostsExcelImportBadRequests(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, table, strategy string
|
||||
file bool
|
||||
}{
|
||||
{"no_file", "hosts", "0", false},
|
||||
{"no_table", "", "0", true},
|
||||
{"unknown_table", "unknown", "0", true},
|
||||
{"no_strategy", "hosts", "", true},
|
||||
{"unknown_strategy", "hosts", "2", true},
|
||||
{"invalid_workbook", "hosts", "0", true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rec := importExcelRequest(t, []byte("invalid workbook"), tc.table, tc.strategy, tc.file)
|
||||
if rec.Code != http.StatusBadRequest || rec.Body.Len() == 0 {
|
||||
t.Fatalf("want explained HTTP400, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetColumnName(t *testing.T) {
|
||||
colIdx := 500
|
||||
colName := ""
|
||||
for colIdx >= 0 {
|
||||
colName = fmt.Sprintf("%c", 'A'+colIdx%26) + colName
|
||||
colIdx = colIdx/26 - 1
|
||||
api := new(WafCommonApi)
|
||||
for index, want := range map[int]string{0: "A", 25: "Z", 26: "AA", 51: "AZ", 52: "BA", 500: "SG"} {
|
||||
if got := api.GetColumnName(index); got != want {
|
||||
t.Errorf("column %s: want %s, got %s", strconv.Itoa(index), want, got)
|
||||
}
|
||||
}
|
||||
fmt.Println(colName)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package waf_service
|
||||
|
||||
import (
|
||||
"SamWaf/common/uuid"
|
||||
"SamWaf/customtype"
|
||||
"SamWaf/global"
|
||||
"SamWaf/model"
|
||||
"SamWaf/model/baseorm"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ReturnImportData struct {
|
||||
SuccessInt int
|
||||
FailInt int
|
||||
Msg string
|
||||
}
|
||||
|
||||
// ImportHostsExcel 导入包含完整 Hosts 表头的网站数据。
|
||||
func ImportHostsExcel(rows [][]string, importCodeStrategy string) ReturnImportData {
|
||||
var result ReturnImportData
|
||||
if importCodeStrategy != "0" && importCodeStrategy != "1" {
|
||||
result.FailInt = 1
|
||||
result.Msg = "导入策略无效,仅支持 0 或 1"
|
||||
return result
|
||||
}
|
||||
processImportData(rows, importCodeStrategy, &result)
|
||||
return result
|
||||
}
|
||||
|
||||
func processImportData(rows [][]string, strategy string, result *ReturnImportData) {
|
||||
if len(rows) == 0 {
|
||||
result.FailInt = 1
|
||||
result.Msg = "行 1, 缺少表头 |"
|
||||
return
|
||||
}
|
||||
typ := reflect.TypeOf(model.Hosts{})
|
||||
columns := make(map[string]int)
|
||||
for column, name := range rows[0] {
|
||||
if name == " - " || name == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := columns[name]; exists {
|
||||
// 只输出模型字段信息,不回显表头文本。
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if field.Name != "BaseOrm" && field.Tag.Get("json") == name {
|
||||
result.FailInt = 1
|
||||
result.Msg = fmt.Sprintf("行 1, 字段 %s (%s) 表头重复 |", field.Name, field.Type)
|
||||
return
|
||||
}
|
||||
}
|
||||
result.FailInt = 1
|
||||
result.Msg = fmt.Sprintf("行 1, 第 %d 列表头重复 |", column+1)
|
||||
return
|
||||
}
|
||||
columns[name] = column
|
||||
}
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if field.Name == "BaseOrm" {
|
||||
continue
|
||||
}
|
||||
if _, exists := columns[field.Tag.Get("json")]; !exists {
|
||||
result.FailInt = 1
|
||||
result.Msg = fmt.Sprintf("行 1, 缺少字段 %s (%s) 表头 |", field.Name, field.Type)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for index, row := range rows[1:] {
|
||||
rowNumber := index + 2
|
||||
if importRowEmpty(row) {
|
||||
continue
|
||||
}
|
||||
var rowErr error
|
||||
for column := len(rows[0]); column < len(row); column++ {
|
||||
if strings.TrimSpace(row[column]) != "" {
|
||||
rowErr = fmt.Errorf("第 %d 列超出表头范围", column+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
if rowErr != nil {
|
||||
result.addRowFailure(rowNumber, rowErr)
|
||||
continue
|
||||
}
|
||||
hostName := importCell(row, columns["host"])
|
||||
if strings.TrimSpace(hostName) == "" || hostName == "全局网站" {
|
||||
continue
|
||||
}
|
||||
|
||||
var host model.Hosts
|
||||
value := reflect.ValueOf(&host).Elem()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if field.Name == "BaseOrm" {
|
||||
continue
|
||||
}
|
||||
if err := setImportScalar(value.Field(i), importCell(row, columns[field.Tag.Get("json")])); err != nil {
|
||||
rowErr = fmt.Errorf("字段 %s (%s) %s", field.Name, field.Type, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
if rowErr != nil {
|
||||
result.addRowFailure(rowNumber, rowErr)
|
||||
continue
|
||||
}
|
||||
if strategy == "0" {
|
||||
host.Code = uuid.GenUUID()
|
||||
} else if err := checkHostCodeData(host.Code); err != nil {
|
||||
result.addRowFailure(rowNumber, err)
|
||||
continue
|
||||
}
|
||||
if err := checkHostPortData(host.Host, host.Port); err != nil {
|
||||
result.addRowFailure(rowNumber, err)
|
||||
continue
|
||||
}
|
||||
now := customtype.JsonTime(time.Now())
|
||||
host.BaseOrm = baseorm.BaseOrm{
|
||||
Id: uuid.GenUUID(), USER_CODE: global.GWAF_USER_CODE, Tenant_ID: global.GWAF_TENANT_ID,
|
||||
CREATE_TIME: now, UPDATE_TIME: now,
|
||||
}
|
||||
if err := global.GWAF_LOCAL_DB.Create(&host).Error; err != nil {
|
||||
result.addRowFailure(rowNumber, errors.New("插入失败"))
|
||||
continue
|
||||
}
|
||||
result.SuccessInt++
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReturnImportData) addRowFailure(rowNumber int, err error) {
|
||||
r.FailInt++
|
||||
r.Msg += fmt.Sprintf("行 %d, %s |", rowNumber, err)
|
||||
}
|
||||
|
||||
func importCell(row []string, column int) string {
|
||||
if column >= len(row) {
|
||||
return ""
|
||||
}
|
||||
return row[column]
|
||||
}
|
||||
|
||||
func importRowEmpty(row []string) bool {
|
||||
for _, cell := range row {
|
||||
if strings.TrimSpace(cell) != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// setImportScalar 转换成功后才写入字段;错误不包含单元格内容。
|
||||
func setImportScalar(field reflect.Value, text string) error {
|
||||
if !field.IsValid() || !field.CanSet() {
|
||||
return errors.New("字段不可写")
|
||||
}
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
field.SetString(text)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
n, err := strconv.ParseInt(text, 10, field.Type().Bits())
|
||||
if err != nil {
|
||||
return errors.New("整数无效或超出范围")
|
||||
}
|
||||
field.SetInt(n)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
n, err := strconv.ParseUint(text, 10, field.Type().Bits())
|
||||
if err != nil {
|
||||
return errors.New("无符号整数无效或超出范围")
|
||||
}
|
||||
field.SetUint(n)
|
||||
case reflect.Bool:
|
||||
b, err := strconv.ParseBool(text)
|
||||
if err != nil {
|
||||
return errors.New("布尔值无效")
|
||||
}
|
||||
field.SetBool(b)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
n, err := strconv.ParseFloat(text, field.Type().Bits())
|
||||
if err != nil || math.IsNaN(n) || math.IsInf(n, 0) {
|
||||
return errors.New("浮点数无效或超出范围")
|
||||
}
|
||||
field.SetFloat(n)
|
||||
default:
|
||||
return errors.New("不支持的字段类型")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkHostCodeData(code string) error {
|
||||
var count int64
|
||||
if global.GWAF_LOCAL_DB == nil || global.GWAF_LOCAL_DB.Model(&model.Hosts{}).Where("code = ?", code).Count(&count).Error != nil {
|
||||
return errors.New("字段 Code (string) 重复检查失败")
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("字段 Code (string) 数据已存在不进行插入")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkHostPortData(host string, port int) error {
|
||||
var count int64
|
||||
if global.GWAF_LOCAL_DB == nil || global.GWAF_LOCAL_DB.Model(&model.Hosts{}).Where("host = ? AND port = ?", host, port).Count(&count).Error != nil {
|
||||
return errors.New("字段 Host (string) + Port (int) 重复检查失败")
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("字段 Host (string) + Port (int) 数据已存在不进行插入")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package waf_service
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetImportScalarSignedWidths(t *testing.T) {
|
||||
for _, sample := range []interface{}{int(0), int8(0), int16(0), int32(0), int64(0)} {
|
||||
typ := reflect.TypeOf(sample)
|
||||
t.Run(typ.String(), func(t *testing.T) {
|
||||
limit := new(big.Int).Lsh(big.NewInt(1), uint(typ.Bits()-1))
|
||||
min := new(big.Int).Neg(new(big.Int).Set(limit))
|
||||
max := new(big.Int).Sub(new(big.Int).Set(limit), big.NewInt(1))
|
||||
for _, valid := range []string{min.String(), max.String(), "0", "-1", "+1"} {
|
||||
field := reflect.New(typ).Elem()
|
||||
if err := setImportScalar(field, valid); err != nil {
|
||||
t.Fatalf("valid boundary rejected: %v", err)
|
||||
}
|
||||
want, _ := strconv.ParseInt(valid, 10, typ.Bits())
|
||||
if field.Int() != want {
|
||||
t.Fatalf("got %d, want %d", field.Int(), want)
|
||||
}
|
||||
}
|
||||
for _, invalid := range []string{
|
||||
new(big.Int).Sub(min, big.NewInt(1)).String(),
|
||||
new(big.Int).Add(max, big.NewInt(1)).String(),
|
||||
"", "invalid-secret", "1.5", "1e3", "0x10", " 1", "1 ",
|
||||
} {
|
||||
assertImportScalarRejected(t, typ, invalid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetImportScalarUnsignedWidths(t *testing.T) {
|
||||
for _, sample := range []interface{}{uint(0), uint8(0), uint16(0), uint32(0), uint64(0), uintptr(0)} {
|
||||
typ := reflect.TypeOf(sample)
|
||||
t.Run(typ.String(), func(t *testing.T) {
|
||||
limit := new(big.Int).Lsh(big.NewInt(1), uint(typ.Bits()))
|
||||
max := new(big.Int).Sub(new(big.Int).Set(limit), big.NewInt(1))
|
||||
for _, valid := range []string{"0", "1", max.String()} {
|
||||
field := reflect.New(typ).Elem()
|
||||
if err := setImportScalar(field, valid); err != nil {
|
||||
t.Fatalf("valid boundary rejected: %v", err)
|
||||
}
|
||||
want, _ := strconv.ParseUint(valid, 10, typ.Bits())
|
||||
if field.Uint() != want {
|
||||
t.Fatalf("got %d, want %d", field.Uint(), want)
|
||||
}
|
||||
}
|
||||
for _, invalid := range []string{limit.String(), "-1", "", "invalid-secret", "1.5", "0x10", " 1"} {
|
||||
assertImportScalarRejected(t, typ, invalid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetImportScalarStringAndBool(t *testing.T) {
|
||||
var text string
|
||||
for _, valid := range []string{"", " original text ", "中文"} {
|
||||
if err := setImportScalar(reflect.ValueOf(&text).Elem(), valid); err != nil || text != valid {
|
||||
t.Fatal("string value was not preserved")
|
||||
}
|
||||
}
|
||||
for _, valid := range []string{"true", "false", "1", "0", "TRUE", "FALSE"} {
|
||||
var value bool
|
||||
if err := setImportScalar(reflect.ValueOf(&value).Elem(), valid); err != nil {
|
||||
t.Fatalf("valid bool rejected: %v", err)
|
||||
}
|
||||
want, _ := strconv.ParseBool(valid)
|
||||
if value != want {
|
||||
t.Fatalf("got %v, want %v", value, want)
|
||||
}
|
||||
}
|
||||
for _, invalid := range []string{"", "2", "invalid-secret", " true"} {
|
||||
assertImportScalarRejected(t, reflect.TypeOf(false), invalid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetImportScalarFloatWidths(t *testing.T) {
|
||||
for _, sample := range []interface{}{float32(0), float64(0)} {
|
||||
typ := reflect.TypeOf(sample)
|
||||
t.Run(typ.String(), func(t *testing.T) {
|
||||
max, smallest := math.MaxFloat64, math.SmallestNonzeroFloat64
|
||||
overflow := "1e309"
|
||||
if typ.Bits() == 32 {
|
||||
max, smallest, overflow = math.MaxFloat32, math.SmallestNonzeroFloat32, "1e39"
|
||||
}
|
||||
for _, valid := range []string{
|
||||
"0", "-1.25", "1.5e2",
|
||||
strconv.FormatFloat(max, 'g', -1, typ.Bits()),
|
||||
strconv.FormatFloat(-max, 'g', -1, typ.Bits()),
|
||||
strconv.FormatFloat(smallest, 'g', -1, typ.Bits()),
|
||||
} {
|
||||
field := reflect.New(typ).Elem()
|
||||
if err := setImportScalar(field, valid); err != nil {
|
||||
t.Fatalf("valid float rejected: %v", err)
|
||||
}
|
||||
want, _ := strconv.ParseFloat(valid, typ.Bits())
|
||||
if field.Float() != want {
|
||||
t.Fatalf("got %v, want %v", field.Float(), want)
|
||||
}
|
||||
}
|
||||
for _, invalid := range []string{"", "invalid-secret", overflow, "-" + overflow, "NaN", "Inf", "+Inf", "-Inf", " 1"} {
|
||||
assertImportScalarRejected(t, typ, invalid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetImportScalarUnsupportedAndUnwritable(t *testing.T) {
|
||||
for _, sample := range []interface{}{
|
||||
struct{}{}, []string{}, [1]int{}, map[string]string{}, (*int)(nil), complex64(0), complex128(0),
|
||||
make(chan int), (func())(nil),
|
||||
} {
|
||||
typ := reflect.TypeOf(sample)
|
||||
t.Run(typ.String(), func(t *testing.T) {
|
||||
assertImportScalarRejected(t, typ, "invalid-secret")
|
||||
})
|
||||
}
|
||||
var value interface{}
|
||||
if err := setImportScalar(reflect.ValueOf(&value).Elem(), "invalid-secret"); err == nil {
|
||||
t.Fatal("interface field accepted")
|
||||
}
|
||||
for _, field := range []reflect.Value{reflect.Value{}, reflect.ValueOf(1)} {
|
||||
if err := setImportScalar(field, "invalid-secret"); err == nil {
|
||||
t.Fatal("unwritable field accepted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetImportScalarNamedTypes(t *testing.T) {
|
||||
type signed int64
|
||||
type unsigned uint16
|
||||
type text string
|
||||
type flag bool
|
||||
type number float32
|
||||
for _, tc := range []struct {
|
||||
initial interface{}
|
||||
input string
|
||||
want interface{}
|
||||
}{
|
||||
{signed(0), "9223372036854775807", signed(math.MaxInt64)},
|
||||
{unsigned(0), "65535", unsigned(math.MaxUint16)},
|
||||
{text(""), "unchanged", text("unchanged")},
|
||||
{flag(false), "true", flag(true)},
|
||||
{number(0), "1.25", number(1.25)},
|
||||
} {
|
||||
field := reflect.New(reflect.TypeOf(tc.initial)).Elem()
|
||||
if err := setImportScalar(field, tc.input); err != nil || !reflect.DeepEqual(field.Interface(), tc.want) {
|
||||
t.Fatalf("named scalar conversion failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertImportScalarRejected(t *testing.T, typ reflect.Type, input string) {
|
||||
t.Helper()
|
||||
field := reflect.New(typ).Elem()
|
||||
switch field.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
field.SetInt(7)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
field.SetUint(7)
|
||||
case reflect.Bool:
|
||||
field.SetBool(true)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
field.SetFloat(7)
|
||||
}
|
||||
before := field.Interface()
|
||||
err := setImportScalar(field, input)
|
||||
if err == nil {
|
||||
t.Fatal("invalid value or unsupported type accepted")
|
||||
}
|
||||
if input != "" && strings.Contains(err.Error(), input) {
|
||||
t.Fatal("error contains the input value")
|
||||
}
|
||||
if !reflect.DeepEqual(field.Interface(), before) {
|
||||
t.Fatal("failed conversion changed the field")
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,22 @@
|
||||
# 5. fresh_install: true 的条目只在"全新安装"时生成,不参与版本区间计算
|
||||
|
||||
notes:
|
||||
- id: v1_3_25_hosts_excel_integer_import
|
||||
version: v1.3.25
|
||||
kind: check
|
||||
level: low
|
||||
page: /waf-host/wafhost
|
||||
apply:
|
||||
type: navigate
|
||||
zh:
|
||||
title: 网站 Excel 导入支持各位宽整数
|
||||
detail: 网站 Excel 导入支持包括紧急模式到期时间在内的整数字段,转换错误会标明行号与字段,每行最多计一次失败。导入仍要求完整字段表头;旧文件缺少新增列时,请使用当前版本重新导出后填写。
|
||||
revert: 无需调整配置;导入前请确认文件字段完整,并核对导入结果中的成功数和失败原因。
|
||||
en:
|
||||
title: Website Excel import supports all integer widths
|
||||
detail: Website Excel import supports integer fields including the emergency-mode expiry time. Conversion errors identify the row and field, with at most one failure per row. A complete column header is still required; if an older file lacks new columns, export a fresh file from the current version before filling it in.
|
||||
revert: No configuration changes are needed. Check that the file has all columns before import, then review the success count and failure reasons.
|
||||
|
||||
- id: v1_3_25_website_access_diagnostic
|
||||
version: v1.3.25
|
||||
kind: check
|
||||
|
||||
Reference in New Issue
Block a user