diff --git a/activityPub.go b/activityPub.go index 419b252..9cc4823 100644 --- a/activityPub.go +++ b/activityPub.go @@ -267,14 +267,13 @@ func (a *goBlog) apGetRemoteActor(iri string) (*asPerson, int, error) { return actor, 0, nil } -func (db *database) apGetAllInboxes(blog string) ([]string, error) { +func (db *database) apGetAllInboxes(blog string) (inboxes []string, err error) { rows, err := db.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog)) if err != nil { return nil, err } - inboxes := []string{} + var inbox string for rows.Next() { - var inbox string err = rows.Scan(&inbox) if err != nil { return nil, err diff --git a/authentication_test.go b/authentication_test.go index 027c68a..1425856 100644 --- a/authentication_test.go +++ b/authentication_test.go @@ -44,9 +44,7 @@ func Test_authMiddleware(t *testing.T) { } _ = app.initDatabase(false) - app.initSessions() - _ = app.initTemplateStrings() - _ = app.initRendering() + app.initComponents() app.d = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { _, _ = rw.Write([]byte("ABC Test")) diff --git a/blogstats_test.go b/blogstats_test.go new file mode 100644 index 0000000..d0db3a4 --- /dev/null +++ b/blogstats_test.go @@ -0,0 +1,153 @@ +package main + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.goblog.app/app/pkgs/contenttype" +) + +func Test_blogStats(t *testing.T) { + app := &goBlog{ + cfg: &config{ + Db: &configDb{ + File: filepath.Join(t.TempDir(), "test.db"), + }, + Server: &configServer{ + PublicAddress: "https://example.com", + }, + Blogs: map[string]*configBlog{ + "en": { + Lang: "en", + BlogStats: &configBlogStats{ + Enabled: true, + Path: "/stats", + }, + Sections: map[string]*configSection{ + "test": {}, + }, + }, + }, + DefaultBlog: "en", + User: &configUser{}, + Webmention: &configWebmention{ + DisableSending: true, + }, + }, + } + + _ = app.initDatabase(false) + app.initComponents() + + // Insert post + + err := app.createPost(&post{ + Content: "This is a simple **test** post", + Blog: "en", + Section: "test", + Published: "2020-06-01", + Status: statusPublished, + }) + require.NoError(t, err) + + err = app.createPost(&post{ + Content: "This is another simple **test** post", + Blog: "en", + Section: "test", + Published: "2021-05-01", + Status: statusPublished, + }) + require.NoError(t, err) + + // Test stats + + sd, err := app.db.getBlogStats("en") + require.NoError(t, err) + require.NotNil(t, sd) + + require.NotNil(t, sd.Total) + assert.Equal(t, "2", sd.Total.Posts) + assert.Equal(t, "12", sd.Total.Words) + assert.Equal(t, "48", sd.Total.Chars) + + // 2021 + require.NotNil(t, sd.Years) + row := sd.Years[0] + require.NotNil(t, row) + assert.Equal(t, "2021", row.Name) + assert.Equal(t, "1", row.Posts) + assert.Equal(t, "6", row.Words) + assert.Equal(t, "27", row.Chars) + + // 2021-05 + require.NotNil(t, sd.Months) + require.NotEmpty(t, sd.Months["2021"]) + row = sd.Months["2021"][0] + require.NotNil(t, row) + assert.Equal(t, "05", row.Name) + assert.Equal(t, "1", row.Posts) + assert.Equal(t, "6", row.Words) + assert.Equal(t, "27", row.Chars) + + // 2020 + require.NotNil(t, sd.Years) + row = sd.Years[1] + require.NotNil(t, row) + assert.Equal(t, "2020", row.Name) + assert.Equal(t, "1", row.Posts) + assert.Equal(t, "6", row.Words) + assert.Equal(t, "21", row.Chars) + + // Test if cache exists + + assert.NotNil(t, app.db.loadBlogStatsCache("en")) + + // Test HTML + + t.Run("Test stats table", func(t *testing.T) { + h := http.HandlerFunc(app.serveBlogStatsTable) + + req := httptest.NewRequest(http.MethodGet, "/abc", nil) + req = req.WithContext(context.WithValue(req.Context(), blogKey, "en")) + + rec := httptest.NewRecorder() + + h(rec, req) + + res := rec.Result() + resBody, _ := io.ReadAll(res.Body) + _ = res.Body.Close() + resString := string(resBody) + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, resString, "class=statsyear data-year=2021") + assert.Contains(t, res.Header.Get(contentType), contenttype.HTML) + }) + + t.Run("Test stats page", func(t *testing.T) { + h := http.HandlerFunc(app.serveBlogStats) + + req := httptest.NewRequest(http.MethodGet, "/abc", nil) + req = req.WithContext(context.WithValue(req.Context(), blogKey, "en")) + + rec := httptest.NewRecorder() + + h(rec, req) + + res := rec.Result() + resBody, _ := io.ReadAll(res.Body) + _ = res.Body.Close() + resString := string(resBody) + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Contains(t, resString, "data-table=/stats.table.html") + assert.Contains(t, res.Header.Get(contentType), contenttype.HTML) + }) + +} diff --git a/captcha_test.go b/captcha_test.go index cb676b8..99b0105 100644 --- a/captcha_test.go +++ b/captcha_test.go @@ -31,9 +31,7 @@ func Test_captchaMiddleware(t *testing.T) { } _ = app.initDatabase(false) - app.initSessions() - _ = app.initTemplateStrings() - _ = app.initRendering() + app.initComponents() h := app.captchaMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { _, _ = rw.Write([]byte("ABC Test")) diff --git a/errors_test.go b/errors_test.go index 9036834..6e711c0 100644 --- a/errors_test.go +++ b/errors_test.go @@ -31,10 +31,7 @@ func Test_errors(t *testing.T) { } _ = app.initDatabase(false) - app.initMarkdown() - app.initSessions() - _ = app.initTemplateStrings() - _ = app.initRendering() + app.initComponents() t.Run("Test 404, no HTML", func(t *testing.T) { h := http.HandlerFunc(app.serve404) diff --git a/hooks.go b/hooks.go index e4cdb3e..02f4229 100644 --- a/hooks.go +++ b/hooks.go @@ -21,13 +21,15 @@ type postHookFunc func(*post) func (a *goBlog) postPostHooks(p *post) { // Hooks after post published - for _, cmdTmplString := range a.cfg.Hooks.PostPost { - go func(p *post, cmdTmplString string) { - a.cfg.Hooks.executeTemplateCommand("post-post", cmdTmplString, map[string]interface{}{ - "URL": a.fullPostURL(p), - "Post": p, - }) - }(p, cmdTmplString) + if hc := a.cfg.Hooks; hc != nil { + for _, cmdTmplString := range hc.PostPost { + go func(p *post, cmdTmplString string) { + a.cfg.Hooks.executeTemplateCommand("post-post", cmdTmplString, map[string]interface{}{ + "URL": a.fullPostURL(p), + "Post": p, + }) + }(p, cmdTmplString) + } } for _, f := range a.pPostHooks { go f(p) @@ -36,13 +38,15 @@ func (a *goBlog) postPostHooks(p *post) { func (a *goBlog) postUpdateHooks(p *post) { // Hooks after post updated - for _, cmdTmplString := range a.cfg.Hooks.PostUpdate { - go func(p *post, cmdTmplString string) { - a.cfg.Hooks.executeTemplateCommand("post-update", cmdTmplString, map[string]interface{}{ - "URL": a.fullPostURL(p), - "Post": p, - }) - }(p, cmdTmplString) + if hc := a.cfg.Hooks; hc != nil { + for _, cmdTmplString := range hc.PostUpdate { + go func(p *post, cmdTmplString string) { + a.cfg.Hooks.executeTemplateCommand("post-update", cmdTmplString, map[string]interface{}{ + "URL": a.fullPostURL(p), + "Post": p, + }) + }(p, cmdTmplString) + } } for _, f := range a.pUpdateHooks { go f(p) @@ -50,13 +54,15 @@ func (a *goBlog) postUpdateHooks(p *post) { } func (a *goBlog) postDeleteHooks(p *post) { - for _, cmdTmplString := range a.cfg.Hooks.PostDelete { - go func(p *post, cmdTmplString string) { - a.cfg.Hooks.executeTemplateCommand("post-delete", cmdTmplString, map[string]interface{}{ - "URL": a.fullPostURL(p), - "Post": p, - }) - }(p, cmdTmplString) + if hc := a.cfg.Hooks; hc != nil { + for _, cmdTmplString := range hc.PostDelete { + go func(p *post, cmdTmplString string) { + a.cfg.Hooks.executeTemplateCommand("post-delete", cmdTmplString, map[string]interface{}{ + "URL": a.fullPostURL(p), + "Post": p, + }) + }(p, cmdTmplString) + } } for _, f := range a.pDeleteHooks { go f(p) diff --git a/httpClient.go b/httpClient.go index b7b76d5..6eee87a 100644 --- a/httpClient.go +++ b/httpClient.go @@ -17,7 +17,3 @@ func getHTTPClient() httpClient { }, } } - -func (a *goBlog) initHTTPClient() { - a.httpClient = getHTTPClient() -} diff --git a/main.go b/main.go index 748fb57..271bbea 100644 --- a/main.go +++ b/main.go @@ -46,8 +46,12 @@ func main() { }() } - app := &goBlog{} - app.initHTTPClient() + // Init regular garbage collection + initGC() + + app := &goBlog{ + httpClient: getHTTPClient(), + } // Initialize config if err = app.initConfig(); err != nil { @@ -79,9 +83,6 @@ func main() { return } - // Init regular garbage collection - initGC() - // Execute pre-start hooks app.preStartHooks() @@ -91,18 +92,36 @@ func main() { return } - log.Println("Initialize components...") - - app.initMarkdown() - // Link check tool after init of markdown if len(os.Args) >= 2 && os.Args[1] == "check" { + app.initMarkdown() app.checkAllExternalLinks() app.shutdown.ShutdownAndWait() return } - // More initializations + // Initialize components + app.initComponents() + + // Start cron hooks + app.startHourlyHooks() + + // Start the server + err = app.startServer() + if err != nil { + app.logErrAndQuit("Failed to start server(s):", err.Error()) + return + } + + // Wait till everything is shutdown + app.shutdown.Wait() +} + +func (app *goBlog) initComponents() { + var err error + // Log start + log.Println("Initialize components...") + app.initMarkdown() if err = app.initTemplateAssets(); err != nil { // Needs minify app.logErrAndQuit("Failed to init template assets:", err.Error()) return @@ -135,21 +154,8 @@ func main() { app.initTelegram() app.initBlogStats() app.initSessions() - - // Start cron hooks - app.startHourlyHooks() - + // Log finish log.Println("Initialized components") - - // Start the server - err = app.startServer() - if err != nil { - app.logErrAndQuit("Failed to start server(s):", err.Error()) - return - } - - // Wait till everything is shutdown - app.shutdown.Wait() } func (a *goBlog) logErrAndQuit(v ...interface{}) { diff --git a/postsDb.go b/postsDb.go index 3fbca98..5954d78 100644 --- a/postsDb.go +++ b/postsDb.go @@ -80,10 +80,10 @@ func (a *goBlog) checkPost(p *post) (err error) { p.Slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random) } published := timeNoErr(dateparse.ParseLocal(p.Published)) - pathTmplString := a.cfg.Blogs[p.Blog].Sections[p.Section].PathTemplate - if pathTmplString == "" { - return errors.New("path template empty") - } + pathTmplString := defaultIfEmpty( + a.cfg.Blogs[p.Blog].Sections[p.Section].PathTemplate, + "{{printf \""+a.getRelativePath(p.Blog, "/%v/%02d/%02d/%v")+"\" .Section .Year .Month .Slug}}", + ) pathTmpl, err := template.New("location").Parse(pathTmplString) if err != nil { return errors.New("failed to parse location template") diff --git a/webmentionSending.go b/webmentionSending.go index 5060c3b..9b5b616 100644 --- a/webmentionSending.go +++ b/webmentionSending.go @@ -35,7 +35,10 @@ func (a *goBlog) sendWebmentions(p *post) error { return err } links = append(links, contentLinks...) - links = append(links, p.firstParameter("link"), p.firstParameter(a.cfg.Micropub.LikeParam), p.firstParameter(a.cfg.Micropub.ReplyParam), p.firstParameter(a.cfg.Micropub.BookmarkParam)) + links = append(links, p.firstParameter("link")) + if mpc := a.cfg.Micropub; mpc != nil { + links = append(links, p.firstParameter(a.cfg.Micropub.LikeParam), p.firstParameter(a.cfg.Micropub.ReplyParam), p.firstParameter(a.cfg.Micropub.BookmarkParam)) + } for _, link := range funk.UniqString(links) { if link == "" { continue