作品展示页:前端展示 + 后台增删改查
- 新增 projects 表与迁移(唯一 slug 索引) - 后端:公开 GET /api/projects,后台 GET/POST/PUT/DELETE /api/admin/projects - 前端:/projects 双虚线边框卡片页(参考 diygod.cc/projects),后台管理页 - 左栏/手机顶栏/后台侧栏均加「作品」入口 - 补充项目 CRUD 单元测试与后台鉴权拦截测试
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -291,3 +292,86 @@ func TestListWithMaliciousOrderByFailsSafe(t *testing.T) {
|
||||
t.Errorf("unexpected total %d", page.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectCRUD(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
|
||||
// empty list is a non-nil slice
|
||||
list, err := s.ListProjects("")
|
||||
if err != nil {
|
||||
t.Fatalf("ListProjects: %v", err)
|
||||
}
|
||||
if list == nil {
|
||||
t.Fatal("ListProjects returned nil slice")
|
||||
}
|
||||
|
||||
created, err := s.CreateProject(model.ProjectInput{
|
||||
Title: "Folo",
|
||||
Summary: "This AI RSS reader reads the internet for you",
|
||||
CoverURL: "https://folo.is/cover.webp",
|
||||
URL: "https://folo.is",
|
||||
RepoURL: "https://github.com/DIYgod/RSSHub-Radar",
|
||||
Status: model.StatusPublished,
|
||||
Position: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProject: %v", err)
|
||||
}
|
||||
if created.ID == 0 {
|
||||
t.Fatal("created project has no ID")
|
||||
}
|
||||
if created.Slug != "folo" {
|
||||
t.Errorf("slug = %q, want folo", created.Slug)
|
||||
}
|
||||
|
||||
// duplicate slug gets disambiguated
|
||||
dup, err := s.CreateProject(model.ProjectInput{Title: "Folo", Status: model.StatusPublished})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProject dup: %v", err)
|
||||
}
|
||||
if dup.Slug == created.Slug {
|
||||
t.Errorf("duplicate slug not disambiguated: %q", dup.Slug)
|
||||
}
|
||||
|
||||
// public list excludes drafts; admin ("") includes them
|
||||
draft, err := s.CreateProject(model.ProjectInput{Title: "Secret", Status: model.StatusDraft})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProject draft: %v", err)
|
||||
}
|
||||
pub, _ := s.ListProjects(model.StatusPublished)
|
||||
if len(pub) != 2 {
|
||||
t.Errorf("published list len = %d, want 2", len(pub))
|
||||
}
|
||||
all, _ := s.ListProjects("")
|
||||
if len(all) != 3 {
|
||||
t.Errorf("all list len = %d, want 3", len(all))
|
||||
}
|
||||
|
||||
// update
|
||||
upd, err := s.UpdateProject(created.ID, model.ProjectInput{Summary: "Updated summary", Position: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateProject: %v", err)
|
||||
}
|
||||
if upd.Summary != "Updated summary" || upd.Position != 1 {
|
||||
t.Errorf("update did not apply: %+v", upd)
|
||||
}
|
||||
|
||||
// get
|
||||
got, err := s.GetProject(draft.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetProject: %v", err)
|
||||
}
|
||||
if got.Status != model.StatusDraft {
|
||||
t.Errorf("status = %q, want draft", got.Status)
|
||||
}
|
||||
|
||||
// delete
|
||||
if err := s.DeleteProject(created.ID); err != nil {
|
||||
t.Fatalf("DeleteProject: %v", err)
|
||||
}
|
||||
_, err = s.GetProject(created.ID)
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("after delete: got %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user