[feature] migrate to monorepo
This commit is contained in:
commit
05ddc1f783
267 changed files with 75165 additions and 0 deletions
24
backend/internal/server/database.go
Normal file
24
backend/internal/server/database.go
Normal 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
|
||||
}
|
64
backend/internal/server/database_test.go
Normal file
64
backend/internal/server/database_test.go
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
31
backend/internal/server/ent.go
Normal file
31
backend/internal/server/ent.go
Normal 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
|
||||
}
|
40
backend/internal/server/ent_test.go
Normal file
40
backend/internal/server/ent_test.go
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
90
backend/internal/server/server.go
Normal file
90
backend/internal/server/server.go
Normal 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
|
||||
}
|
220
backend/internal/server/server_test.go
Normal file
220
backend/internal/server/server_test.go
Normal 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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue