refactor(api): expose project identity DTOs
This commit is contained in:
42
backend/internal/logic/projects/dto.go
Normal file
42
backend/internal/logic/projects/dto.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package projects
|
||||
|
||||
import "senlinai-agent/backend/internal/models"
|
||||
|
||||
// ProjectDTO 是项目 API 的稳定响应;ID 使用公开 identity,避免泄漏数据库自增主键。
|
||||
type ProjectDTO struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Identifier string `json:"identifier"`
|
||||
Icon string `json:"icon"`
|
||||
Background string `json:"background"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// CreateProjectRequest 描述创建项目时允许客户端写入的字段。
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name"`
|
||||
Identifier string `json:"identifier"`
|
||||
Icon string `json:"icon"`
|
||||
Background string `json:"background"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// UpdateProjectRequest 使用指针区分“未提供”和“显式清空”,且只开放项目设置字段。
|
||||
type UpdateProjectRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Identifier *string `json:"identifier"`
|
||||
Icon *string `json:"icon"`
|
||||
Background *string `json:"background"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
func projectDTO(project models.SenlinAgentProject) ProjectDTO {
|
||||
return ProjectDTO{
|
||||
ID: project.Identity,
|
||||
Name: project.Name,
|
||||
Identifier: project.Identifier,
|
||||
Icon: project.Icon,
|
||||
Background: project.Background,
|
||||
Description: project.Description,
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"senlinai-agent/backend/internal/httpx"
|
||||
"senlinai-agent/backend/internal/logic/auth"
|
||||
"senlinai-agent/backend/internal/logic/files"
|
||||
)
|
||||
@@ -27,6 +30,8 @@ func NewHandler(service *Service, fileServices ...*files.Service) *Handler {
|
||||
func (h *Handler) Register(router gin.IRouter) {
|
||||
router.POST("/projects", h.createProject)
|
||||
router.GET("/projects", h.listProjects)
|
||||
router.GET("/projects/:id", h.getProject)
|
||||
router.PATCH("/projects/:id", h.updateProject)
|
||||
router.GET("/projects/:id/dashboard", h.dashboard)
|
||||
router.GET("/projects/:id/workspace", h.workspace)
|
||||
router.GET("/projects/:id/tags", h.listTags)
|
||||
@@ -40,48 +45,76 @@ func (h *Handler) Register(router gin.IRouter) {
|
||||
func (h *Handler) createProject(c *gin.Context) {
|
||||
userID, ok := auth.CurrentUserID(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效")
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
Name string `json:"name"`
|
||||
Identifier string `json:"identifier"`
|
||||
Icon string `json:"icon"`
|
||||
Background string `json:"background"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
var input CreateProjectRequest
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
||||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
|
||||
return
|
||||
}
|
||||
project, err := h.service.CreateProjectWithInput(userID, CreateProjectInput{
|
||||
Name: input.Name,
|
||||
Identifier: input.Identifier,
|
||||
Icon: input.Icon,
|
||||
Background: input.Background,
|
||||
Description: input.Description,
|
||||
})
|
||||
project, err := h.service.CreateProjectWithInput(userID, input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
writeProjectError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, project)
|
||||
c.JSON(http.StatusCreated, projectDTO(*project))
|
||||
}
|
||||
|
||||
func (h *Handler) listProjects(c *gin.Context) {
|
||||
userID, ok := auth.CurrentUserID(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效")
|
||||
return
|
||||
}
|
||||
projects, err := h.service.ListProjects(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list projects"})
|
||||
httpx.Error(c, http.StatusInternalServerError, "internal_error", "项目列表加载失败")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, projects)
|
||||
}
|
||||
|
||||
func (h *Handler) getProject(c *gin.Context) {
|
||||
userID, ok := auth.CurrentUserID(c)
|
||||
if !ok {
|
||||
httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效")
|
||||
return
|
||||
}
|
||||
identity, ok := httpx.IdentityParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
project, err := h.service.GetProject(userID, identity)
|
||||
if err != nil {
|
||||
writeProjectError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, project)
|
||||
}
|
||||
|
||||
func (h *Handler) updateProject(c *gin.Context) {
|
||||
userID, ok := auth.CurrentUserID(c)
|
||||
if !ok {
|
||||
httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效")
|
||||
return
|
||||
}
|
||||
identity, ok := httpx.IdentityParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input UpdateProjectRequest
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateProject(userID, identity, input); err != nil {
|
||||
writeProjectError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) dashboard(c *gin.Context) {
|
||||
userID, ok := auth.CurrentUserID(c)
|
||||
if !ok {
|
||||
@@ -340,6 +373,19 @@ func parseProjectID(c *gin.Context) (uint, bool) {
|
||||
return uint(projectID), true
|
||||
}
|
||||
|
||||
func writeProjectError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
httpx.Error(c, http.StatusNotFound, "not_found", "项目不存在")
|
||||
case errors.Is(err, ErrProjectIdentifierConflict):
|
||||
httpx.Error(c, http.StatusConflict, "conflict", "项目标识已存在")
|
||||
case errors.Is(err, ErrProjectNameRequired), errors.Is(err, ErrProjectIdentifierRequired):
|
||||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
|
||||
default:
|
||||
httpx.Error(c, http.StatusInternalServerError, "internal_error", "项目操作失败")
|
||||
}
|
||||
}
|
||||
|
||||
func parseTaskID(c *gin.Context) (uint, bool) {
|
||||
taskID, err := strconv.ParseUint(c.Param("taskID"), 10, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -41,6 +41,146 @@ func TestCreateProjectHandlerPersistsMetadata(t *testing.T) {
|
||||
require.Equal(t, "compass", project.Icon)
|
||||
require.Equal(t, "#165DFF", project.Background)
|
||||
require.Equal(t, "RSS exploration workspace", project.Description)
|
||||
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, project.Identity, payload["id"])
|
||||
require.ElementsMatch(t, []string{"id", "name", "identifier", "icon", "background", "description"}, mapKeys(payload))
|
||||
}
|
||||
|
||||
func TestCreateProjectReturnsChineseInvalidRequestEnvelope(t *testing.T) {
|
||||
router, _, _ := newProjectsHandlerTestRouter(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader([]byte(`{"name":" "}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
var payload httpx.ErrorEnvelope
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, "invalid_request", payload.Error.Code)
|
||||
require.Equal(t, "请求参数无效", payload.Error.Message)
|
||||
}
|
||||
|
||||
func TestCreateProjectReturnsConflictForDuplicateIdentifier(t *testing.T) {
|
||||
router, project, _ := newProjectsHandlerTestRouter(t)
|
||||
body, err := json.Marshal(gin.H{"name": "Another", "identifier": project.Identifier})
|
||||
require.NoError(t, err)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusConflict, rec.Code)
|
||||
var payload httpx.ErrorEnvelope
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, "conflict", payload.Error.Code)
|
||||
require.Equal(t, "项目标识已存在", payload.Error.Message)
|
||||
}
|
||||
|
||||
func TestGetProjectUsesOwnedIdentityDTO(t *testing.T) {
|
||||
router, project, _ := newProjectsHandlerTestRouter(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+project.Identity, nil)
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, project.Identity, payload["id"])
|
||||
require.ElementsMatch(t, []string{"id", "name", "identifier", "icon", "background", "description"}, mapKeys(payload))
|
||||
}
|
||||
|
||||
func TestListProjectsUsesIdentityDTO(t *testing.T) {
|
||||
router, project, _ := newProjectsHandlerTestRouter(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/projects", nil)
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
var payload []map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Len(t, payload, 1)
|
||||
require.Equal(t, project.Identity, payload[0]["id"])
|
||||
require.ElementsMatch(t, []string{"id", "name", "identifier", "icon", "background", "description"}, mapKeys(payload[0]))
|
||||
}
|
||||
|
||||
func TestUpdateProject(t *testing.T) {
|
||||
t.Run("updates the allowed project fields by identity", func(t *testing.T) {
|
||||
router, project, _ := newProjectsHandlerTestRouter(t)
|
||||
body, err := json.Marshal(gin.H{
|
||||
"name": "Alpha Next",
|
||||
"identifier": "ALPHA-NEXT",
|
||||
"icon": "tree",
|
||||
"background": "#0FC6C2",
|
||||
"description": "更新后的项目说明",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/projects/"+project.Identity, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusNoContent, rec.Code)
|
||||
require.Empty(t, rec.Body.String())
|
||||
var updated models.SenlinAgentProject
|
||||
require.NoError(t, models.DBService.First(&updated, project.ID).Error)
|
||||
require.Equal(t, "Alpha Next", updated.Name)
|
||||
require.Equal(t, "ALPHA-NEXT", updated.Identifier)
|
||||
require.Equal(t, "tree", updated.Icon)
|
||||
require.Equal(t, "#0FC6C2", updated.Background)
|
||||
require.Equal(t, "更新后的项目说明", updated.Description)
|
||||
})
|
||||
|
||||
t.Run("returns not found when the identity is not owned by the current user", func(t *testing.T) {
|
||||
router, _, _ := newProjectsHandlerTestRouter(t)
|
||||
other, err := NewService().CreateProject(2, "Other", "")
|
||||
require.NoError(t, err)
|
||||
body, err := json.Marshal(gin.H{"name": "不可见项目"})
|
||||
require.NoError(t, err)
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/projects/"+other.Identity, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, rec.Code)
|
||||
var payload httpx.ErrorEnvelope
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, "not_found", payload.Error.Code)
|
||||
require.Equal(t, "项目不存在", payload.Error.Message)
|
||||
})
|
||||
|
||||
t.Run("returns conflict for another owned project identifier", func(t *testing.T) {
|
||||
router, project, _ := newProjectsHandlerTestRouter(t)
|
||||
_, err := NewService().CreateProjectWithInput(1, CreateProjectRequest{Name: "Beta", Identifier: "BETA"})
|
||||
require.NoError(t, err)
|
||||
body, err := json.Marshal(gin.H{"identifier": "BETA"})
|
||||
require.NoError(t, err)
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/projects/"+project.Identity, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusConflict, rec.Code)
|
||||
var payload httpx.ErrorEnvelope
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, "conflict", payload.Error.Code)
|
||||
require.Equal(t, "项目标识已存在", payload.Error.Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateTaskHandlerPersistsTask(t *testing.T) {
|
||||
@@ -175,3 +315,11 @@ func newProjectsHandlerTestRouter(t *testing.T) (*gin.Engine, *models.SenlinAgen
|
||||
)
|
||||
return router, project, storageDir
|
||||
}
|
||||
|
||||
func mapKeys(value map[string]any) []string {
|
||||
keys := make([]string, 0, len(value))
|
||||
for key := range value {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
20
backend/internal/logic/projects/ownership.go
Normal file
20
backend/internal/logic/projects/ownership.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"senlinai-agent/backend/internal/models"
|
||||
)
|
||||
|
||||
// FindOwnedProject 同时按 owner_id 与公开 identity 查询,防止仅凭可猜测标识越权访问项目。
|
||||
// 内部自增 ID 只在通过所有权校验后供关联查询使用,不进入 API 契约。
|
||||
func FindOwnedProject(userID uint, identity string) (*models.SenlinAgentProject, error) {
|
||||
var project models.SenlinAgentProject
|
||||
result := models.DBService.Where("owner_id = ? AND identity = ?", userID, identity).Limit(1).Find(&project)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return &project, nil
|
||||
}
|
||||
@@ -105,14 +105,6 @@ type ProjectWorkspace struct {
|
||||
CronPlans []CronPlan `json:"cronPlans"`
|
||||
}
|
||||
|
||||
type CreateProjectInput struct {
|
||||
Name string
|
||||
Identifier string
|
||||
Icon string
|
||||
Background string
|
||||
Description string
|
||||
}
|
||||
|
||||
type CreateTaskInput struct {
|
||||
Title string
|
||||
Description string
|
||||
@@ -147,18 +139,27 @@ func NewService() *Service {
|
||||
}
|
||||
|
||||
func (s *Service) CreateProject(ownerID uint, name string, description string) (*models.SenlinAgentProject, error) {
|
||||
return s.CreateProjectWithInput(ownerID, CreateProjectInput{Name: name, Description: description})
|
||||
return s.CreateProjectWithInput(ownerID, CreateProjectRequest{Name: name, Description: description})
|
||||
}
|
||||
|
||||
func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectInput) (*models.SenlinAgentProject, error) {
|
||||
func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectRequest) (*models.SenlinAgentProject, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
if name == "" {
|
||||
return nil, errors.New("project name is required")
|
||||
return nil, ErrProjectNameRequired
|
||||
}
|
||||
identifier := strings.TrimSpace(input.Identifier)
|
||||
if identifier == "" {
|
||||
identifier = projectInitials(name)
|
||||
}
|
||||
var count int64
|
||||
if err := models.DBService.Model(&models.SenlinAgentProject{}).
|
||||
Where("owner_id = ? AND identifier = ?", ownerID, identifier).
|
||||
Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, ErrProjectIdentifierConflict
|
||||
}
|
||||
project := &models.SenlinAgentProject{
|
||||
OwnerID: ownerID,
|
||||
Name: name,
|
||||
@@ -170,10 +171,78 @@ func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectInput)
|
||||
return project, models.DBService.Create(project).Error
|
||||
}
|
||||
|
||||
func (s *Service) ListProjects(ownerID uint) ([]models.SenlinAgentProject, error) {
|
||||
// ListProjects 只返回当前所有者的公开 DTO,不让 handler 接触或序列化数据库模型。
|
||||
func (s *Service) ListProjects(ownerID uint) ([]ProjectDTO, error) {
|
||||
var projects []models.SenlinAgentProject
|
||||
err := models.DBService.Where("owner_id = ?", ownerID).Order("updated_at desc").Find(&projects).Error
|
||||
return projects, err
|
||||
if err := models.DBService.Where("owner_id = ?", ownerID).Order("updated_at desc").Find(&projects).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]ProjectDTO, 0, len(projects))
|
||||
for _, project := range projects {
|
||||
result = append(result, projectDTO(project))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetProject 使用公开 identity 和所有权边界读取单个项目。
|
||||
func (s *Service) GetProject(ownerID uint, identity string) (ProjectDTO, error) {
|
||||
project, err := FindOwnedProject(ownerID, identity)
|
||||
if err != nil {
|
||||
return ProjectDTO{}, err
|
||||
}
|
||||
return projectDTO(*project), nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrProjectNameRequired = errors.New("project name is required")
|
||||
ErrProjectIdentifierRequired = errors.New("project identifier is required")
|
||||
ErrProjectIdentifierConflict = errors.New("project identifier already exists")
|
||||
)
|
||||
|
||||
// UpdateProject 仅更新项目设置白名单字段,查询和冲突检查均限定在当前所有者内。
|
||||
func (s *Service) UpdateProject(ownerID uint, identity string, input UpdateProjectRequest) error {
|
||||
project, err := FindOwnedProject(ownerID, identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updates := make(map[string]any, 5)
|
||||
if input.Name != nil {
|
||||
name := strings.TrimSpace(*input.Name)
|
||||
if name == "" {
|
||||
return ErrProjectNameRequired
|
||||
}
|
||||
updates["name"] = name
|
||||
}
|
||||
if input.Identifier != nil {
|
||||
identifier := strings.TrimSpace(*input.Identifier)
|
||||
if identifier == "" {
|
||||
return ErrProjectIdentifierRequired
|
||||
}
|
||||
var count int64
|
||||
if err := models.DBService.Model(&models.SenlinAgentProject{}).
|
||||
Where("owner_id = ? AND identifier = ? AND id <> ?", ownerID, identifier, project.ID).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return ErrProjectIdentifierConflict
|
||||
}
|
||||
updates["identifier"] = identifier
|
||||
}
|
||||
if input.Icon != nil {
|
||||
updates["icon"] = strings.TrimSpace(*input.Icon)
|
||||
}
|
||||
if input.Background != nil {
|
||||
updates["background"] = strings.TrimSpace(*input.Background)
|
||||
}
|
||||
if input.Description != nil {
|
||||
updates["description"] = strings.TrimSpace(*input.Description)
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
return models.DBService.Model(project).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (s *Service) CreateTask(ownerID uint, projectID uint, input CreateTaskInput) (*models.SenlinAgentTask, error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -11,6 +12,23 @@ import (
|
||||
"senlinai-agent/backend/internal/models"
|
||||
)
|
||||
|
||||
func TestFindOwnedProjectScopesIdentityByOwner(t *testing.T) {
|
||||
newTestDB(t)
|
||||
service := NewService()
|
||||
owned, err := service.CreateProject(1, "Alpha", "")
|
||||
require.NoError(t, err)
|
||||
other, err := service.CreateProject(2, "Beta", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := FindOwnedProject(1, owned.Identity)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, owned.ID, found.ID)
|
||||
|
||||
_, err = FindOwnedProject(1, other.Identity)
|
||||
require.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
||||
}
|
||||
|
||||
func TestProjectTagsAreScopedToProject(t *testing.T) {
|
||||
newTestDB(t)
|
||||
service := NewService()
|
||||
@@ -34,7 +52,7 @@ func TestCreateProjectPersistsWorkspaceMetadata(t *testing.T) {
|
||||
newTestDB(t)
|
||||
service := NewService()
|
||||
|
||||
project, err := service.CreateProjectWithInput(1, CreateProjectInput{
|
||||
project, err := service.CreateProjectWithInput(1, CreateProjectRequest{
|
||||
Name: "探索项目",
|
||||
Identifier: "EXP",
|
||||
Icon: "compass",
|
||||
|
||||
Reference in New Issue
Block a user