From 598ffc6423136f093c142ec0c326eca05dd7b9c2 Mon Sep 17 00:00:00 2001 From: Gus Date: Fri, 1 Aug 2025 16:47:32 +0800 Subject: [PATCH 1/7] test: update isValidTable tests --- database/model_internal_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/database/model_internal_test.go b/database/model_internal_test.go index 728cb714..0bb726b6 100644 --- a/database/model_internal_test.go +++ b/database/model_internal_test.go @@ -6,15 +6,18 @@ import ( ) func TestIsValidTable(t *testing.T) { - if !isValidTable("users") { - t.Fatalf("expected users table to be valid") + for _, name := range GetSchemaTables() { + if !isValidTable(name) { + t.Fatalf("expected %s table to be valid", name) + } } + if isValidTable("unknown") { t.Fatalf("unexpected valid table") } } -func TestIsValidTableEdgeCases(t *testing.T) { +func TestIsValidTableNonexistentTables(t *testing.T) { invalid := []string{ "", "user!@#", From f1bd0c9c3fca4b54fb6f85ee5972107611e25aec Mon Sep 17 00:00:00 2001 From: Gus Date: Fri, 1 Aug 2025 17:06:10 +0800 Subject: [PATCH 2/7] Add CLI tests with SQLite --- cli/accounts/factory_test.go | 26 +++++++++ cli/accounts/handler_test.go | 37 +++++++++++++ cli/clitest/helpers.go | 35 ++++++++++++ cli/panel/menu_test.go | 95 ++++++++++++++++++++++++++++++++ cli/posts/handler_test.go | 101 +++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 4 ++ 7 files changed, 300 insertions(+) create mode 100644 cli/accounts/factory_test.go create mode 100644 cli/accounts/handler_test.go create mode 100644 cli/clitest/helpers.go create mode 100644 cli/panel/menu_test.go create mode 100644 cli/posts/handler_test.go diff --git a/cli/accounts/factory_test.go b/cli/accounts/factory_test.go new file mode 100644 index 00000000..ad953940 --- /dev/null +++ b/cli/accounts/factory_test.go @@ -0,0 +1,26 @@ +package accounts + +import ( + clitest "github.com/oullin/cli/clitest" + "testing" + + "github.com/oullin/database" +) + +func TestMakeHandler(t *testing.T) { + conn := clitest.MakeSQLiteConnection(t, &database.APIKey{}) + h, err := MakeHandler(conn, clitest.MakeTestEnv()) + if err != nil { + t.Fatalf("make handler: %v", err) + } + if h.TokenHandler == nil || h.Tokens == nil { + t.Fatalf("handler not properly initialized") + } + if err := h.CreateAccount("sampleaccount"); err != nil { + t.Fatalf("create account: %v", err) + } + var key database.APIKey + if err := conn.Sql().First(&key, "account_name = ?", "sampleaccount").Error; err != nil { + t.Fatalf("key not saved: %v", err) + } +} diff --git a/cli/accounts/handler_test.go b/cli/accounts/handler_test.go new file mode 100644 index 00000000..a79f9c26 --- /dev/null +++ b/cli/accounts/handler_test.go @@ -0,0 +1,37 @@ +package accounts + +import ( + "testing" + + clitest "github.com/oullin/cli/clitest" + "github.com/oullin/database" +) + +func setupAccountHandler(t *testing.T) *Handler { + conn := clitest.MakeSQLiteConnection(t, &database.APIKey{}) + h, err := MakeHandler(conn, clitest.MakeTestEnv()) + if err != nil { + t.Fatalf("make handler: %v", err) + } + return h +} + +func TestCreateReadSignature(t *testing.T) { + h := setupAccountHandler(t) + if err := h.CreateAccount("tester"); err != nil { + t.Fatalf("create: %v", err) + } + if err := h.ReadAccount("tester"); err != nil { + t.Fatalf("read: %v", err) + } + if err := h.CreateSignature("tester"); err != nil { + t.Fatalf("signature: %v", err) + } +} + +func TestCreateAccountInvalid(t *testing.T) { + h := setupAccountHandler(t) + if err := h.CreateAccount("ab"); err == nil { + t.Fatalf("expected error") + } +} diff --git a/cli/clitest/helpers.go b/cli/clitest/helpers.go new file mode 100644 index 00000000..11cf9dc0 --- /dev/null +++ b/cli/clitest/helpers.go @@ -0,0 +1,35 @@ +package clitest + +import ( + "reflect" + "testing" + "unsafe" + + "github.com/google/uuid" + "github.com/oullin/database" + "github.com/oullin/env" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func MakeSQLiteConnection(t *testing.T, models ...interface{}) *database.Connection { + t.Helper() + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if len(models) > 0 { + if err := db.AutoMigrate(models...); err != nil { + t.Fatalf("migrate: %v", err) + } + } + conn := &database.Connection{} + v := reflect.ValueOf(conn).Elem() + field := v.FieldByName("driver") + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(db)) + return conn +} + +func MakeTestEnv() *env.Environment { + return &env.Environment{App: env.AppEnvironment{MasterKey: uuid.NewString()[:32]}} +} diff --git a/cli/panel/menu_test.go b/cli/panel/menu_test.go new file mode 100644 index 00000000..308f86c9 --- /dev/null +++ b/cli/panel/menu_test.go @@ -0,0 +1,95 @@ +package panel + +import ( + "bufio" + "io" + "os" + "strings" + "testing" + + "github.com/oullin/pkg" +) + +func captureOutput(fn func()) string { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + fn() + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = old + return string(out) +} + +func TestPrintLineAndGetChoiceNil(t *testing.T) { + m := Menu{Reader: bufio.NewReader(strings.NewReader("\n"))} + m.PrintLine() + if m.GetChoice() != 0 { + t.Fatalf("expected 0") + } +} + +func TestPrint(t *testing.T) { + m := Menu{Reader: bufio.NewReader(strings.NewReader(""))} + _ = captureOutput(func() { m.Print() }) +} + +func TestCenterText(t *testing.T) { + m := Menu{} + if got := m.CenterText("hi", 6); got != " hi " { + t.Fatalf("unexpected: %q", got) + } + if got := m.CenterText("toolong", 4); got != "tool" { + t.Fatalf("unexpected truncation: %q", got) + } +} + +func TestPrintOption(t *testing.T) { + m := Menu{} + out := captureOutput(func() { m.PrintOption("x", 5) }) + if !strings.Contains(out, "║ x ║") { + t.Fatalf("unexpected output: %q", out) + } +} + +func TestCaptureInput(t *testing.T) { + m := Menu{Reader: bufio.NewReader(strings.NewReader("2\n"))} + if err := m.CaptureInput(); err != nil { + t.Fatalf("capture: %v", err) + } + if m.GetChoice() != 2 { + t.Fatalf("choice: %d", m.GetChoice()) + } + + bad := Menu{Reader: bufio.NewReader(strings.NewReader("bad\n"))} + if err := bad.CaptureInput(); err == nil { + t.Fatalf("expected error") + } +} + +func TestCaptureAccountName(t *testing.T) { + m := Menu{Reader: bufio.NewReader(strings.NewReader("Alice\n"))} + name, err := m.CaptureAccountName() + if err != nil || name != "Alice" { + t.Fatalf("got %q err %v", name, err) + } + + bad := Menu{Reader: bufio.NewReader(strings.NewReader("\n"))} + if _, err := bad.CaptureAccountName(); err == nil { + t.Fatalf("expected error") + } +} + +func TestCapturePostURL(t *testing.T) { + goodURL := "https://raw.githubusercontent.com/user/repo/file.md" + m := Menu{Reader: bufio.NewReader(strings.NewReader(goodURL + "\n")), Validator: pkg.GetDefaultValidator()} + in, err := m.CapturePostURL() + if err != nil || in.Url != goodURL { + t.Fatalf("got %v err %v", in, err) + } + + m2 := Menu{Reader: bufio.NewReader(strings.NewReader("http://example.com\n")), Validator: pkg.GetDefaultValidator()} + if _, err := m2.CapturePostURL(); err == nil { + t.Fatalf("expected error") + } +} diff --git a/cli/posts/handler_test.go b/cli/posts/handler_test.go new file mode 100644 index 00000000..6c44501c --- /dev/null +++ b/cli/posts/handler_test.go @@ -0,0 +1,101 @@ +package posts + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/google/uuid" + clitest "github.com/oullin/cli/clitest" + "github.com/oullin/database" + "github.com/oullin/pkg" + "github.com/oullin/pkg/markdown" +) + +func captureOutput(fn func()) string { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + fn() + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = old + return string(out) +} + +func setupPostsHandler(t *testing.T) (*Handler, *database.Connection) { + conn := clitest.MakeSQLiteConnection(t, &database.User{}, &database.Post{}, &database.Category{}, &database.PostCategory{}, &database.Tag{}, &database.PostTag{}) + user := database.User{UUID: uuid.NewString(), Username: "jdoe", FirstName: "John", LastName: "Doe", Email: "jdoe@example.com", PasswordHash: "x"} + if err := conn.Sql().Create(&user).Error; err != nil { + t.Fatalf("user create: %v", err) + } + conn.Sql().Create(&database.Category{UUID: uuid.NewString(), Name: "Tech", Slug: "tech"}) + conn.Sql().Create(&database.Tag{UUID: uuid.NewString(), Name: "Go", Slug: "go"}) + input := &Input{Url: "http://example"} + h := MakeHandler(input, pkg.MakeDefaultClient(nil), conn) + return &h, conn +} + +func TestHandlePost(t *testing.T) { + h, conn := setupPostsHandler(t) + post := &markdown.Post{ + FrontMatter: markdown.FrontMatter{ + Title: "Hello", + Excerpt: "ex", + Slug: "hello", + Author: "jdoe", + Categories: "tech", + PublishedAt: time.Now().Format("2006-01-02"), + Tags: []string{"go"}, + }, + Content: "world", + } + if err := h.HandlePost(post); err != nil { + t.Fatalf("handle: %v", err) + } + var p database.Post + if err := conn.Sql().Preload("Categories").First(&p, "slug = ?", "hello").Error; err != nil { + t.Fatalf("post not created: %v", err) + } + if len(p.Categories) != 1 { + t.Fatalf("expected 1 category") + } + _ = captureOutput(func() { h.RenderArticle(post) }) +} + +func TestHandlePostMissingAuthor(t *testing.T) { + h, _ := setupPostsHandler(t) + post := &markdown.Post{FrontMatter: markdown.FrontMatter{Author: "none"}} + if err := h.HandlePost(post); err == nil { + t.Fatalf("expected error") + } +} + +func TestNotParsed(t *testing.T) { + h, conn := setupPostsHandler(t) + md := "---\nauthor: jdoe\nslug: parsed\ncategories: tech\npublished_at: 2024-01-01\ntags:\n - go\n---\ncontent" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(md)) })) + defer srv.Close() + h.Input.Url = srv.URL + ok, err := h.NotParsed() + if err != nil || !ok { + t.Fatalf("not parsed: %v", err) + } + var p database.Post + if err := conn.Sql().First(&p, "slug = ?", "parsed").Error; err != nil { + t.Fatalf("post not saved") + } +} + +func TestNotParsedError(t *testing.T) { + h, _ := setupPostsHandler(t) + srv := httptest.NewServer(http.NotFoundHandler()) + defer srv.Close() + h.Input.Url = srv.URL + if ok, err := h.NotParsed(); err == nil || ok { + t.Fatalf("expected error") + } +} diff --git a/go.mod b/go.mod index 47db9a96..44426d43 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( golang.org/x/text v0.27.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.6.0 + gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.0 ) @@ -52,6 +53,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect diff --git a/go.sum b/go.sum index 193c8f6a..7818e3a9 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -233,6 +235,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= From 7329f5048ee5b254ecd3d9f39777611ebb60c24f Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 5 Aug 2025 10:08:48 +0800 Subject: [PATCH 3/7] Use testcontainers in CLI tests --- cli/accounts/factory_test.go | 2 +- cli/accounts/handler_test.go | 2 +- cli/clitest/helpers.go | 64 +++++++++++++++++++++++++++++------- cli/posts/handler_test.go | 2 +- go.mod | 2 -- go.sum | 4 --- 6 files changed, 55 insertions(+), 21 deletions(-) diff --git a/cli/accounts/factory_test.go b/cli/accounts/factory_test.go index ad953940..7ad903c4 100644 --- a/cli/accounts/factory_test.go +++ b/cli/accounts/factory_test.go @@ -8,7 +8,7 @@ import ( ) func TestMakeHandler(t *testing.T) { - conn := clitest.MakeSQLiteConnection(t, &database.APIKey{}) + conn := clitest.MakeTestConnection(t, &database.APIKey{}) h, err := MakeHandler(conn, clitest.MakeTestEnv()) if err != nil { t.Fatalf("make handler: %v", err) diff --git a/cli/accounts/handler_test.go b/cli/accounts/handler_test.go index a79f9c26..28d68da0 100644 --- a/cli/accounts/handler_test.go +++ b/cli/accounts/handler_test.go @@ -8,7 +8,7 @@ import ( ) func setupAccountHandler(t *testing.T) *Handler { - conn := clitest.MakeSQLiteConnection(t, &database.APIKey{}) + conn := clitest.MakeTestConnection(t, &database.APIKey{}) h, err := MakeHandler(conn, clitest.MakeTestEnv()) if err != nil { t.Fatalf("make handler: %v", err) diff --git a/cli/clitest/helpers.go b/cli/clitest/helpers.go index 11cf9dc0..9017248e 100644 --- a/cli/clitest/helpers.go +++ b/cli/clitest/helpers.go @@ -1,32 +1,72 @@ package clitest import ( - "reflect" + "context" + "os/exec" "testing" - "unsafe" "github.com/google/uuid" "github.com/oullin/database" "github.com/oullin/env" - "gorm.io/driver/sqlite" - "gorm.io/gorm" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" ) -func MakeSQLiteConnection(t *testing.T, models ...interface{}) *database.Connection { +func MakeTestConnection(t *testing.T, models ...interface{}) *database.Connection { t.Helper() - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + + if _, err := exec.LookPath("docker"); err != nil { + t.Skip("docker not installed") + } + + ctx := context.Background() + + pg, err := postgres.RunContainer(ctx, + testcontainers.WithImage("postgres:16-alpine"), + postgres.WithDatabase("testdb"), + postgres.WithUsername("test"), + postgres.WithPassword("secret"), + postgres.BasicWaitStrategies(), + ) + if err != nil { + t.Fatalf("container run err: %v", err) + } + t.Cleanup(func() { pg.Terminate(ctx) }) + + host, err := pg.Host(ctx) + if err != nil { + t.Fatalf("host err: %v", err) + } + port, err := pg.MappedPort(ctx, "5432/tcp") + if err != nil { + t.Fatalf("port err: %v", err) + } + + e := &env.Environment{ + DB: env.DBEnvironment{ + UserName: "test", + UserPassword: "secret", + DatabaseName: "testdb", + Port: port.Int(), + Host: host, + DriverName: database.DriverName, + SSLMode: "disable", + TimeZone: "UTC", + }, + } + + conn, err := database.MakeConnection(e) if err != nil { - t.Fatalf("open sqlite: %v", err) + t.Fatalf("make connection: %v", err) } + t.Cleanup(func() { conn.Close() }) + if len(models) > 0 { - if err := db.AutoMigrate(models...); err != nil { + if err := conn.Sql().AutoMigrate(models...); err != nil { t.Fatalf("migrate: %v", err) } } - conn := &database.Connection{} - v := reflect.ValueOf(conn).Elem() - field := v.FieldByName("driver") - reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(db)) + return conn } diff --git a/cli/posts/handler_test.go b/cli/posts/handler_test.go index 6c44501c..22ac8fd1 100644 --- a/cli/posts/handler_test.go +++ b/cli/posts/handler_test.go @@ -27,7 +27,7 @@ func captureOutput(fn func()) string { } func setupPostsHandler(t *testing.T) (*Handler, *database.Connection) { - conn := clitest.MakeSQLiteConnection(t, &database.User{}, &database.Post{}, &database.Category{}, &database.PostCategory{}, &database.Tag{}, &database.PostTag{}) + conn := clitest.MakeTestConnection(t, &database.User{}, &database.Post{}, &database.Category{}, &database.PostCategory{}, &database.Tag{}, &database.PostTag{}) user := database.User{UUID: uuid.NewString(), Username: "jdoe", FirstName: "John", LastName: "Doe", Email: "jdoe@example.com", PasswordHash: "x"} if err := conn.Sql().Create(&user).Error; err != nil { t.Fatalf("user create: %v", err) diff --git a/go.mod b/go.mod index 44426d43..47db9a96 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( golang.org/x/text v0.27.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.6.0 - gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.0 ) @@ -53,7 +52,6 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect diff --git a/go.sum b/go.sum index 7818e3a9..193c8f6a 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,6 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -235,8 +233,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= -gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= -gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= From 47ee792439b5a3155e24ba2ab0356bfeba1a92f7 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 5 Aug 2025 10:19:21 +0800 Subject: [PATCH 4/7] test: run table checks as subtests --- database/model_internal_test.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/database/model_internal_test.go b/database/model_internal_test.go index 0bb726b6..a3f91e97 100644 --- a/database/model_internal_test.go +++ b/database/model_internal_test.go @@ -7,14 +7,19 @@ import ( func TestIsValidTable(t *testing.T) { for _, name := range GetSchemaTables() { - if !isValidTable(name) { - t.Fatalf("expected %s table to be valid", name) - } + name := name + t.Run(name, func(t *testing.T) { + if !isValidTable(name) { + t.Errorf("expected table %q to be valid", name) + } + }) } - if isValidTable("unknown") { - t.Fatalf("unexpected valid table") - } + t.Run("nonexistent table", func(t *testing.T) { + if isValidTable("unknown") { + t.Error(`expected table "unknown" to be invalid`) + } + }) } func TestIsValidTableNonexistentTables(t *testing.T) { @@ -30,8 +35,11 @@ func TestIsValidTableNonexistentTables(t *testing.T) { } for _, name := range invalid { - if isValidTable(name) { - t.Fatalf("%q should be invalid", name) - } + name := name + t.Run(name, func(t *testing.T) { + if isValidTable(name) { + t.Errorf("%q should be invalid", name) + } + }) } } From 2a3321eae88440a4336336b4860c5b8c078734b1 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 5 Aug 2025 10:28:43 +0800 Subject: [PATCH 5/7] Run CLI tests in CI --- .github/workflows/tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 380e5c97..26baa423 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,6 +53,11 @@ jobs: go test -coverpkg=./... ./database/... -coverprofile=coverage-database-${{ matrix.go-version }}.out go tool cover -func=coverage-database-${{ matrix.go-version }}.out | tail -n 1 + - name: Run CLI tests + run: | + go test -coverpkg=./... ./cli/... -coverprofile=coverage-cli-${{ matrix.go-version }}.out + go tool cover -func=coverage-cli-${{ matrix.go-version }}.out | tail -n 1 + - name: Merge coverage reports run: | echo "mode: set" > coverage-${{ matrix.go-version }}.out From 2be345a30bcc004280e41291a0c97a710aec722d Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 5 Aug 2025 10:40:55 +0800 Subject: [PATCH 6/7] Test clitest helpers --- cli/accounts/factory_test.go | 9 +++++++++ cli/accounts/handler_test.go | 14 ++++++++++++++ cli/clitest/helpers_test.go | 22 ++++++++++++++++++++++ cli/posts/handler_test.go | 27 +++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 cli/clitest/helpers_test.go diff --git a/cli/accounts/factory_test.go b/cli/accounts/factory_test.go index 7ad903c4..0afebce6 100644 --- a/cli/accounts/factory_test.go +++ b/cli/accounts/factory_test.go @@ -24,3 +24,12 @@ func TestMakeHandler(t *testing.T) { t.Fatalf("key not saved: %v", err) } } + +func TestMakeHandlerInvalidKey(t *testing.T) { + conn := clitest.MakeTestConnection(t) + env := clitest.MakeTestEnv() + env.App.MasterKey = "short" + if _, err := MakeHandler(conn, env); err == nil { + t.Fatalf("expected error") + } +} diff --git a/cli/accounts/handler_test.go b/cli/accounts/handler_test.go index 28d68da0..d7a9da72 100644 --- a/cli/accounts/handler_test.go +++ b/cli/accounts/handler_test.go @@ -35,3 +35,17 @@ func TestCreateAccountInvalid(t *testing.T) { t.Fatalf("expected error") } } + +func TestReadAccountNotFound(t *testing.T) { + h := setupAccountHandler(t) + if err := h.ReadAccount("missing"); err == nil { + t.Fatalf("expected error") + } +} + +func TestCreateSignatureNotFound(t *testing.T) { + h := setupAccountHandler(t) + if err := h.CreateSignature("missing"); err == nil { + t.Fatalf("expected error") + } +} diff --git a/cli/clitest/helpers_test.go b/cli/clitest/helpers_test.go new file mode 100644 index 00000000..1fb1867f --- /dev/null +++ b/cli/clitest/helpers_test.go @@ -0,0 +1,22 @@ +package clitest + +import ( + "os/exec" + "testing" +) + +func TestMakeTestEnv(t *testing.T) { + env := MakeTestEnv() + if len(env.App.MasterKey) != 32 { + t.Fatalf("expected master key length 32, got %d", len(env.App.MasterKey)) + } +} + +func TestMakeTestConnectionSkipsWithoutDocker(t *testing.T) { + if _, err := exec.LookPath("docker"); err == nil { + t.Skip("docker available") + } + t.Run("skip", func(t *testing.T) { + MakeTestConnection(t) + }) +} diff --git a/cli/posts/handler_test.go b/cli/posts/handler_test.go index 22ac8fd1..1d3baa19 100644 --- a/cli/posts/handler_test.go +++ b/cli/posts/handler_test.go @@ -74,6 +74,33 @@ func TestHandlePostMissingAuthor(t *testing.T) { } } +func TestHandlePostEmptyCategories(t *testing.T) { + h, _ := setupPostsHandler(t) + post := &markdown.Post{FrontMatter: markdown.FrontMatter{Author: "jdoe"}} + if err := h.HandlePost(post); err == nil { + t.Fatalf("expected error") + } +} + +func TestHandlePostInvalidDate(t *testing.T) { + h, _ := setupPostsHandler(t) + post := &markdown.Post{FrontMatter: markdown.FrontMatter{Author: "jdoe", PublishedAt: "bad"}} + if err := h.HandlePost(post); err == nil { + t.Fatalf("expected error") + } +} + +func TestHandlePostDuplicateSlug(t *testing.T) { + h, _ := setupPostsHandler(t) + post := &markdown.Post{FrontMatter: markdown.FrontMatter{Author: "jdoe", Slug: "dup", Categories: "tech", PublishedAt: time.Now().Format("2006-01-02")}} + if err := h.HandlePost(post); err != nil { + t.Fatalf("first create: %v", err) + } + if err := h.HandlePost(post); err == nil { + t.Fatalf("expected duplicate error") + } +} + func TestNotParsed(t *testing.T) { h, conn := setupPostsHandler(t) md := "---\nauthor: jdoe\nslug: parsed\ncategories: tech\npublished_at: 2024-01-01\ntags:\n - go\n---\ncontent" From 5fc054546ef46bc8882a991f6a3f84131a9c917f Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 5 Aug 2025 11:54:24 +0800 Subject: [PATCH 7/7] fix docker availability check in clitest --- cli/clitest/helpers_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/clitest/helpers_test.go b/cli/clitest/helpers_test.go index 1fb1867f..8b2f2aed 100644 --- a/cli/clitest/helpers_test.go +++ b/cli/clitest/helpers_test.go @@ -13,8 +13,8 @@ func TestMakeTestEnv(t *testing.T) { } func TestMakeTestConnectionSkipsWithoutDocker(t *testing.T) { - if _, err := exec.LookPath("docker"); err == nil { - t.Skip("docker available") + if _, err := exec.LookPath("docker"); err != nil { + t.Skip("docker not available") } t.Run("skip", func(t *testing.T) { MakeTestConnection(t)