cb3641e7bb
internal/rss parses RSS 2.0 and Atom, and polls each configured feed on its own interval; internal/webfetch is the one door either of them uses to touch the network. The poller writes items as notes with source "rss:<feed>" and nothing else: the answer path reads them back when he asks "что нового в лентах?", and nothing is announced on arrival. A feed that dispatched would be a nag, which is why the plan's breaking-news rule was left out rather than built. webfetch is where the limits live, as code rather than a paragraph: http(s) only, an allowlist (the configured feeds' hosts) and a denylist, a 2 MiB body cap, a 3-redirect cap, one request per host per second, and a refusal to connect to any private address — checked in the dialer's Control hook so it holds for every resolved address and every redirect hop, not just for a literal IP. Off unless configured: no "feeds" block, no poller, no outbound request. How far a feed was read is a config fact (rss:latest:<name>), so a restart does not re-note yesterday's headlines.
49 lines
1.6 KiB
Go
49 lines
1.6 KiB
Go
package router
|
|
|
|
import "testing"
|
|
|
|
func TestParseFeedQuery(t *testing.T) {
|
|
cases := []struct {
|
|
text string
|
|
ok bool
|
|
category string
|
|
}{
|
|
{"что нового в лентах?", true, ""},
|
|
{"что нового?", true, ""},
|
|
{"какие новости?", true, ""},
|
|
{"что нового по технологиям?", true, "технологиям"},
|
|
{"расскажи новости про политику", true, "политику"},
|
|
{"что нового по новостям", true, ""},
|
|
{"what's new in the feeds?", true, ""},
|
|
{"any news about kubernetes", true, "kubernetes"},
|
|
// Statements, not requests.
|
|
{"у меня новая лента в инстаграме", false, ""},
|
|
{"новости меня утомили", false, ""},
|
|
{"напомни полить цветы", false, ""},
|
|
{"", false, ""},
|
|
}
|
|
for _, c := range cases {
|
|
q, ok := ParseFeedQuery(c.text)
|
|
if ok != c.ok {
|
|
t.Errorf("ParseFeedQuery(%q) ok = %v, want %v", c.text, ok, c.ok)
|
|
continue
|
|
}
|
|
if ok && q.Category != c.category {
|
|
t.Errorf("ParseFeedQuery(%q) category = %q, want %q", c.text, q.Category, c.category)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCategoryMatches(t *testing.T) {
|
|
// The inflected form he says must match the form the config spells.
|
|
if !CategoryMatches("Новый релиз [технологии]", "технологиям") {
|
|
t.Error("inflected category did not match")
|
|
}
|
|
if CategoryMatches("Новый релиз [технологии]", "политику") {
|
|
t.Error("unrelated category matched")
|
|
}
|
|
if !CategoryMatches("anything", "") {
|
|
t.Error("an empty category must match everything")
|
|
}
|
|
}
|