diff --git a/backend/cmd/upgrade_from_golden_test.go b/backend/cmd/upgrade_from_golden_test.go index c7390252..7460b8a7 100644 --- a/backend/cmd/upgrade_from_golden_test.go +++ b/backend/cmd/upgrade_from_golden_test.go @@ -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) } diff --git a/backend/openflare/plugins/server/domain/dashboard/routers.go b/backend/openflare/plugins/server/domain/dashboard/routers.go index e78f32d7..644fcb02 100644 --- a/backend/openflare/plugins/server/domain/dashboard/routers.go +++ b/backend/openflare/plugins/server/domain/dashboard/routers.go @@ -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)) diff --git a/backend/openflare/plugins/server/domain/fleet/agent/routers.go b/backend/openflare/plugins/server/domain/fleet/agent/routers.go index 183f7ae7..16308d8b 100644 --- a/backend/openflare/plugins/server/domain/fleet/agent/routers.go +++ b/backend/openflare/plugins/server/domain/fleet/agent/routers.go @@ -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() }() diff --git a/backend/openflare/plugins/server/domain/fleet/flared/routers.go b/backend/openflare/plugins/server/domain/fleet/flared/routers.go index 45de2c68..c85e2808 100644 --- a/backend/openflare/plugins/server/domain/fleet/flared/routers.go +++ b/backend/openflare/plugins/server/domain/fleet/flared/routers.go @@ -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)) diff --git a/backend/openflare/plugins/server/domain/fleet/node/routers.go b/backend/openflare/plugins/server/domain/fleet/node/routers.go index c3eeb2c4..df77fb1b 100644 --- a/backend/openflare/plugins/server/domain/fleet/node/routers.go +++ b/backend/openflare/plugins/server/domain/fleet/node/routers.go @@ -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. diff --git a/backend/openflare/plugins/server/domain/fleet/relay/routers.go b/backend/openflare/plugins/server/domain/fleet/relay/routers.go index 65df8dc2..6c6bdbd7 100644 --- a/backend/openflare/plugins/server/domain/fleet/relay/routers.go +++ b/backend/openflare/plugins/server/domain/fleet/relay/routers.go @@ -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)) diff --git a/backend/openflare/plugins/server/domain/observability/routers.go b/backend/openflare/plugins/server/domain/observability/routers.go index 96f1be14..66a6a66b 100644 --- a/backend/openflare/plugins/server/domain/observability/routers.go +++ b/backend/openflare/plugins/server/domain/observability/routers.go @@ -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)) diff --git a/backend/openflare/plugins/server/domain/option/routers.go b/backend/openflare/plugins/server/domain/option/routers.go index 489d08f5..6628b9df 100644 --- a/backend/openflare/plugins/server/domain/option/routers.go +++ b/backend/openflare/plugins/server/domain/option/routers.go @@ -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("同步成功")) diff --git a/backend/openflare/plugins/server/domain/pages/routers.go b/backend/openflare/plugins/server/domain/pages/routers.go index 7759f14a..b30e6cfe 100644 --- a/backend/openflare/plugins/server/domain/pages/routers.go +++ b/backend/openflare/plugins/server/domain/pages/routers.go @@ -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 { diff --git a/backend/openflare/plugins/server/domain/site/apply_log/routers.go b/backend/openflare/plugins/server/domain/site/apply_log/routers.go index f877b0c7..d74b1ef2 100644 --- a/backend/openflare/plugins/server/domain/site/apply_log/routers.go +++ b/backend/openflare/plugins/server/domain/site/apply_log/routers.go @@ -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)) diff --git a/backend/openflare/plugins/server/domain/site/config_version/routers.go b/backend/openflare/plugins/server/domain/site/config_version/routers.go index 2fa02ace..0a3da883 100644 --- a/backend/openflare/plugins/server/domain/site/config_version/routers.go +++ b/backend/openflare/plugins/server/domain/site/config_version/routers.go @@ -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)) diff --git a/backend/openflare/plugins/server/domain/site/origin/routers.go b/backend/openflare/plugins/server/domain/site/origin/routers.go index 48b402f6..be4da7a7 100644 --- a/backend/openflare/plugins/server/domain/site/origin/routers.go +++ b/backend/openflare/plugins/server/domain/site/origin/routers.go @@ -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 列出全部源站。 diff --git a/backend/openflare/plugins/server/domain/site/proxy_route/routers.go b/backend/openflare/plugins/server/domain/site/proxy_route/routers.go index dbafaad5..aaa914d7 100644 --- a/backend/openflare/plugins/server/domain/site/proxy_route/routers.go +++ b/backend/openflare/plugins/server/domain/site/proxy_route/routers.go @@ -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 列出全部代理规则。 diff --git a/backend/openflare/plugins/server/domain/tls/routers.go b/backend/openflare/plugins/server/domain/tls/routers.go index 4a525eef..0049425a 100644 --- a/backend/openflare/plugins/server/domain/tls/routers.go +++ b/backend/openflare/plugins/server/domain/tls/routers.go @@ -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 证书。 diff --git a/backend/openflare/plugins/server/kernel/apiutil/errors.go b/backend/openflare/plugins/server/kernel/apiutil/errors.go deleted file mode 100644 index 67b88515..00000000 --- a/backend/openflare/plugins/server/kernel/apiutil/errors.go +++ /dev/null @@ -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 -} diff --git a/backend/pkg/response/abort.go b/backend/pkg/response/abort.go index 16d5902b..3d25361c 100644 --- a/backend/pkg/response/abort.go +++ b/backend/pkg/response/abort.go @@ -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 +} diff --git a/backend/pkg/response/abort_helpers_test.go b/backend/pkg/response/abort_helpers_test.go new file mode 100644 index 00000000..d05f1235 --- /dev/null +++ b/backend/pkg/response/abort_helpers_test.go @@ -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) + }) +} diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 18530712..7e0cc19e 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -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 的升级库不重插。