feat: add admin publishing workflow and yar theme
Add Go/Postgres admin APIs, Angular admin UI, manual build flow, asset uploads, markdown import/export, configurable slug generation, and the Yar reading theme. Exclude local docs and generated development artifacts from version control.
This commit is contained in:
parent
b78f4b39c9
commit
f0b50d13ea
121 changed files with 27139 additions and 550 deletions
125
backend/internal/postimport/import_test.go
Normal file
125
backend/internal/postimport/import_test.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package postimport
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeLegacyTime(t *testing.T) {
|
||||
got, err := normalizeLegacyTime("2026-01-13 01:25:27.486491+00")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "2026-01-13T01:25:27Z" {
|
||||
t.Fatalf("normalized time = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArticleToPostFallbackSlugAndBody(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
postsDir := filepath.Join(root, defaultPostsDir)
|
||||
if err := os.MkdirAll(postsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
post, skipped, err := articleToPost(root, postsDir, csvArticle{
|
||||
Title: "Hello World",
|
||||
BodyMD: "\nBody\n",
|
||||
Status: "published",
|
||||
CreatedAt: "2026-01-13 01:25:27.486491+00",
|
||||
UpdatedAt: "2026-01-13 01:25:27.486491+00",
|
||||
PublishedAt: "2026-01-13 01:25:27.486491+00",
|
||||
Type: "post",
|
||||
}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if skipped {
|
||||
t.Fatal("expected importable post")
|
||||
}
|
||||
if post.Frontmatter.Slug != "hello-world" {
|
||||
t.Fatalf("slug = %q", post.Frontmatter.Slug)
|
||||
}
|
||||
if post.Body != "Body\n" {
|
||||
t.Fatalf("body = %q", post.Body)
|
||||
}
|
||||
if post.Frontmatter.PublishedAt == nil || *post.Frontmatter.PublishedAt != "2026-01-13T01:25:27Z" {
|
||||
t.Fatalf("published_at = %#v", post.Frontmatter.PublishedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArticleToPostSkipsExistingWithoutOverwrite(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
postsDir := filepath.Join(root, defaultPostsDir)
|
||||
if err := os.MkdirAll(postsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(postsDir, "smoking.md"), []byte("existing"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, skipped, err := articleToPost(root, postsDir, csvArticle{
|
||||
Slug: "smoking",
|
||||
Title: "抽烟",
|
||||
BodyMD: "Body",
|
||||
Status: "published",
|
||||
CreatedAt: "2026-01-13 01:25:27.486491+00",
|
||||
UpdatedAt: "2026-01-13 01:25:27.486491+00",
|
||||
Type: "post",
|
||||
}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !skipped {
|
||||
t.Fatal("expected existing file to be skipped")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportWritesMarkdown(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "backend", "cmd", "osaetctl"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, "frontend", "site"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "frontend", "site", "package.json"), []byte("{}"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
csvPath := filepath.Join(root, "articles.csv")
|
||||
csvContent := strings.Join([]string{
|
||||
"id,slug,title,body_md,body_html,status,archive_id,author_id,published_at,created_at,updated_at,type",
|
||||
"post-1,test-post,Test Post,\"Line 1\n\nLine 2\",,published,,,2026-01-13 01:25:27.486491+00,2026-01-13 01:25:27.486491+00,2026-01-13 01:25:27.486491+00,post",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(csvPath, []byte(csvContent), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := Import(Options{CSVPath: csvPath, PostsDir: defaultPostsDir, WorkingDir: root})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Imported != 1 || result.SkippedExisting != 0 || result.SkippedNonPost != 0 {
|
||||
t.Fatalf("result = %#v", result)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(root, defaultPostsDir, "test-post.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := string(data)
|
||||
for _, want := range []string{
|
||||
"slug: test-post",
|
||||
"title: Test Post",
|
||||
"status: published",
|
||||
"published_at: \"2026-01-13T01:25:27Z\"",
|
||||
"Line 1",
|
||||
"Line 2",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("expected output to contain %q\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue