[feature] migrate to monorepo
Some checks failed
Build Backend / Build Docker Image (push) Successful in 3m33s
Test Backend / test (push) Failing after 31s

This commit is contained in:
CDN 2025-02-21 00:49:20 +08:00
commit 05ddc1f783
Signed by: CDN
GPG key ID: 0C656827F9F80080
267 changed files with 75165 additions and 0 deletions

View file

@ -0,0 +1,24 @@
package server
import (
"context"
"tss-rocks-be/ent"
"github.com/rs/zerolog/log"
)
func InitDatabase(ctx context.Context, driver, dsn string) (*ent.Client, error) {
client, err := ent.Open(driver, dsn)
if err != nil {
log.Error().Err(err).Msg("failed opening database connection")
return nil, err
}
// Run the auto migration tool
if err := client.Schema.Create(ctx); err != nil {
log.Error().Err(err).Msg("failed creating schema resources")
return nil, err
}
return client, nil
}

View file

@ -0,0 +1,64 @@
package server
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInitDatabase(t *testing.T) {
tests := []struct {
name string
driver string
dsn string
wantErr bool
errContains string
}{
{
name: "success with sqlite3",
driver: "sqlite3",
dsn: "file:ent?mode=memory&cache=shared&_fk=1",
},
{
name: "invalid driver",
driver: "invalid_driver",
dsn: "file:ent?mode=memory",
wantErr: true,
errContains: "unsupported driver",
},
{
name: "invalid dsn",
driver: "sqlite3",
dsn: "file::memory:?not_exist_option=1", // 使用内存数据库但带有无效选项
wantErr: true,
errContains: "foreign_keys pragma is off",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
client, err := InitDatabase(ctx, tt.driver, tt.dsn)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
assert.Nil(t, client)
} else {
require.NoError(t, err)
assert.NotNil(t, client)
// 测试数据库连接是否正常工作
err = client.Schema.Create(ctx)
assert.NoError(t, err)
// 清理
client.Close()
}
})
}
}

View file

@ -0,0 +1,31 @@
package server
import (
"context"
"entgo.io/ent/dialect/sql"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log"
"tss-rocks-be/ent"
"tss-rocks-be/internal/config"
)
// NewEntClient creates a new ent client
func NewEntClient(cfg *config.Config) *ent.Client {
// TODO: Implement database connection based on config
// For now, we'll use SQLite for development
db, err := sql.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal().Err(err).Msg("Failed to connect to database")
}
// Create ent client
client := ent.NewClient(ent.Driver(db))
// Run the auto migration tool
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatal().Err(err).Msg("Failed to create schema resources")
}
return client
}

View file

@ -0,0 +1,40 @@
package server
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"tss-rocks-be/internal/config"
)
func TestNewEntClient(t *testing.T) {
tests := []struct {
name string
cfg *config.Config
}{
{
name: "default sqlite3 config",
cfg: &config.Config{
Database: config.DatabaseConfig{
Driver: "sqlite3",
DSN: "file:ent?mode=memory&cache=shared&_fk=1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := NewEntClient(tt.cfg)
assert.NotNil(t, client)
// 验证客户端是否可以正常工作
err := client.Schema.Create(context.Background())
assert.NoError(t, err)
// 清理
client.Close()
})
}
}

View file

@ -0,0 +1,90 @@
package server
import (
"context"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"tss-rocks-be/ent"
"tss-rocks-be/internal/config"
"tss-rocks-be/internal/handler"
"tss-rocks-be/internal/middleware"
"tss-rocks-be/internal/service"
"tss-rocks-be/internal/storage"
)
type Server struct {
config *config.Config
router *gin.Engine
handler *handler.Handler
server *http.Server
}
func New(cfg *config.Config, client *ent.Client) (*Server, error) {
// Initialize storage
store, err := storage.NewStorage(context.Background(), &cfg.Storage)
if err != nil {
return nil, fmt.Errorf("failed to initialize storage: %w", err)
}
// Initialize service
svc := service.NewService(client, store)
// Initialize RBAC
if err := svc.InitializeRBAC(context.Background()); err != nil {
return nil, fmt.Errorf("failed to initialize RBAC: %w", err)
}
// Initialize handler
h := handler.NewHandler(cfg, svc)
// Initialize router
router := gin.Default()
// Add CORS middleware if needed
router.Use(middleware.CORS())
// 添加全局中间件
router.Use(gin.Logger())
router.Use(gin.Recovery())
router.Use(middleware.RateLimit(&cfg.RateLimit))
// 添加访问日志中间件
accessLog, err := middleware.AccessLog(&cfg.AccessLog)
if err != nil {
return nil, fmt.Errorf("failed to initialize access log: %w", err)
}
router.Use(accessLog)
// 为上传路由添加文件验证中间件
router.POST("/api/v1/media/upload", middleware.ValidateUpload(&cfg.Storage.Upload))
// Register routes
h.RegisterRoutes(router)
return &Server{
config: cfg,
router: router,
handler: h,
}, nil
}
func (s *Server) Start() error {
addr := fmt.Sprintf("%s:%d", s.config.Server.Host, s.config.Server.Port)
s.server = &http.Server{
Addr: addr,
Handler: s.router,
}
log.Info().Msgf("Starting server on %s", addr)
return s.server.ListenAndServe()
}
func (s *Server) Shutdown(ctx context.Context) error {
if s.server != nil {
return s.server.Shutdown(ctx)
}
return nil
}

View file

@ -0,0 +1,220 @@
package server
import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tss-rocks-be/internal/config"
"tss-rocks-be/internal/types"
"tss-rocks-be/ent/enttest"
)
func TestNew(t *testing.T) {
// 创建测试配置
cfg := &config.Config{
Server: config.ServerConfig{
Host: "localhost",
Port: 8080,
},
Storage: config.StorageConfig{
Type: "local",
Local: config.LocalStorage{
RootDir: "testdata",
},
Upload: types.UploadConfig{
MaxSize: 10,
AllowedTypes: []string{"image/jpeg", "image/png"},
AllowedExtensions: []string{".jpg", ".png"},
},
},
RateLimit: types.RateLimitConfig{
IPRate: 100,
IPBurst: 200,
RouteRates: map[string]struct {
Rate int `yaml:"rate"`
Burst int `yaml:"burst"`
}{
"/api/v1/upload": {Rate: 10, Burst: 20},
},
},
AccessLog: types.AccessLogConfig{
EnableConsole: true,
EnableFile: true,
FilePath: "testdata/access.log",
Format: "json",
Level: "info",
Rotation: struct {
MaxSize int `yaml:"max_size"`
MaxAge int `yaml:"max_age"`
MaxBackups int `yaml:"max_backups"`
Compress bool `yaml:"compress"`
LocalTime bool `yaml:"local_time"`
}{
MaxSize: 100,
MaxAge: 7,
MaxBackups: 3,
Compress: true,
LocalTime: true,
},
},
}
// 创建测试数据库客户端
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
// 测试服务器初始化
s, err := New(cfg, client)
require.NoError(t, err)
assert.NotNil(t, s)
assert.NotNil(t, s.router)
assert.NotNil(t, s.handler)
assert.Equal(t, cfg, s.config)
}
func TestNew_StorageError(t *testing.T) {
// 创建一个无效的存储配置
cfg := &config.Config{
Storage: config.StorageConfig{
Type: "invalid_type", // 使用无效的存储类型
},
}
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
s, err := New(cfg, client)
assert.Error(t, err)
assert.Nil(t, s)
assert.Contains(t, err.Error(), "failed to initialize storage")
}
func TestServer_StartAndShutdown(t *testing.T) {
// 创建测试配置
cfg := &config.Config{
Server: config.ServerConfig{
Host: "localhost",
Port: 0, // 使用随机端口
},
Storage: config.StorageConfig{
Type: "local",
Local: config.LocalStorage{
RootDir: "testdata",
},
},
RateLimit: types.RateLimitConfig{
IPRate: 100,
IPBurst: 200,
},
AccessLog: types.AccessLogConfig{
EnableConsole: true,
Format: "json",
Level: "info",
},
}
// 创建测试数据库客户端
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
// 初始化服务器
s, err := New(cfg, client)
require.NoError(t, err)
// 创建一个通道来接收服务器错误
errChan := make(chan error, 1)
// 在 goroutine 中启动服务器
go func() {
err := s.Start()
if err != nil && err != http.ErrServerClosed {
errChan <- err
}
close(errChan)
}()
// 给服务器一些时间启动
time.Sleep(100 * time.Millisecond)
// 测试关闭服务器
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = s.Shutdown(ctx)
assert.NoError(t, err)
// 检查服务器是否有错误发生
err = <-errChan
assert.NoError(t, err)
}
func TestServer_StartError(t *testing.T) {
// 创建一个配置,使用已经被占用的端口来触发错误
cfg := &config.Config{
Server: config.ServerConfig{
Host: "localhost",
Port: 8899, // 使用固定端口以便测试
},
Storage: config.StorageConfig{
Type: "local",
Local: config.LocalStorage{
RootDir: "testdata",
},
},
}
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
// 创建第一个服务器实例
s1, err := New(cfg, client)
require.NoError(t, err)
// 创建一个通道来接收服务器错误
errChan := make(chan error, 1)
// 启动第一个服务器
go func() {
err := s1.Start()
if err != nil && err != http.ErrServerClosed {
errChan <- err
}
close(errChan)
}()
// 给服务器一些时间启动
time.Sleep(100 * time.Millisecond)
// 尝试在同一端口启动第二个服务器,应该会失败
s2, err := New(cfg, client)
require.NoError(t, err)
err = s2.Start()
assert.Error(t, err)
// 清理
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 关闭第一个服务器
err = s1.Shutdown(ctx)
assert.NoError(t, err)
// 检查第一个服务器是否有错误发生
err = <-errChan
assert.NoError(t, err)
// 关闭第二个服务器
err = s2.Shutdown(ctx)
assert.NoError(t, err)
}
func TestServer_ShutdownWithNilServer(t *testing.T) {
s := &Server{}
err := s.Shutdown(context.Background())
assert.NoError(t, err)
}