package projects import ( "bytes" "encoding/json" "mime/multipart" "net/http" "net/http/httptest" "path/filepath" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "senlinai-agent/backend/internal/config" "senlinai-agent/backend/internal/httpx" "senlinai-agent/backend/internal/logic/files" "senlinai-agent/backend/internal/models" ) func TestCreateProjectHandlerPersistsMetadata(t *testing.T) { router, _, _ := newProjectsHandlerTestRouter(t) body, err := json.Marshal(gin.H{ "name": "Explore", "identifier": "EXP", "icon": "compass", "background": "#165DFF", "description": "RSS exploration workspace", }) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/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.StatusCreated, rec.Code) var project models.SenlinAgentProject require.NoError(t, models.DBService.Where("owner_id = ? AND identifier = ?", 1, "EXP").First(&project).Error) require.Equal(t, "Explore", project.Name) require.Equal(t, "compass", project.Icon) require.Equal(t, "#165DFF", project.Background) require.Equal(t, "RSS exploration workspace", project.Description) } func TestCreateTaskHandlerPersistsTask(t *testing.T) { router, project, _ := newProjectsHandlerTestRouter(t) body, err := json.Marshal(gin.H{"title": "整理需求", "description": "形成任务清单", "dueAt": "2026-07-21T09:30:00Z", "tag": "需求"}) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/projects/1/tasks", 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.StatusCreated, rec.Code) var task models.SenlinAgentTask require.NoError(t, models.DBService.Where("project_id = ? AND title = ?", project.ID, "整理需求").First(&task).Error) require.Equal(t, "open", task.Status) require.NotNil(t, task.DueAt) require.NotNil(t, task.TagID) } func TestUploadSourceHandlerStoresFileAndSource(t *testing.T) { router, project, storageDir := newProjectsHandlerTestRouter(t) body := &bytes.Buffer{} writer := multipart.NewWriter(body) require.NoError(t, writer.WriteField("title", "客户资料.txt")) fileWriter, err := writer.CreateFormFile("file", "客户资料.txt") require.NoError(t, err) _, err = fileWriter.Write([]byte("hello")) require.NoError(t, err) require.NoError(t, writer.Close()) req := httptest.NewRequest(http.MethodPost, "/api/projects/1/sources", body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer test-token") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) require.Equal(t, http.StatusCreated, rec.Code) var source models.SenlinAgentSource require.NoError(t, models.DBService.Where("project_id = ? AND kind = ?", project.ID, "file").First(&source).Error) require.Equal(t, "客户资料.txt", source.Title) require.NotEmpty(t, source.FilePath) require.FileExists(t, filepath.Join(storageDir, source.FilePath)) } func TestCreateCronPlanHandlerPersistsPlan(t *testing.T) { router, project, _ := newProjectsHandlerTestRouter(t) body, err := json.Marshal(gin.H{"title": "每日整理", "schedule": "0 9 * * *", "enabled": true, "nextRunAt": "2026-07-22T08:00:00Z"}) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/projects/1/cron-plans", 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.StatusCreated, rec.Code) var plan models.SenlinAgentCronPlan require.NoError(t, models.DBService.Where("project_id = ? AND title = ?", project.ID, "每日整理").First(&plan).Error) require.Equal(t, "0 9 * * *", plan.Schedule) require.True(t, plan.Enabled) require.NotNil(t, plan.NextRunAt) } func TestCreateTagHandlerPersistsProjectTag(t *testing.T) { router, project, _ := newProjectsHandlerTestRouter(t) body, err := json.Marshal(gin.H{"name": "Design"}) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/projects/1/tags", 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.StatusCreated, rec.Code) var tag models.SenlinAgentTag require.NoError(t, models.DBService.Where("project_id = ? AND name = ?", project.ID, "Design").First(&tag).Error) require.Equal(t, project.ID, tag.ProjectID) } func TestListTagsHandlerReturnsProjectTags(t *testing.T) { router, project, _ := newProjectsHandlerTestRouter(t) require.NoError(t, models.DBService.Create(&models.SenlinAgentTag{ProjectID: project.ID, Name: "Design"}).Error) req := httptest.NewRequest(http.MethodGet, "/api/projects/1/tags", nil) req.Header.Set("Authorization", "Bearer test-token") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) require.Equal(t, http.StatusOK, rec.Code) var tags []models.SenlinAgentTag require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &tags)) require.Len(t, tags, 1) require.Equal(t, "Design", tags[0].Name) } func TestUpdateTaskHandlerPersistsTagAndStatus(t *testing.T) { router, project, _ := newProjectsHandlerTestRouter(t) task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: 1, Title: "Draft", Status: "open"} require.NoError(t, models.DBService.Create(&task).Error) body, err := json.Marshal(gin.H{"title": "Draft v2", "description": "Updated", "completed": true, "tag": "Important"}) require.NoError(t, err) req := httptest.NewRequest(http.MethodPatch, "/api/projects/1/tasks/1", 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.StatusOK, rec.Code) var updated models.SenlinAgentTask require.NoError(t, models.DBService.First(&updated, task.ID).Error) require.Equal(t, "Draft v2", updated.Title) require.Equal(t, "done", updated.Status) require.NotNil(t, updated.TagID) } func newProjectsHandlerTestRouter(t *testing.T) (*gin.Engine, *models.SenlinAgentProject, string) { t.Helper() newTestDB(t) require.NoError(t, models.DBService.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error) service := NewService() project, err := service.CreateProject(1, "Alpha", "") require.NoError(t, err) storageDir := t.TempDir() router := httpx.NewProtectedRouter( config.Config{Env: "test"}, func(token string) (uint, error) { return 1, nil }, NewHandler(service, files.NewService(storageDir)), ) return router, project, storageDir }