初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"oci-portal/internal/crypto"
|
||||
"oci-portal/internal/model"
|
||||
"oci-portal/internal/oci"
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// nullClient 是 oci.Client 的空实现,路由测试不触发任何云调用。
|
||||
// 内嵌接口以自动满足 oci.Client,未覆写的方法不会被这些测试调用。
|
||||
type nullClient struct{ oci.Client }
|
||||
|
||||
func (nullClient) ValidateKey(context.Context, oci.Credentials) (oci.TenancyInfo, error) {
|
||||
return oci.TenancyInfo{}, nil
|
||||
}
|
||||
|
||||
func (nullClient) FetchAccountProfile(context.Context, oci.Credentials) (oci.AccountProfile, error) {
|
||||
return oci.AccountProfile{}, nil
|
||||
}
|
||||
|
||||
func (nullClient) ListRegionSubscriptions(context.Context, oci.Credentials) ([]oci.RegionSubscription, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nullClient) SubscribeRegion(context.Context, oci.Credentials, string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nullClient) ListLimits(context.Context, oci.Credentials, oci.LimitsQuery) ([]oci.LimitValue, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nullClient) ListLimitServices(context.Context, oci.Credentials, string) ([]oci.LimitService, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nullClient) ListSubscriptions(context.Context, oci.Credentials) ([]oci.SubscriptionInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nullClient) GetSubscription(context.Context, oci.Credentials, string) (oci.SubscriptionDetail, error) {
|
||||
return oci.SubscriptionDetail{}, nil
|
||||
}
|
||||
|
||||
func newTestRouter(t *testing.T) (*gin.Engine, *service.AuthService, *service.SystemLogService) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open in-memory sqlite: %v", err)
|
||||
}
|
||||
// :memory: 库每个连接彼此独立,异步日志写入会触发池内新连接而丢表,锁单连接
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("db handle: %v", err)
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
if err := db.AutoMigrate(&model.User{}, &model.OciConfig{}, &model.Task{}, &model.TaskLog{}, &model.Setting{}, &model.SystemLog{}, &model.LogEvent{}, &model.Proxy{}, &model.AiKey{}, &model.AiChannel{}, &model.AiModelCache{}, &model.AiCallLog{}); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
cipher, err := crypto.NewCipher("test-key")
|
||||
if err != nil {
|
||||
t.Fatalf("new cipher: %v", err)
|
||||
}
|
||||
auth := service.NewAuthService(db, "test-secret")
|
||||
if err := auth.EnsureAdmin("admin", "pass123"); err != nil {
|
||||
t.Fatalf("ensure admin: %v", err)
|
||||
}
|
||||
ociConfigs := service.NewOciConfigService(db, cipher, nullClient{})
|
||||
settings := service.NewSettingService(db, cipher)
|
||||
notifier := service.NewNotifier(settings)
|
||||
tasks := service.NewTaskService(db, ociConfigs, notifier, settings)
|
||||
systemLogs := service.NewSystemLogService(db)
|
||||
logEvents := service.NewLogEventService(db)
|
||||
oauth := service.NewOAuthService(db, settings, auth)
|
||||
r := NewRouter(auth, oauth, ociConfigs, tasks, service.NewConsoleService(ociConfigs), settings, notifier, systemLogs, logEvents, service.NewProxyService(db, cipher), service.NewAiGatewayService(db, ociConfigs, nullClient{}))
|
||||
return r, auth, systemLogs
|
||||
}
|
||||
|
||||
func doRequest(t *testing.T, r *gin.Engine, method, path, token, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestLoginEndpoint(t *testing.T) {
|
||||
r, _, _ := newTestRouter(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantStatus int
|
||||
}{
|
||||
{name: "正确凭据", body: `{"username":"admin","password":"pass123"}`, wantStatus: http.StatusOK},
|
||||
{name: "密码错误", body: `{"username":"admin","password":"wrong"}`, wantStatus: http.StatusUnauthorized},
|
||||
{name: "缺少字段", body: `{"username":"admin"}`, wantStatus: http.StatusBadRequest},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", tt.body)
|
||||
if w.Code != tt.wantStatus {
|
||||
t.Errorf("status = %d, want %d, body %s", w.Code, tt.wantStatus, w.Body.String())
|
||||
}
|
||||
if tt.wantStatus == http.StatusOK && !strings.Contains(w.Body.String(), "token") {
|
||||
t.Errorf("body = %s, want token field", w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecuredRoutesRequireToken(t *testing.T) {
|
||||
r, auth, _ := newTestRouter(t)
|
||||
token, _, err := auth.Login(context.Background(), "admin", "pass123", "127.0.0.1", "")
|
||||
if err != nil {
|
||||
t.Fatalf("login: %v", err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
wantStatus int
|
||||
}{
|
||||
{name: "无令牌", token: "", wantStatus: http.StatusUnauthorized},
|
||||
{name: "伪造令牌", token: "forged.token.value", wantStatus: http.StatusUnauthorized},
|
||||
{name: "有效令牌", token: token, wantStatus: http.StatusOK},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := doRequest(t, r, http.MethodGet, "/api/v1/oci-configs", tt.token, "")
|
||||
if w.Code != tt.wantStatus {
|
||||
t.Errorf("status = %d, want %d, body %s", w.Code, tt.wantStatus, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemLogsEndpoint(t *testing.T) {
|
||||
r, auth, logs := newTestRouter(t)
|
||||
token, _, err := auth.Login(context.Background(), "admin", "pass123", "127.0.0.1", "")
|
||||
if err != nil {
|
||||
t.Fatalf("login: %v", err)
|
||||
}
|
||||
// 经 HTTP 触发一次登录失败:埋点应异步落一条 401 记录
|
||||
doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", `{"username":"admin","password":"wrong"}`)
|
||||
logs.Wait()
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
wantTotal string
|
||||
}{
|
||||
{name: "无关键字返回全部", query: "", wantTotal: `"total":1`},
|
||||
{name: "关键字命中登录路径", query: "?keyword=login", wantTotal: `"total":1`},
|
||||
{name: "关键字不命中返回空", query: "?keyword=no-such", wantTotal: `"total":0`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := doRequest(t, r, http.MethodGet, "/api/v1/system-logs"+tt.query, token, "")
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d, body %s", w.Code, http.StatusOK, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, "items") || !strings.Contains(body, tt.wantTotal) {
|
||||
t.Errorf("body = %s, want items and %s", body, tt.wantTotal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user