a44ed21967
- Add GET /api/tenant/publish-records: brand-scoped paginated list that merges in-flight (queued/publishing) records ahead of history (success/failed/cancelled), enriched with article title and latest desktop task id; wire up service/handler/router/swagger/router_test - Rework PublishManagementView to consume publish-records instead of desktop publish tasks; add publishRecordsApi + shared-types (ListPublishRecordsParams, PublishRecordListResponse) - Enterprise site Update: distinguish explicit null from missing PATCH fields via EnterpriseSitePatchString, dedup site_url before write, verify RowsAffected, and map unique-constraint violations to a clear conflict - MediaView: enterprise site edit/delete flows with favicon fallback handling - Localize enterprise site / PBootCMS error messages to Chinese across the backend and the admin-web error map, plus legacy raw-message translation - deploy: gate migration-coupled service rollouts behind RUN_MIGRATIONS unless ALLOW_SKIP_MIGRATIONS_FOR_APP_ROLLOUT=true; handle empty SERVICES safely Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
118 lines
3.8 KiB
Go
118 lines
3.8 KiB
Go
package transport
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func init() {
|
|
gin.SetMode(gin.TestMode)
|
|
}
|
|
|
|
func TestProtectedRoutes_RequireAuth(t *testing.T) {
|
|
// Build a minimal Gin engine with just the auth middleware and a mock handler
|
|
// to verify that unauthenticated requests are rejected at the middleware level.
|
|
// We can't fully test RegisterRoutes without a bootstrap.App (needs DB/Redis),
|
|
// so we test the middleware behavior directly.
|
|
|
|
routes := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{http.MethodGet, "/api/auth/me"},
|
|
{http.MethodPost, "/api/auth/password"},
|
|
{http.MethodPost, "/api/auth/logout"},
|
|
{http.MethodPost, "/api/desktop/clients/register"},
|
|
{http.MethodGet, "/api/tenant/accounts"},
|
|
{http.MethodPost, "/api/tenant/accounts/:id/request-delete"},
|
|
{http.MethodPost, "/api/tenant/tasks/:id/reconcile"},
|
|
{http.MethodPost, "/api/tenant/tasks/:id/cancel"},
|
|
{http.MethodGet, "/api/tenant/publish-tasks"},
|
|
{http.MethodPost, "/api/tenant/publish-tasks/:id/retry"},
|
|
{http.MethodGet, "/api/tenant/publish-records"},
|
|
{http.MethodPost, "/api/tenant/publish-jobs"},
|
|
{http.MethodGet, "/api/tenant/media/platforms"},
|
|
{http.MethodGet, "/api/tenant/workspace/overview"},
|
|
{http.MethodGet, "/api/tenant/templates"},
|
|
{http.MethodGet, "/api/tenant/articles"},
|
|
{http.MethodGet, "/api/tenant/brands"},
|
|
{http.MethodGet, "/api/tenant/brands/library-summary"},
|
|
{http.MethodGet, "/api/tenant/monitoring/brands/:brand_id/questions/:question_id/detail"},
|
|
{http.MethodPost, "/api/tenant/brands"},
|
|
}
|
|
|
|
for _, rt := range routes {
|
|
t.Run(rt.method+" "+rt.path, func(t *testing.T) {
|
|
// A real engine with auth middleware would reject these.
|
|
// Here we verify the route patterns are what we expect.
|
|
// This is a documentation-style test that pins the API surface.
|
|
assert.NotEmpty(t, rt.path)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublicRoutes_NoAuth(t *testing.T) {
|
|
// Verify public routes don't need auth by checking expected paths
|
|
publicRoutes := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{http.MethodGet, "/api/auth/password-key"},
|
|
{http.MethodPost, "/api/auth/login"},
|
|
{http.MethodPost, "/api/auth/refresh"},
|
|
{http.MethodGet, "/api/health/live"},
|
|
{http.MethodGet, "/api/health/ready"},
|
|
}
|
|
|
|
for _, rt := range publicRoutes {
|
|
t.Run(rt.method+" "+rt.path, func(t *testing.T) {
|
|
assert.NotEmpty(t, rt.path)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDesktopClientRoutes_ClientTokenAuth(t *testing.T) {
|
|
routes := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{http.MethodGet, "/api/desktop/dispatch"},
|
|
{http.MethodGet, "/api/desktop/accounts"},
|
|
{http.MethodGet, "/api/desktop/publish-tasks"},
|
|
{http.MethodGet, "/api/desktop/content/assets/:token"},
|
|
{http.MethodPost, "/api/desktop/clients/offline"},
|
|
{http.MethodPost, "/api/desktop/tasks/lease"},
|
|
{http.MethodPost, "/api/desktop/tasks/:id/result"},
|
|
{http.MethodPost, "/api/desktop/publish-tasks/:id/retry"},
|
|
{http.MethodPost, "/api/desktop/monitoring/tasks/lease"},
|
|
{http.MethodPost, "/api/desktop/monitoring/tasks/resume"},
|
|
{http.MethodPost, "/api/desktop/monitoring/tasks/:id/result"},
|
|
{http.MethodPost, "/api/desktop/monitoring/tasks/:id/skip"},
|
|
{http.MethodPost, "/api/desktop/monitoring/tasks/:id/cancel"},
|
|
}
|
|
|
|
for _, rt := range routes {
|
|
t.Run(rt.method+" "+rt.path, func(t *testing.T) {
|
|
assert.NotEmpty(t, rt.path)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHealthEndpoint_Live(t *testing.T) {
|
|
r := gin.New()
|
|
r.GET("/api/health/live", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": gin.H{"status": "alive"}})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/health/live", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "alive")
|
|
}
|