mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
merge(cordis): abort helpers and gold upgrade fixture
Bring Wavelet AbortNotFoundIfMissing/AbortBadRequestOnError, switch OpenFlare handlers to them, and pin the golden upgrade test to v3.5.4 via git archive.
This commit is contained in:
@@ -31,6 +31,7 @@ import (
|
||||
|
||||
const (
|
||||
goldenRoot = "/Users/ryan/Code/Go/OpenFlare"
|
||||
goldCommit = "9f79fb99"
|
||||
goldGooseVersion = int64(202608090003)
|
||||
sampleZoneDomain = "l3-upgrade-golden.example"
|
||||
goldMigrateWait = 75 * time.Second
|
||||
@@ -41,6 +42,7 @@ const (
|
||||
var (
|
||||
goldBinOnce sync.Once
|
||||
goldBinPath string
|
||||
goldSrcDir string
|
||||
goldBinErr error
|
||||
)
|
||||
|
||||
@@ -244,10 +246,46 @@ func killGolden(cmd *exec.Cmd) {
|
||||
func buildGoldenBinary(t *testing.T) string {
|
||||
t.Helper()
|
||||
goldBinOnce.Do(func() {
|
||||
if _, err := os.Stat(filepath.Join(goldenRoot, "main.go")); err != nil {
|
||||
goldBinErr = fmt.Errorf("golden tree %s: %w", goldenRoot, err)
|
||||
src, err := os.MkdirTemp("", "of-gold-src-")
|
||||
if err != nil {
|
||||
goldBinErr = err
|
||||
return
|
||||
}
|
||||
archive := exec.Command("git", "-C", goldenRoot, "archive", goldCommit)
|
||||
extract := exec.Command("tar", "-x", "-C", src)
|
||||
pipe, err := archive.StdoutPipe()
|
||||
if err != nil {
|
||||
goldBinErr = fmt.Errorf("gold archive pipe: %w", err)
|
||||
return
|
||||
}
|
||||
extract.Stdin = pipe
|
||||
var archiveErr, extractErr bytes.Buffer
|
||||
archive.Stderr = &archiveErr
|
||||
extract.Stderr = &extractErr
|
||||
if err := archive.Start(); err != nil {
|
||||
goldBinErr = fmt.Errorf("git archive %s: %w", goldCommit, err)
|
||||
return
|
||||
}
|
||||
if err := extract.Start(); err != nil {
|
||||
_ = archive.Process.Kill()
|
||||
goldBinErr = fmt.Errorf("extract gold %s: %w", goldCommit, err)
|
||||
return
|
||||
}
|
||||
if err := extract.Wait(); err != nil {
|
||||
_ = archive.Wait()
|
||||
goldBinErr = fmt.Errorf("extract gold %s: %w\n%s", goldCommit, err, extractErr.String())
|
||||
return
|
||||
}
|
||||
if err := archive.Wait(); err != nil {
|
||||
goldBinErr = fmt.Errorf("git archive %s: %w\n%s", goldCommit, err, archiveErr.String())
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(src, "main.go")); err != nil {
|
||||
goldBinErr = fmt.Errorf("gold %s at %s: %w", goldCommit, src, err)
|
||||
return
|
||||
}
|
||||
goldSrcDir = src
|
||||
|
||||
dir, err := os.MkdirTemp("", "of-gold-bin-")
|
||||
if err != nil {
|
||||
goldBinErr = err
|
||||
@@ -255,12 +293,12 @@ func buildGoldenBinary(t *testing.T) string {
|
||||
}
|
||||
out := filepath.Join(dir, "gold")
|
||||
cmd := exec.Command("go", "build", "-o", out, ".")
|
||||
cmd.Dir = goldenRoot
|
||||
cmd.Dir = src
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &buf
|
||||
if err := cmd.Run(); err != nil {
|
||||
goldBinErr = fmt.Errorf("go build golden: %w\n%s", err, buf.String())
|
||||
goldBinErr = fmt.Errorf("go build golden %s: %w\n%s", goldCommit, err, buf.String())
|
||||
return
|
||||
}
|
||||
goldBinPath = out
|
||||
@@ -273,8 +311,9 @@ func buildGoldenBinary(t *testing.T) string {
|
||||
|
||||
func copyGoldConfig(t *testing.T, dir string) string {
|
||||
t.Helper()
|
||||
buildGoldenBinary(t)
|
||||
dst := filepath.Join(dir, "config.yaml")
|
||||
src, err := os.Open(filepath.Join(goldenRoot, "config.example.yaml")) //nolint:gosec // fixed golden path
|
||||
src, err := os.Open(filepath.Join(goldSrcDir, "config.example.yaml")) //nolint:gosec // extracted gold snapshot
|
||||
if err != nil {
|
||||
t.Fatalf("open golden config.example.yaml: %v", err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package dashboard
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"Wavelet/openflare/plugins/server/kernel/apiutil"
|
||||
"Wavelet/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -26,7 +25,7 @@ import (
|
||||
// @Router /api/v1/d/dashboard/overview [get]
|
||||
func GetOverviewHandler(c *gin.Context) {
|
||||
overview, err := GetOverview(c.Request.Context())
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(overview))
|
||||
|
||||
@@ -44,7 +44,7 @@ func RegisterHandler(c *gin.Context) {
|
||||
} else {
|
||||
result, err = RegisterWithDiscovery(c.Request.Context(), payload)
|
||||
}
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
@@ -76,7 +76,7 @@ func HeartbeatHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
heartbeat, err := HeartbeatNode(c.Request.Context(), authNode, payload)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(heartbeat))
|
||||
@@ -98,7 +98,7 @@ func GetActiveConfigHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
config, err := GetActiveConfig(c.Request.Context())
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(config))
|
||||
@@ -122,7 +122,7 @@ func SyncWAFIPGroupsHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
result, err := SyncWAFIPGroups(c.Request.Context(), input)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
@@ -149,7 +149,7 @@ func ReportApplyLogHandler(c *gin.Context) {
|
||||
payload.NodeID = authNode.NodeID
|
||||
}
|
||||
log, err := ReportApplyLog(c.Request.Context(), payload)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(log))
|
||||
@@ -172,7 +172,7 @@ func GetPagesDeploymentHashHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
hash, err := pages.GetDeploymentPackageHash(c.Request.Context(), deploymentID)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(protocol.PagesDeploymentHashResponse{
|
||||
@@ -198,7 +198,7 @@ func DownloadPagesPackageHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
packageObj, err := pages.OpenDeploymentPackage(c.Request.Context(), deploymentID)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
defer func() { _ = packageObj.Body.Close() }()
|
||||
@@ -226,7 +226,7 @@ func GetPagesProjectLatestHashHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
metadata, err := pages.GetProjectLatestPackageMetadata(c.Request.Context(), projectID)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(protocol.PagesProjectLatestHashResponse{
|
||||
@@ -256,7 +256,7 @@ func DownloadPagesProjectLatestPackageHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
packageObj, err := pages.OpenProjectLatestPackage(c.Request.Context(), projectID)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
defer func() { _ = packageObj.Body.Close() }()
|
||||
|
||||
@@ -45,7 +45,7 @@ func PostHeartbeat(c *gin.Context) {
|
||||
}
|
||||
|
||||
result, err := Heartbeat(c.Request.Context(), node, payload)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
@@ -75,7 +75,7 @@ func GetActiveConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
config, err := GetTunnelConfig(c.Request.Context(), node)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(config))
|
||||
@@ -106,7 +106,7 @@ func PostApplyLog(c *gin.Context) {
|
||||
}
|
||||
|
||||
log, err := ReportApplyLog(c.Request.Context(), payload)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(log))
|
||||
|
||||
@@ -19,7 +19,7 @@ func handleLogicError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return apiutil.AbortNotFoundIfMissing(c, err, errNodeNotFound)
|
||||
return response.AbortNotFoundIfMissing(c, err, errNodeNotFound)
|
||||
}
|
||||
|
||||
// ListNodesHandler lists all nodes.
|
||||
|
||||
@@ -46,7 +46,7 @@ func PostHeartbeat(c *gin.Context) {
|
||||
}
|
||||
|
||||
result, err := Heartbeat(c.Request.Context(), node, payload)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
|
||||
@@ -39,7 +39,7 @@ func GetAccessLogOverviewHandler(c *gin.Context) {
|
||||
Hours: readQueryInt(c, "hours"),
|
||||
BucketMinutes: readQueryInt(c, "bucket_minutes"),
|
||||
})
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
@@ -70,11 +70,11 @@ func GetAccessLogOverviewHandler(c *gin.Context) {
|
||||
// @Router /api/v1/d/access-logs [get]
|
||||
func GetAccessLogsHandler(c *gin.Context) {
|
||||
query, err := readAccessLogQuery(c)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
logs, err := ListAccessLogs(c.Request.Context(), query)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(logs))
|
||||
@@ -103,12 +103,12 @@ func GetAccessLogsHandler(c *gin.Context) {
|
||||
// @Router /api/v1/d/access-logs/folds [get]
|
||||
func GetFoldedAccessLogsHandler(c *gin.Context) {
|
||||
query, err := readAccessLogQuery(c)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
query.FoldMinutes = readQueryInt(c, "fold_minutes")
|
||||
logs, err := ListFoldedAccessLogs(c.Request.Context(), query)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(logs))
|
||||
@@ -149,7 +149,7 @@ func GetFoldedAccessLogIPsHandler(c *gin.Context) {
|
||||
SortBy: c.Query("sort_by"),
|
||||
SortOrder: c.Query("sort_order"),
|
||||
})
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
@@ -190,7 +190,7 @@ func GetAccessLogIPSummariesHandler(c *gin.Context) {
|
||||
SortBy: c.Query("sort_by"),
|
||||
SortOrder: c.Query("sort_order"),
|
||||
})
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
@@ -221,7 +221,7 @@ func GetAccessLogIPTrendHandler(c *gin.Context) {
|
||||
Hours: readQueryInt(c, "hours"),
|
||||
BucketMinutes: readQueryInt(c, "bucket_minutes"),
|
||||
})
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
@@ -250,7 +250,7 @@ func GetAccessLogIPAnalysisHandler(c *gin.Context) {
|
||||
Host: c.Query("host"),
|
||||
Hours: readQueryInt(c, "hours"),
|
||||
})
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
@@ -276,7 +276,7 @@ func CleanupAccessLogsHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
result, err := CleanupAccessLogs(c.Request.Context(), input)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
|
||||
@@ -41,7 +41,7 @@ func GetStatusHandler(c *gin.Context) {
|
||||
// @Router /api/v1/d/option [get]
|
||||
func ListOptionsHandler(c *gin.Context) {
|
||||
options, err := listOptions(c.Request.Context())
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(options))
|
||||
@@ -66,7 +66,7 @@ func UpdateOptionHandler(c *gin.Context) {
|
||||
if !apiutil.BindJSON(c, &option) {
|
||||
return
|
||||
}
|
||||
if apiutil.AbortBadRequestOnError(c, updateOption(c.Request.Context(), option)) {
|
||||
if response.AbortBadRequestOnError(c, updateOption(c.Request.Context(), option)) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
@@ -91,7 +91,7 @@ func UpdateOptionsBatchHandler(c *gin.Context) {
|
||||
if !apiutil.BindJSON(c, &payload) {
|
||||
return
|
||||
}
|
||||
if apiutil.AbortBadRequestOnError(c, updateOptionsBatch(c.Request.Context(), payload)) {
|
||||
if response.AbortBadRequestOnError(c, updateOptionsBatch(c.Request.Context(), payload)) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
@@ -117,7 +117,7 @@ func LookupGeoIPHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
view, err := lookupGeoIP(c.Request.Context(), request.Provider, request.IP)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(view))
|
||||
@@ -137,7 +137,7 @@ func LookupGeoIPHandler(c *gin.Context) {
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/d/uptimekuma/sync [post]
|
||||
func SyncUptimeKumaHandler(c *gin.Context) {
|
||||
if apiutil.AbortBadRequestOnError(c, syncUptimeKuma(c.Request.Context())) {
|
||||
if response.AbortBadRequestOnError(c, syncUptimeKuma(c.Request.Context())) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK("同步成功"))
|
||||
|
||||
@@ -24,7 +24,7 @@ func handleLogicError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return apiutil.AbortNotFoundIfMissing(c, err, errPagesProjectNotFound)
|
||||
return response.AbortNotFoundIfMissing(c, err, errPagesProjectNotFound)
|
||||
}
|
||||
|
||||
func handleSourceLogicError(c *gin.Context, err error) bool {
|
||||
|
||||
@@ -35,7 +35,7 @@ func GetApplyLogs(c *gin.Context) {
|
||||
PageNo: readIntQuery(c, "pageNo", "page_no"),
|
||||
PageSize: readIntQuery(c, "pageSize", "page_size"),
|
||||
})
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
@@ -61,7 +61,7 @@ func CleanupApplyLogs(c *gin.Context) {
|
||||
}
|
||||
|
||||
result, err := Cleanup(c.Request.Context(), input)
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
if response.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
|
||||
@@ -16,7 +16,7 @@ func handleLogicError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return apiutil.AbortNotFoundIfMissing(c, err, "记录不存在")
|
||||
return response.AbortNotFoundIfMissing(c, err, "记录不存在")
|
||||
}
|
||||
|
||||
func versionParam(c *gin.Context) (string, bool) {
|
||||
@@ -83,7 +83,7 @@ func GetConfigVersionHandler(c *gin.Context) {
|
||||
// @Router /api/v1/d/config-versions/active [get]
|
||||
func GetActiveConfigVersionHandler(c *gin.Context) {
|
||||
version, err := GetActiveConfigVersion(c.Request.Context())
|
||||
if apiutil.AbortNotFoundIfMissing(c, err, errNoActiveVersion) {
|
||||
if response.AbortNotFoundIfMissing(c, err, errNoActiveVersion) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(version))
|
||||
|
||||
@@ -16,7 +16,7 @@ func handleLogicError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return apiutil.AbortNotFoundIfMissing(c, err, errOriginNotFound)
|
||||
return response.AbortNotFoundIfMissing(c, err, errOriginNotFound)
|
||||
}
|
||||
|
||||
// GetOrigins 列出全部源站。
|
||||
|
||||
@@ -16,7 +16,7 @@ func handleLogicError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return apiutil.AbortNotFoundIfMissing(c, err, errProxyRouteNotFound)
|
||||
return response.AbortNotFoundIfMissing(c, err, errProxyRouteNotFound)
|
||||
}
|
||||
|
||||
// GetProxyRoutes 列出全部代理规则。
|
||||
|
||||
@@ -16,7 +16,7 @@ func handleLogicError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return apiutil.AbortNotFoundIfMissing(c, err, "记录不存在")
|
||||
return response.AbortNotFoundIfMissing(c, err, "记录不存在")
|
||||
}
|
||||
|
||||
// GetCertificates 列出 TLS 证书。
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package apiutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"Wavelet/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AbortNotFoundIfMissing maps gorm.ErrRecordNotFound to 404; other errors to 400.
|
||||
func AbortNotFoundIfMissing(c *gin.Context, err error, notFoundMsg string) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.AbortNotFound(c, notFoundMsg)
|
||||
return true
|
||||
}
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
// AbortBadRequestOnError writes a 400 for any non-nil error.
|
||||
func AbortBadRequestOnError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return true
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AbortBadRequest 以 400 中断请求并将错误挂载到 Gin Error 链,供全局中间件统一记录 Trace 并响应。
|
||||
@@ -43,3 +45,27 @@ func AbortTooManyRequests(c *gin.Context, msg string) {
|
||||
func AbortConflict(c *gin.Context, msg string) {
|
||||
AbortWithError(c, http.StatusConflict, msg)
|
||||
}
|
||||
|
||||
// AbortNotFoundIfMissing 在 err 非空时中断请求:gorm.ErrRecordNotFound 映射为 404(文案 notFoundMsg),其余映射为 400(文案 err.Error())。
|
||||
// 返回是否已中断。err 为 nil 时不写响应并返回 false。
|
||||
func AbortNotFoundIfMissing(c *gin.Context, err error, notFoundMsg string) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
AbortNotFound(c, notFoundMsg)
|
||||
return true
|
||||
}
|
||||
AbortBadRequest(c, err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
// AbortBadRequestOnError 在 err 非空时以 400 中断请求,文案为 err.Error()。
|
||||
// 返回是否已中断。err 为 nil 时不写响应并返回 false。
|
||||
func AbortBadRequestOnError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
AbortBadRequest(c, err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package response
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestAbortNotFoundIfMissing(t *testing.T) {
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
assert.False(t, AbortNotFoundIfMissing(c, nil, "gone"))
|
||||
assert.False(t, c.IsAborted())
|
||||
assert.Empty(t, c.Errors)
|
||||
})
|
||||
|
||||
t.Run("record not found", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
assert.True(t, AbortNotFoundIfMissing(c, gorm.ErrRecordNotFound, "记录不存在"))
|
||||
assert.True(t, c.IsAborted())
|
||||
var apiErr *APIError
|
||||
require.True(t, errors.As(c.Errors.Last().Err, &apiErr))
|
||||
assert.Equal(t, http.StatusNotFound, apiErr.Code)
|
||||
assert.Equal(t, "记录不存在", apiErr.Msg)
|
||||
})
|
||||
|
||||
t.Run("other error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
assert.True(t, AbortNotFoundIfMissing(c, errors.New("boom"), "记录不存在"))
|
||||
assert.True(t, c.IsAborted())
|
||||
var apiErr *APIError
|
||||
require.True(t, errors.As(c.Errors.Last().Err, &apiErr))
|
||||
assert.Equal(t, http.StatusBadRequest, apiErr.Code)
|
||||
assert.Equal(t, "boom", apiErr.Msg)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAbortBadRequestOnError(t *testing.T) {
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
assert.False(t, AbortBadRequestOnError(c, nil))
|
||||
assert.False(t, c.IsAborted())
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
assert.True(t, AbortBadRequestOnError(c, errors.New("bad")))
|
||||
assert.True(t, c.IsAborted())
|
||||
var apiErr *APIError
|
||||
require.True(t, errors.As(c.Errors.Last().Err, &apiErr))
|
||||
assert.Equal(t, http.StatusBadRequest, apiErr.Code)
|
||||
assert.Equal(t, "bad", apiErr.Msg)
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ sidebar: false
|
||||
### 🛠 修复
|
||||
|
||||
- 人机验证与健康检查去掉双路径:浏览器只请求 `/api/v1/cap/challenge` 与 `/api/v1/cap/redeem`,探针只保留 `GET /api/healthz`(`{"status":"ok"}`)。旧的 `/api/cap/*`、`/api/health` 与 `/healthz` 不再注册。
|
||||
- Handler 把「记录不存在 → 404、其它错误 → 400」的分支改走上游 `response.AbortNotFoundIfMissing` / `AbortBadRequestOnError`,不再在 OpenFlare 里各写一份。
|
||||
- 控制面 `server` 插件按限界上下文重排目录:去掉 `openflare/` 与 `router/v1` 嵌套;业务在 `domain/`(site/fleet/pages 等),共享内核在 `kernel/`(model/repository 与适配器),HTTP 装配在 `httpapi`。接口路径与表结构不变。
|
||||
- `server` 插件把 stamp、of_* SQL 与 ClickHouse 迁入单一 `migrate/` 包,updater 提到 `server/updater/`;删除已停用的 76 条历史迁移。全新安装会写入 OpenFlare 定时任务与产品配置默认值,已 stamp 的升级库不重插。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user