diff --git a/app.go b/app.go index fe964c1..f016d4a 100644 --- a/app.go +++ b/app.go @@ -36,7 +36,7 @@ type goBlog struct { pPostHooks []postHookFunc pUpdateHooks []postHookFunc pDeleteHooks []postHookFunc - // HTTP + // HTTP Routers d *dynamicHandler privateMode bool privateModeHandler []func(http.Handler) http.Handler diff --git a/database_test.go b/database_test.go index c431848..6108eae 100644 --- a/database_test.go +++ b/database_test.go @@ -4,6 +4,10 @@ import ( "testing" ) +func (a *goBlog) setInMemoryDatabase() { + a.db, _ = a.openDatabase(":memory:", false) +} + func Test_database(t *testing.T) { t.Run("Basic Database Test", func(t *testing.T) { app := &goBlog{} diff --git a/httpClient.go b/httpClient.go index e275587..d674dc5 100644 --- a/httpClient.go +++ b/httpClient.go @@ -5,7 +5,11 @@ import ( "time" ) -var appHttpClient = &http.Client{ +type httpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +var appHttpClient httpClient = &http.Client{ Timeout: 5 * time.Minute, Transport: &http.Transport{ DisableKeepAlives: true, diff --git a/httpClient_test.go b/httpClient_test.go new file mode 100644 index 0000000..be1e497 --- /dev/null +++ b/httpClient_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "io" + "net/http" + "strings" + "sync" +) + +type fakeHttpClient struct { + req *http.Request + res *http.Response + err error + enabled bool + // internal + alt httpClient + mx sync.Mutex +} + +var fakeAppHttpClient *fakeHttpClient + +func init() { + fakeAppHttpClient = &fakeHttpClient{ + alt: appHttpClient, + } + appHttpClient = fakeAppHttpClient +} + +func (c *fakeHttpClient) Do(req *http.Request) (*http.Response, error) { + if !c.enabled { + return c.alt.Do(req) + } + c.req = req + return c.res, c.err +} + +func (c *fakeHttpClient) clean() { + c.req = nil + c.err = nil + c.res = nil +} + +func (c *fakeHttpClient) setFakeResponse(statusCode int, body string, err error) { + c.clean() + c.err = err + c.res = &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(strings.NewReader(body)), + } +} + +func (c *fakeHttpClient) lock(enabled bool) { + c.mx.Lock() + c.clean() + c.enabled = enabled +} + +func (c *fakeHttpClient) unlock() { + c.enabled = false + c.clean() + c.mx.Unlock() +} diff --git a/notifications.go b/notifications.go index 9c0a23e..9cd8fe6 100644 --- a/notifications.go +++ b/notifications.go @@ -30,11 +30,9 @@ func (a *goBlog) sendNotification(text string) { log.Println("Failed to save notification:", err.Error()) } if an := a.cfg.Notifications; an != nil { - if tg := an.Telegram; tg != nil && tg.Enabled { - err := sendTelegramMessage(n.Text, "", tg.BotToken, tg.ChatID) - if err != nil { - log.Println("Failed to send Telegram notification:", err.Error()) - } + err := an.Telegram.send(n.Text, "") + if err != nil { + log.Println("Failed to send Telegram notification:", err.Error()) } } } diff --git a/telegram.go b/telegram.go index 083ef5f..4299942 100644 --- a/telegram.go +++ b/telegram.go @@ -14,24 +14,27 @@ import ( const telegramBaseURL = "https://api.telegram.org/bot" func (a *goBlog) initTelegram() { - enable := false - for _, b := range a.cfg.Blogs { - if tg := b.Telegram; tg != nil && tg.Enabled && tg.BotToken != "" && tg.ChatID != "" { - enable = true - } - } - if enable { - a.pPostHooks = append(a.pPostHooks, func(p *post) { - if p.isPublishedSectionPost() { - tgPost(a.cfg.Blogs[p.Blog].Telegram, p.title(), a.fullPostURL(p), a.shortPostURL(p)) + a.pPostHooks = append(a.pPostHooks, func(p *post) { + if tg := a.cfg.Blogs[p.Blog].Telegram; tg.enabled() && p.isPublishedSectionPost() { + if html := tg.generateHTML(p.title(), a.fullPostURL(p), a.shortPostURL(p)); html != "" { + if err := tg.send(html, "HTML"); err != nil { + log.Printf("Failed to send post to Telegram: %v", err) + } } - }) - } + } + }) } -func tgPost(tg *configTelegram, title, fullURL, shortURL string) { +func (tg *configTelegram) enabled() bool { if tg == nil || !tg.Enabled || tg.BotToken == "" || tg.ChatID == "" { - return + return false + } + return true +} + +func (tg *configTelegram) generateHTML(title, fullURL, shortURL string) string { + if !tg.enabled() { + return "" } replacer := strings.NewReplacer("<", "<", ">", ">", "&", "&") var message bytes.Buffer @@ -48,19 +51,20 @@ func tgPost(tg *configTelegram, title, fullURL, shortURL string) { message.WriteString(replacer.Replace(shortURL)) message.WriteString("") } - if err := sendTelegramMessage(message.String(), "HTML", tg.BotToken, tg.ChatID); err != nil { - log.Println(err.Error()) - } + return message.String() } -func sendTelegramMessage(message, mode, token, chat string) error { +func (tg *configTelegram) send(message, mode string) error { + if !tg.enabled() { + return nil + } params := url.Values{} - params.Add("chat_id", chat) + params.Add("chat_id", tg.ChatID) params.Add("text", message) if mode != "" { params.Add("parse_mode", mode) } - tgURL, err := url.Parse(telegramBaseURL + token + "/sendMessage") + tgURL, err := url.Parse(telegramBaseURL + tg.BotToken + "/sendMessage") if err != nil { return errors.New("failed to create Telegram request") } diff --git a/telegram_test.go b/telegram_test.go new file mode 100644 index 0000000..7e07fea --- /dev/null +++ b/telegram_test.go @@ -0,0 +1,193 @@ +package main + +import ( + "net/http" + "testing" + "time" +) + +func Test_configTelegram_enabled(t *testing.T) { + if (&configTelegram{}).enabled() == true { + t.Error("Telegram shouldn't be enabled") + } + + if (&configTelegram{ + Enabled: true, + }).enabled() == true { + t.Error("Telegram shouldn't be enabled") + } + + if (&configTelegram{ + Enabled: true, + ChatID: "abc", + }).enabled() == true { + t.Error("Telegram shouldn't be enabled") + } + + if (&configTelegram{ + Enabled: true, + BotToken: "abc", + }).enabled() == true { + t.Error("Telegram shouldn't be enabled") + } + + if (&configTelegram{ + Enabled: true, + BotToken: "abc", + ChatID: "abc", + }).enabled() != true { + t.Error("Telegram should be enabled") + } +} + +func Test_configTelegram_generateHTML(t *testing.T) { + tg := &configTelegram{ + Enabled: true, + ChatID: "abc", + BotToken: "abc", + } + + // Without Instant View + + expected := "Title\n\nhttps://example.com/s/1" + if got := tg.generateHTML("Title", "https://example.com/test", "https://example.com/s/1"); got != expected { + t.Errorf("Wrong result, got: %v", got) + } + + // With Instant View + + tg.InstantViewHash = "abc" + expected = "Title\n\nhttps://example.com/s/1" + if got := tg.generateHTML("Title", "https://example.com/test", "https://example.com/s/1"); got != expected { + t.Errorf("Wrong result, got: %v", got) + } +} + +func Test_configTelegram_send(t *testing.T) { + fakeAppHttpClient.lock(true) + defer fakeAppHttpClient.unlock() + + tg := &configTelegram{ + Enabled: true, + ChatID: "chatid", + BotToken: "bottoken", + } + + fakeAppHttpClient.setFakeResponse(200, "", nil) + + err := tg.send("Message", "HTML") + if err != nil { + t.Errorf("Error: %v", err) + } + + if fakeAppHttpClient.req == nil { + t.Error("Empty request") + } + if fakeAppHttpClient.err != nil { + t.Error("Error in request") + } + if fakeAppHttpClient.req.Method != http.MethodPost { + t.Error("Wrong method") + } + if u := fakeAppHttpClient.req.URL.String(); u != "https://api.telegram.org/botbottoken/sendMessage?chat_id=chatid&parse_mode=HTML&text=Message" { + t.Errorf("Wrong request URL, got: %v", u) + } +} + +func Test_goBlog_initTelegram(t *testing.T) { + app := &goBlog{ + pPostHooks: []postHookFunc{}, + } + + app.initTelegram() + + if len(app.pPostHooks) != 1 { + t.Error("Hook not registered") + } +} + +func Test_telegram(t *testing.T) { + t.Run("Send post to Telegram", func(t *testing.T) { + fakeAppHttpClient.lock(true) + defer fakeAppHttpClient.unlock() + + fakeAppHttpClient.setFakeResponse(200, "", nil) + + app := &goBlog{ + pPostHooks: []postHookFunc{}, + cfg: &config{ + Server: &configServer{ + PublicAddress: "https://example.com", + }, + Blogs: map[string]*configBlog{ + "en": { + Telegram: &configTelegram{ + Enabled: true, + ChatID: "chatid", + BotToken: "bottoken", + }, + }, + }, + }, + } + app.setInMemoryDatabase() + + app.initTelegram() + + p := &post{ + Path: "/test", + Parameters: map[string][]string{ + "title": {"Title"}, + }, + Published: time.Now().String(), + Section: "test", + Blog: "en", + Status: statusPublished, + } + + app.pPostHooks[0](p) + + if u := fakeAppHttpClient.req.URL.String(); u != "https://api.telegram.org/botbottoken/sendMessage?chat_id=chatid&parse_mode=HTML&text=Title%0A%0A%3Ca+href%3D%22https%3A%2F%2Fexample.com%2Fs%2F1%22%3Ehttps%3A%2F%2Fexample.com%2Fs%2F1%3C%2Fa%3E" { + t.Errorf("Wrong request URL, got: %v", u) + } + }) + + t.Run("Telegram disabled", func(t *testing.T) { + fakeAppHttpClient.lock(true) + defer fakeAppHttpClient.unlock() + + fakeAppHttpClient.setFakeResponse(200, "", nil) + + app := &goBlog{ + pPostHooks: []postHookFunc{}, + cfg: &config{ + Server: &configServer{ + PublicAddress: "https://example.com", + }, + Blogs: map[string]*configBlog{ + "en": {}, + }, + }, + } + app.setInMemoryDatabase() + + app.initTelegram() + + p := &post{ + Path: "/test", + Parameters: map[string][]string{ + "title": {"Title"}, + }, + Published: time.Now().String(), + Section: "test", + Blog: "en", + Status: statusPublished, + } + + app.pPostHooks[0](p) + + if fakeAppHttpClient.req != nil { + t.Error("There should be no request") + } + }) +}