403 lines
15 KiB
Go
403 lines
15 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"kra/internal/conf"
|
|
"kra/internal/server/handler"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func emptyHandlers() *handler.Set {
|
|
return &handler.Set{
|
|
Authority: &handler.Authority{}, Menu: &handler.Menu{}, API: &handler.API{},
|
|
Permission: &handler.Permission{}, Organization: &handler.Organization{},
|
|
Announcement: &handler.Announcement{}, Email: &handler.Email{}, Payment: &handler.Payment{}, Task: &handler.Task{},
|
|
Media: &handler.Media{}, Audit: &handler.Audit{}, Export: &handler.Export{},
|
|
Version: &handler.Version{}, Dictionary: &handler.Dictionary{}, Parameter: &handler.Parameter{},
|
|
APIToken: &handler.APIToken{}, SystemConfig: &handler.SystemConfig{}, Public: &handler.Public{},
|
|
User: &handler.User{}, Navigation: &handler.Navigation{}, Session: &handler.Session{},
|
|
}
|
|
}
|
|
|
|
func TestGinRouteContract(t *testing.T) {
|
|
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
|
routes := engine.Routes()
|
|
actual := make([]string, 0, len(routes))
|
|
seen := make(map[string]struct{}, len(routes))
|
|
for _, route := range routes {
|
|
key := route.Method + " " + route.Path
|
|
if _, exists := seen[key]; exists {
|
|
t.Fatalf("duplicate route %s", key)
|
|
}
|
|
seen[key] = struct{}{}
|
|
actual = append(actual, key)
|
|
}
|
|
sort.Strings(actual)
|
|
if value := strings.Join(actual, "\n"); value != expectedGinRouteContract {
|
|
t.Fatalf("route contract changed:\n%s", value)
|
|
}
|
|
}
|
|
|
|
func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
|
var output bytes.Buffer
|
|
logger := slog.New(slog.NewJSONHandler(&output, nil))
|
|
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, logger, "test")
|
|
text := output.String()
|
|
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
|
t.Fatalf("registered route log count = %d, want %d", got, want)
|
|
}
|
|
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":186`) {
|
|
t.Fatalf("startup route summary is missing: %s", text)
|
|
}
|
|
}
|
|
|
|
func TestSwaggerUsesRegisteredGinRoutes(t *testing.T) {
|
|
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
|
response := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil)
|
|
engine.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("swagger document status = %d, body=%s", response.Code, response.Body.String())
|
|
}
|
|
body := response.Body.String()
|
|
for _, expected := range []string{`"swagger":"2.0"`, `"version":"v1.0.0"`, `"/base/login"`, `"/timedTask/triggerTimedTask"`, `"/mediaUpload/{uploadId}"`} {
|
|
if !strings.Contains(body, expected) {
|
|
t.Fatalf("swagger document missing %s", expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSwaggerSupportsRouterPrefix(t *testing.T) {
|
|
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{RouterPrefix: "/admin"}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
|
for _, path := range []string{"/admin/swagger/index.html", "/admin/swagger/doc.json"} {
|
|
response := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodGet, path, nil)
|
|
engine.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("swagger path %s status = %d, body=%s", path, response.Code, response.Body.String())
|
|
}
|
|
if strings.HasSuffix(path, "/doc.json") && !strings.Contains(response.Body.String(), `"basePath":"/admin"`) {
|
|
t.Fatalf("swagger document does not use router prefix: %s", response.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSwaggerUsesRootBasePathWithoutRouterPrefix(t *testing.T) {
|
|
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
|
response := httptest.NewRecorder()
|
|
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil))
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"basePath":"/"`) {
|
|
t.Fatalf("swagger basePath mismatch: status=%d body=%s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGinDoesNotTrustForwardedIPByDefault(t *testing.T) {
|
|
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
|
engine.GET("/client-ip", func(c *gin.Context) { c.String(http.StatusOK, c.ClientIP()) })
|
|
request := httptest.NewRequest(http.MethodGet, "/client-ip", nil)
|
|
request.RemoteAddr = "203.0.113.10:4321"
|
|
request.Header.Set("X-Forwarded-For", "198.51.100.20")
|
|
response := httptest.NewRecorder()
|
|
|
|
engine.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || response.Body.String() != "203.0.113.10" {
|
|
t.Fatalf("client IP = %q, status=%d", response.Body.String(), response.Code)
|
|
}
|
|
}
|
|
|
|
func TestAnnouncementDataSourceRequiresAuthentication(t *testing.T) {
|
|
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
|
response := httptest.NewRecorder()
|
|
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/info/getInfoDataSource", nil))
|
|
if response.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLocalStorageResponseHeaders(t *testing.T) {
|
|
root := t.TempDir()
|
|
for name, body := range map[string]string{"script.html": "<script>alert(1)</script>", "image.png": "png"} {
|
|
if err := os.WriteFile(filepath.Join(root, name), []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
|
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
|
|
|
for _, test := range []struct {
|
|
path string
|
|
attachment bool
|
|
}{
|
|
{path: "/uploads/file/script.html", attachment: true},
|
|
{path: "/uploads/file/image.png", attachment: false},
|
|
} {
|
|
response := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodGet, test.path, nil)
|
|
engine.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || response.Header().Get("X-Content-Type-Options") != "nosniff" {
|
|
t.Fatalf("unexpected static response for %s: status=%d headers=%v", test.path, response.Code, response.Header())
|
|
}
|
|
hasAttachment := strings.Contains(response.Header().Get("Content-Disposition"), "attachment")
|
|
if hasAttachment != test.attachment {
|
|
t.Fatalf("attachment header for %s = %v, want %v", test.path, hasAttachment, test.attachment)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLocalStorageRoutesAreRegistered(t *testing.T) {
|
|
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}})
|
|
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
|
seen := map[string]bool{}
|
|
for _, route := range engine.Routes() {
|
|
seen[route.Method+" "+route.Path] = true
|
|
}
|
|
for _, expected := range []string{"GET /uploads/file/*filepath", "HEAD /uploads/file/*filepath"} {
|
|
if !seen[expected] {
|
|
t.Fatalf("missing registered static route %s", expected)
|
|
}
|
|
}
|
|
response := httptest.NewRecorder()
|
|
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil))
|
|
if strings.Contains(response.Body.String(), "/uploads/file/") {
|
|
t.Fatal("swagger document should not expose static file wildcard")
|
|
}
|
|
}
|
|
|
|
func TestLocalStorageOldPrefixStopsServingAfterReload(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "image.png"), []byte("png"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
|
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
|
runtime.Replace(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "files"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
|
|
|
oldResponse := httptest.NewRecorder()
|
|
engine.ServeHTTP(oldResponse, httptest.NewRequest(http.MethodGet, "/uploads/file/image.png", nil))
|
|
if oldResponse.Code != http.StatusNotFound {
|
|
t.Fatalf("old static prefix status = %d, want %d", oldResponse.Code, http.StatusNotFound)
|
|
}
|
|
newResponse := httptest.NewRecorder()
|
|
engine.ServeHTTP(newResponse, httptest.NewRequest(http.MethodGet, "/files/image.png", nil))
|
|
if newResponse.Code != http.StatusOK {
|
|
t.Fatalf("new static prefix status = %d, want %d", newResponse.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestLocalStorageConflictingPrefixFallsBackWithoutStartupPanic(t *testing.T) {
|
|
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "api"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
|
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
|
for _, route := range engine.Routes() {
|
|
if route.Method == http.MethodGet && route.Path == "/api/*filepath" {
|
|
t.Fatal("conflicting static wildcard must not be registered")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLocalStorageRouteIsNotRegisteredForRemoteStorage(t *testing.T) {
|
|
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "s3"}})
|
|
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
|
for _, route := range engine.Routes() {
|
|
if strings.HasSuffix(route.Path, "/*filepath") && strings.HasPrefix(route.Path, "/uploads/file/") {
|
|
t.Fatalf("remote storage registered local route %s %s", route.Method, route.Path)
|
|
}
|
|
}
|
|
}
|
|
|
|
const expectedGinRouteContract = `DELETE /api/deleteApisByIds
|
|
DELETE /dataAccessLog/deleteDataAccessLogByIds
|
|
DELETE /department/deleteDepartment
|
|
DELETE /info/deleteInfo
|
|
DELETE /info/deleteInfoByIds
|
|
DELETE /mediaUpload/:uploadId
|
|
DELETE /position/deletePosition
|
|
DELETE /sysDictionary/deleteSysDictionary
|
|
DELETE /sysDictionaryDetail/deleteSysDictionaryDetail
|
|
DELETE /sysError/deleteSysError
|
|
DELETE /sysError/deleteSysErrorByIds
|
|
DELETE /sysExportTemplate/deleteSysExportTemplate
|
|
DELETE /sysExportTemplate/deleteSysExportTemplateByIds
|
|
DELETE /sysLoginLog/deleteLoginLog
|
|
DELETE /sysLoginLog/deleteLoginLogByIds
|
|
DELETE /sysOperationRecord/deleteSysOperationRecord
|
|
DELETE /sysOperationRecord/deleteSysOperationRecordByIds
|
|
DELETE /sysParams/deleteSysParams
|
|
DELETE /sysParams/deleteSysParamsByIds
|
|
DELETE /sysVersion/deleteSysVersion
|
|
DELETE /sysVersion/deleteSysVersionByIds
|
|
DELETE /timedTask/deleteTimedTask
|
|
DELETE /user/deleteUser
|
|
GET /api/freshCasbin
|
|
GET /api/getApiGroups
|
|
GET /api/getApiRoles
|
|
GET /api/syncApi
|
|
GET /attachmentCategory/getCategoryList
|
|
GET /authority/getDataScopeDepts
|
|
GET /authority/getUsersByAuthority
|
|
GET /department/findDepartment
|
|
GET /department/getDepartmentUsers
|
|
GET /fileUploadAndDownload/findFile
|
|
GET /health
|
|
GET /info/findInfo
|
|
GET /info/getInfoDataSource
|
|
GET /info/getInfoList
|
|
GET /info/getInfoPublic
|
|
GET /logViewer/content
|
|
GET /logViewer/dates
|
|
GET /logViewer/files
|
|
GET /menu/getMenuRoles
|
|
GET /payment/configs
|
|
GET /payment/orders
|
|
GET /position/findPosition
|
|
GET /position/getPositionUsers
|
|
GET /securityConfig/getSecurityConfig
|
|
GET /swagger/*any
|
|
GET /sysDictionary/exportSysDictionary
|
|
GET /sysDictionary/findSysDictionary
|
|
GET /sysDictionary/getSysDictionaryList
|
|
GET /sysDictionary/getSysDictionaryListWithDetails
|
|
GET /sysDictionaryDetail/findSysDictionaryDetail
|
|
GET /sysDictionaryDetail/getDictionaryDetailsByParent
|
|
GET /sysDictionaryDetail/getDictionaryPath
|
|
GET /sysDictionaryDetail/getDictionaryTreeList
|
|
GET /sysDictionaryDetail/getDictionaryTreeListByType
|
|
GET /sysDictionaryDetail/getSysDictionaryDetailList
|
|
GET /sysError/findSysError
|
|
GET /sysError/getSysErrorList
|
|
GET /sysExportTemplate/exportExcel
|
|
GET /sysExportTemplate/exportExcelByToken
|
|
GET /sysExportTemplate/exportTemplate
|
|
GET /sysExportTemplate/exportTemplateByToken
|
|
GET /sysExportTemplate/findSysExportTemplate
|
|
GET /sysExportTemplate/getSysExportTemplateList
|
|
GET /sysExportTemplate/previewSQL
|
|
GET /sysLoginLog/findLoginLog
|
|
GET /sysLoginLog/getLoginLogList
|
|
GET /sysOperationRecord/findSysOperationRecord
|
|
GET /sysOperationRecord/getSysOperationRecordList
|
|
GET /sysParams/findSysParams
|
|
GET /sysParams/getSysParam
|
|
GET /sysParams/getSysParamsList
|
|
GET /sysVersion/downloadVersionJson
|
|
GET /sysVersion/findSysVersion
|
|
GET /sysVersion/getSysVersionList
|
|
GET /timedTask/alertStream
|
|
GET /timedTask/getRegisteredMethods
|
|
GET /timedTask/getTimedTaskList
|
|
GET /timedTask/getTimedTaskLogList
|
|
GET /user/getUserInfo
|
|
POST /api/createApi
|
|
POST /api/deleteApi
|
|
POST /api/enterSyncApi
|
|
POST /api/getAllApis
|
|
POST /api/getApiById
|
|
POST /api/getApiList
|
|
POST /api/ignoreApi
|
|
POST /api/setApiRoles
|
|
POST /api/updateApi
|
|
POST /attachmentCategory/addCategory
|
|
POST /attachmentCategory/deleteCategory
|
|
POST /authority/copyAuthority
|
|
POST /authority/createAuthority
|
|
POST /authority/deleteAuthority
|
|
POST /authority/getAuthorityList
|
|
POST /authority/setDataScope
|
|
POST /authority/setRoleUsers
|
|
POST /authorityBtn/canRemoveAuthorityBtn
|
|
POST /authorityBtn/getAuthorityBtn
|
|
POST /authorityBtn/setAuthorityBtn
|
|
POST /base/captcha
|
|
POST /base/login
|
|
POST /casbin/getPolicyPathByAuthorityId
|
|
POST /casbin/updateCasbin
|
|
POST /dataAccessLog/getDataAccessLogList
|
|
POST /department/createDepartment
|
|
POST /department/getDepartmentList
|
|
POST /department/setDepartmentUsers
|
|
POST /email/emailTest
|
|
POST /email/sendEmail
|
|
POST /fileUploadAndDownload/deleteFile
|
|
POST /fileUploadAndDownload/deleteFiles
|
|
POST /fileUploadAndDownload/editFileName
|
|
POST /fileUploadAndDownload/getFileList
|
|
POST /fileUploadAndDownload/importURL
|
|
POST /fileUploadAndDownload/listOssFiles
|
|
POST /fileUploadAndDownload/upload
|
|
POST /info/createInfo
|
|
POST /init/checkdb
|
|
POST /init/initdb
|
|
POST /jwt/jsonInBlacklist
|
|
POST /mediaUpload/chunk
|
|
POST /mediaUpload/complete
|
|
POST /mediaUpload/init
|
|
POST /menu/addBaseMenu
|
|
POST /menu/addMenuAuthority
|
|
POST /menu/deleteBaseMenu
|
|
POST /menu/getBaseMenuById
|
|
POST /menu/getBaseMenuTree
|
|
POST /menu/getMenu
|
|
POST /menu/getMenuAuthority
|
|
POST /menu/getMenuList
|
|
POST /menu/setMenuRoles
|
|
POST /menu/updateBaseMenu
|
|
POST /payment/callback/:provider
|
|
POST /payment/config
|
|
POST /payment/create
|
|
POST /payment/order
|
|
POST /payment/query
|
|
POST /payment/refund
|
|
POST /position/createPosition
|
|
POST /position/getPositionList
|
|
POST /position/setPositionUsers
|
|
POST /securityConfig/setSecurityConfig
|
|
POST /sysApiToken/createApiToken
|
|
POST /sysApiToken/deleteApiToken
|
|
POST /sysApiToken/getApiTokenList
|
|
POST /sysDictionary/createSysDictionary
|
|
POST /sysDictionary/importSysDictionary
|
|
POST /sysDictionaryDetail/createSysDictionaryDetail
|
|
POST /sysError/createSysError
|
|
POST /sysExportTemplate/createSysExportTemplate
|
|
POST /sysExportTemplate/importExcel
|
|
POST /sysParams/createSysParams
|
|
POST /sysVersion/exportVersion
|
|
POST /sysVersion/importVersion
|
|
POST /system/getServerInfo
|
|
POST /system/getSystemConfig
|
|
POST /system/reloadSystem
|
|
POST /system/setSystemConfig
|
|
POST /timedTask/createTimedTask
|
|
POST /timedTask/toggleTimedTask
|
|
POST /timedTask/triggerTimedTask
|
|
POST /user/admin_register
|
|
POST /user/changePassword
|
|
POST /user/getUserList
|
|
POST /user/resetPassword
|
|
POST /user/setUserAuthorities
|
|
POST /user/setUserAuthority
|
|
POST /user/setUserDepartments
|
|
POST /user/setUserPositions
|
|
PUT /authority/updateAuthority
|
|
PUT /department/updateDepartment
|
|
PUT /info/updateInfo
|
|
PUT /position/updatePosition
|
|
PUT /sysDictionary/updateSysDictionary
|
|
PUT /sysDictionaryDetail/updateSysDictionaryDetail
|
|
PUT /sysError/updateSysError
|
|
PUT /sysExportTemplate/updateSysExportTemplate
|
|
PUT /sysParams/updateSysParams
|
|
PUT /timedTask/updateTimedTask
|
|
PUT /user/setSelfInfo
|
|
PUT /user/setSelfSetting
|
|
PUT /user/setUserInfo`
|