修复项目无法删除问题

This commit is contained in:
2026-08-13 20:42:09 +08:00
parent cb88a69012
commit a407e69ba7
18 changed files with 623 additions and 63 deletions

View File

@@ -96,16 +96,25 @@ func TestDeleteProjectPostgresIntegration(t *testing.T) {
}
unused := newProject("DEL"+strings.ToUpper(suffix), "unused deletion project")
history := newProject("HIS"+strings.ToUpper(suffix), "history deletion project")
history.Status = model.ProjectEnded
notEnded := newProject("RUN"+strings.ToUpper(suffix), "running history project")
notEnded.Status = model.ProjectRunning
if err := db.Create(&unused).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&history).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&notEnded).Error; err != nil {
t.Fatal(err)
}
grant := model.UserProject{UserID: admin.ID, ProjectID: unused.ID, CreatedAt: now}
if err := db.Create(&grant).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&model.UserProject{UserID: admin.ID, ProjectID: history.ID, CreatedAt: now}).Error; err != nil {
t.Fatal(err)
}
createdAudit := func(projectID string) model.AuditEntry {
return model.AuditEntry{
ID: uuid.NewString(), ProjectID: &projectID, ActorUserID: &admin.ID,
@@ -130,6 +139,12 @@ func TestDeleteProjectPostgresIntegration(t *testing.T) {
if err := db.Create(&queueSession).Error; err != nil {
t.Fatal(err)
}
notEndedSession := queueSession
notEndedSession.ID = uuid.NewString()
notEndedSession.ProjectID = notEnded.ID
if err := db.Create(&notEndedSession).Error; err != nil {
t.Fatal(err)
}
loginRecorder := httptest.NewRecorder()
loginRequest := httptest.NewRequest(http.MethodPost, "/api/admin/auth/login",
@@ -201,20 +216,25 @@ func TestDeleteProjectPostgresIntegration(t *testing.T) {
}
})
t.Run("project with queue history is rejected intact", func(t *testing.T) {
t.Run("ended project with queue history is archived intact", func(t *testing.T) {
response := deleteRequest(history.ID)
if response.Code != http.StatusConflict {
t.Fatalf("delete history project status = %d, want 409; body = %s", response.Code, response.Body.String())
if response.Code != http.StatusNoContent {
t.Fatalf("archive history project status = %d, want 204; body = %s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), `"code":"PROJECT_HAS_HISTORY"`) {
t.Fatalf("delete history project body = %s, want PROJECT_HAS_HISTORY", response.Body.String())
}
if !strings.Contains(response.Body.String(), "不能删除") || !strings.Contains(response.Body.String(), "结束") {
t.Fatalf("delete history project message is not actionable Chinese: %s", response.Body.String())
}
if err := db.First(&model.Project{}, "id = ?", history.ID).Error; err != nil {
var archived model.Project
if err := db.First(&archived, "id = ?", history.ID).Error; err != nil {
t.Fatalf("history project was not retained: %v", err)
}
if archived.ArchivedAt == nil || !archived.ArchivedAt.Equal(now) {
t.Fatalf("archived_at = %v, want %v", archived.ArchivedAt, now)
}
var archivedGrantCount int64
if err := db.Model(&model.UserProject{}).Where("project_id = ?", history.ID).Count(&archivedGrantCount).Error; err != nil {
t.Fatal(err)
}
if archivedGrantCount != 0 {
t.Fatalf("archived project grants = %d, want 0", archivedGrantCount)
}
if err := db.First(&model.QueueSession{}, "id = ?", queueSession.ID).Error; err != nil {
t.Fatalf("queue session was not retained: %v", err)
}
@@ -225,14 +245,59 @@ func TestDeleteProjectPostgresIntegration(t *testing.T) {
if retained.ProjectID == nil || *retained.ProjectID != history.ID {
t.Fatalf("history audit project_id = %v, want %s", retained.ProjectID, history.ID)
}
var deletedAuditCount int64
if err := db.Model(&model.AuditEntry{}).
Where("action = ? AND entity_id = ?", "PROJECT_DELETED", history.ID).
Count(&deletedAuditCount).Error; err != nil {
var archivedAudit model.AuditEntry
if err := db.Where("action = ? AND entity_id = ?", "PROJECT_ARCHIVED", history.ID).First(&archivedAudit).Error; err != nil {
t.Fatalf("load PROJECT_ARCHIVED audit: %v", err)
}
if archivedAudit.ProjectID == nil || *archivedAudit.ProjectID != history.ID {
t.Fatalf("PROJECT_ARCHIVED project_id = %v, want %s", archivedAudit.ProjectID, history.ID)
}
second := deleteRequest(history.ID)
if second.Code != http.StatusNotFound {
t.Fatalf("second archive status = %d, want 404; body = %s", second.Code, second.Body.String())
}
requestAdmin := func(method, path, body string) *httptest.ResponseRecorder {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(method, path, strings.NewReader(body))
request.AddCookie(adminCookie)
server.Handler().ServeHTTP(recorder, request)
return recorder
}
profile := requestAdmin(http.MethodPut, "/api/admin/projects/"+history.ID,
`{"name":"archived","code":"`+history.Code+`","timezone":"Asia/Shanghai","ticket_prefix":"A"}`)
if profile.Code != http.StatusNotFound {
t.Fatalf("update archived project status = %d, want 404; body = %s", profile.Code, profile.Body.String())
}
settings := requestAdmin(http.MethodPut, "/api/admin/projects/"+history.ID+"/settings", `{"status":"ENDED"}`)
if settings.Code != http.StatusNotFound {
t.Fatalf("update archived settings status = %d, want 404; body = %s", settings.Code, settings.Body.String())
}
createUser := requestAdmin(http.MethodPost, "/api/admin/users",
`{"username":"archived_grant_`+suffix+`","password":"Password123!","role":"STAFF","project_ids":["`+history.ID+`"]}`)
if createUser.Code != http.StatusUnprocessableEntity || !strings.Contains(createUser.Body.String(), `"code":"INVALID_PROJECT"`) {
t.Fatalf("grant archived project status = %d, body = %s; want 422 INVALID_PROJECT", createUser.Code, createUser.Body.String())
}
adminUsers := requestAdmin(http.MethodGet, "/api/admin/users", "")
if adminUsers.Code != http.StatusOK {
t.Fatalf("admin users status = %d, want 200; body = %s", adminUsers.Code, adminUsers.Body.String())
}
if strings.Contains(adminUsers.Body.String(), history.ID) {
t.Fatalf("admin users leaked archived project id: %s", adminUsers.Body.String())
}
})
t.Run("project with history must be ended before archival", func(t *testing.T) {
response := deleteRequest(notEnded.ID)
if response.Code != http.StatusConflict || !strings.Contains(response.Body.String(), `"code":"PROJECT_MUST_BE_ENDED"`) {
t.Fatalf("archive running project status = %d, body = %s; want 409 PROJECT_MUST_BE_ENDED", response.Code, response.Body.String())
}
var retained model.Project
if err := db.First(&retained, "id = ?", notEnded.ID).Error; err != nil {
t.Fatal(err)
}
if deletedAuditCount != 0 {
t.Fatalf("PROJECT_DELETED audit count for retained project = %d, want 0", deletedAuditCount)
if retained.ArchivedAt != nil {
t.Fatalf("running project archived_at = %v, want nil", retained.ArchivedAt)
}
})
}