项目文件夹

文件
2026-07-13 12:30:20 +08:00

4840 行
134 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package server
import (
"fmt"
"os"
"strings"
"testing"
"time"
"unicode/utf8"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"github.com/zakirullin/files.md/server/db"
"github.com/zakirullin/files.md/server/fs"
"github.com/zakirullin/files.md/server/journal"
"github.com/zakirullin/files.md/server/pkg/tg"
"github.com/zakirullin/files.md/server/pkg/txt"
"github.com/zakirullin/files.md/server/userconfig"
)
func init() {
fs.Ctime = func(fi os.FileInfo) int64 {
return 0
}
}
// inboxMsgHash returns the hash of the nth non-header block in Chat.md, or
// a placeholder string if the inbox doesn't yet exist or has fewer entries.
// Tests use this to resolve the stable inbox-entry identifier the bot now
// carries in its callback params (previously a positional int index).
func inboxMsgHash(t *testing.T, userFS *fs.FS, nth int) string {
t.Helper()
content, err := userFS.Read(fs.DirUserRoot, fs.ChatFilename)
if err != nil {
return "nohash"
}
blocks := readChatMsgs(content)
i := 0
for _, block := range blocks {
if strings.HasPrefix(block, "#### ") {
continue
}
if i == nth {
return chatBlockHash(block)
}
i++
}
return "nohash"
}
func TestSaveFromTextMsg(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2025, 6, 29, 12, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, "New task"))
r.NoError(err)
chat, err := bot.fs.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 29 June, Sunday\n- [ ] `12:00` New task\n", chat)
}
// A message containing a slash-token in the middle (e.g. "Check the /token
// please") used to be intercepted as a `/token` command invocation and never
// reached the chat. With the cmd-extraction now requiring the slashed token
// to be the entire message, this kind of note must save normally with the
// `/token` text intact.
func TestSaveFromTextMsgWithSlashCmdInTheMiddle(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2025, 6, 29, 12, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
const text = "Check the /token please"
rawUpdate := tgbotapi.Update{
Message: &tgbotapi.Message{
Chat: &tgbotapi.Chat{ID: -1},
Text: text,
Entities: []tgbotapi.MessageEntity{{
Type: "bot_command",
Offset: 10, // index of '/' in text
Length: 6, // length of "/token"
}},
},
}
err = bot.Reply(tg.NewTGUpd(rawUpdate))
r.NoError(err)
chat, err := bot.fs.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 29 June, Sunday\n- [ ] `12:00` Check the /token please\n", chat)
}
// TODO Chat.md
func TestSaveFromLongTextMsg(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, strings.Repeat("a", 34)))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
tasks, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(tasks, 1)
filename := "A" + strings.Repeat("a", 33) + ".md"
r.Equal(filename, tasks[0].Name)
content, err := bot.fs.Read("notes", filename)
r.NoError(err)
r.Equal("", content)
}
// TODO Chat.md
//
// func TestSaveFromTextMsgWithSanitize(t *testing.T) {
// r := require.New(t)
//
// mode := userconfig.DefaultConfig.Mode
// userconfig.DefaultConfig.Mode = userconfig.ModeTasks
// defer func() {
// userconfig.DefaultConfig.Mode = mode
// }()
//
// userFS, err := fs.NewFS("/", afero.NewMemMapFs())
// r.NoError(err)
// err = userFS.CreateSystemDirs()
// r.NoError(err)
//
// tgram := tg.NewFakeTG()
//
// bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
// err = bot.Reply(tg.NewUpd(-1, "New task/"))
// r.NoError(err)
//
// err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"c5e7dfaf771", inboxMsgHash(t, userFS, 0)})))
// r.NoError(err)
//
// tasks, err := bot.fs.FilesAndDirs("home")
// r.NoError(err)
//
// r.Len(tasks, 1)
// r.Equal("New task.md", tasks[0].Name)
//
// content, err := bot.fs.Read("home", "New task.md")
// r.NoError(err)
// r.Equal("New task/", content)
//
// err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("home", nil)))
// r.NoError(err)
//
// err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("add_item", []string{"db0a776589b", inboxMsgHash(t, userFS, 0)})))
// r.NoError(err)
//
// r.Equal("<b>1</b> items"+wideSpacer, tgram.LastSentText)
// r.Equal(tg.NewKeyboard([]tg.Row{
// tg.NewBtn("👀 New task/", tg.NewCmd("task", []string{"home", "24e70ffbf48"})),
// },
// ), tgram.LastSentKeyboard)
// }
func TestAddMultilineNoteToNotes(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, "New task\nContent"))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
tasks, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(tasks, 1)
r.Equal("New task.md", tasks[0].Name)
r.True(tasks[0].IsMultiline)
content, err := bot.fs.Read("notes", "New task.md")
r.NoError(err)
r.Equal("Content", content)
}
func TestAddNoteWithSpecCharsToNotes(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
userFS.CreateSystemDirs()
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, "New task\nUrl! https://g.com (Also_text] ##header\n-item1\n-item2\n1+1=2"))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
tasks, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(tasks, 1)
r.Equal("New task.md", tasks[0].Name)
r.True(tasks[0].IsMultiline)
content, err := bot.fs.Read("notes", "New task.md")
r.NoError(err)
r.Equal("Url! https://g.com (Also_text] ##header\n-item1\n-item2\n1+1=2", content)
}
func TestAddNoteWithOnlyWhitespace(t *testing.T) {
// Test adding a task that contains only whitespace characters
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
err = bot.Reply(tg.NewUpd(-1, " \t\n"))
r.EqualError(err, "save: empty message")
tasks, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(tasks, 0)
}
func TestAddNoteWithLeadingAndTrailingSpaces(t *testing.T) {
// Test adding a task with leading and trailing spaces in the name
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
userFS.CreateSystemDirs()
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, " Task with spaces "))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
tasks, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(tasks, 1)
r.Equal("Task with spaces.md", tasks[0].Name)
}
func TestShowEmptyTodayList(t *testing.T) {
// Test displaying today's tasks when there are none
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("home", nil)))
r.NoError(err)
r.Equal("🌴 Nothing here yet - send me something!", tgram.LastSentText)
}
func TestSaveFromTextMsgWithUnicodeCharacters(t *testing.T) {
// Test handling of text messages containing Unicode characters
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
unicodeText := "测试含有Unicode字符的文本🚀🌟"
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, unicodeText))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
tasks, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(tasks, 1)
r.Equal("测试含有Unicode字符的文本🚀🌟.md", tasks[0].Name)
}
func TestSaveFromEmptyTextMsg(t *testing.T) {
// Test handling of empty text messages
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, ""))
r.EqualError(err, "save: empty message")
tasks, err := bot.fs.FilesAndDirs("home")
r.NoError(err)
r.Len(tasks, 0)
}
func TestSaveFromRegularReply(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("home", "Existing file.md", "Existing content")
r.NoError(err)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
database.SetHashOrPathByMsgID(255, "home/Existing file.md")
bot := NewBot(-1, tgram, userFS, database, fakeConfig())
upd := tg.NewUpd(-1, "Line")
upd.ReplyToMessageID = 255
err = bot.Reply(upd)
r.NoError(err)
files, err := bot.fs.FilesAndDirs("home")
r.NoError(err)
r.Len(files, 1)
content, err := bot.fs.Read("home", "Existing file.md")
r.NoError(err)
r.Equal("Existing content\n\nLine\n", content)
}
func TestSaveFromPhotoWithCaption(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
upd.PhotoCaption = "Caption"
err = bot.Reply(upd)
r.NoError(err)
content, err := userFS.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 11 August, Sunday\n- [ ] `09:54` ![](media/tg_PHOTO_ID)\nCaption\n", content)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
files, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(files, 1)
r.Equal("Caption.md", files[0].Name)
r.True(files[0].IsMultiline)
content, err = bot.fs.Read("notes", "Caption.md")
r.NoError(err)
r.Equal("![](media/tg_PHOTO_ID)\nCaption", content)
}
func TestSaveFromPhotoWithLongCaption(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
upd.PhotoCaption = strings.Repeat("a", 34)
err = bot.Reply(upd)
r.NoError(err)
content, err := userFS.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 11 August, Sunday\n- [ ] `09:54` ![](media/tg_PHOTO_ID)\nAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", content)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
content, err = bot.fs.Read("notes", "A"+strings.Repeat("a", 33)+".md")
r.NoError(err)
r.Equal(fmt.Sprintf("![](media/tg_PHOTO_ID)\nA%s", strings.Repeat("a", 33)), content)
}
func TestSaveFromPhotoWithSanitizedCaption(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
upd.PhotoCaption = "Caption/"
err = bot.Reply(upd)
r.NoError(err)
content, err := userFS.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 11 August, Sunday\n- [ ] `09:54` ![](media/tg_PHOTO_ID)\nCaption/\n", content)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
files, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(files, 1)
r.Equal("Caption.md", files[0].Name)
r.True(files[0].IsMultiline)
content, err = bot.fs.Read("notes", "Caption.md")
r.NoError(err)
r.Equal("![](media/tg_PHOTO_ID)\nCaption/", content)
}
func TestSaveFromPhotoWithoutCaption(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
userFS.CreateDirsIfNotExist("notes")
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
err = bot.Reply(upd)
r.NoError(err)
content, err := userFS.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 11 August, Sunday\n- [ ] `09:54` ![](media/tg_PHOTO_ID)\n", content)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
files, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(files, 1)
// Be aware that it's not regular
r.Equal("Img 11.08.24 0954.md", files[0].Name)
r.True(files[0].IsMultiline)
// Be aware that it's not regular
content, err = bot.fs.Read("notes", "Img 11.08.24 0954.md")
r.NoError(err)
r.Equal("![](media/tg_PHOTO_ID)", content)
}
func TestSaveFromReplyPhotoWithCaption(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("home", "Existing file.md", "Existing content")
r.NoError(err)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
database.SetHashOrPathByMsgID(255, "home/Existing file.md")
bot := NewBot(-1, tgram, userFS, database, fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
upd.PhotoCaption = "Caption"
upd.ReplyToMessageID = 255
err = bot.Reply(upd)
r.NoError(err)
content, err := bot.fs.Read("home", "Existing file.md")
r.NoError(err)
r.Equal("Existing content\n\n![](media/tg_PHOTO_ID)\nCaption\n", content)
}
func TestAddTaskToLater(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.CreateSystemDirs())
r.NoError(userFS.Write(fs.DirUserRoot, fs.ChatFilename, "- [ ] First task"))
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
r.NoError(bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv_later", []string{chatBlockHash("- [ ] First task")}))))
todayMD, err := userFS.Read(fs.DirUserRoot, fs.ChatFilename)
r.NoError(err)
r.NotContains(todayMD, "First task")
laterMD, err := userFS.Read(fs.DirUserRoot, fs.LaterFilename)
r.NoError(err)
r.Contains(laterMD, "- [ ] First task")
}
func completeTask(t *testing.T, initial, hashed string) string {
t.Helper()
savedNow := now
t.Cleanup(func() { now = savedNow })
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write("/", "Chat.md", initial))
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), fakeConfig())
r.NoError(bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("c", []string{chatBlockHash(hashed)}))))
got, err := userFS.Read("/", "Chat.md")
r.NoError(err)
return got
}
func TestCompleteTask_Incomplete_NoTimestamp_NoHeader(t *testing.T) {
got := completeTask(t, "- [ ] New task", "- [ ] New task")
require.Equal(t, "- [x] New task", got)
}
func TestCompleteTask_Incomplete_WithTimestamp_NoHeader(t *testing.T) {
got := completeTask(t, "- [ ] `00:00` New task", "- [ ] `00:00` New task")
require.Equal(t, "- [x] `00:00` New task", got)
}
func TestCompleteTask_Incomplete_NoTimestamp_WithHeader(t *testing.T) {
got := completeTask(t,
"#### 1 January, Thursday\n- [ ] New task",
"- [ ] New task",
)
require.Equal(t, "#### 1 January, Thursday\n- [x] New task", got)
}
func TestCompleteTask_Incomplete_WithTimestamp_WithHeader(t *testing.T) {
got := completeTask(t,
"#### 1 January, Thursday\n- [ ] `09:30` New task",
"- [ ] `09:30` New task",
)
require.Equal(t, "#### 1 January, Thursday\n- [x] `09:30` New task", got)
}
// Both todayBlockHash and CompleteChecklistItem hash the first line after
// the marker, so the box must still flip even when the block has
// continuation lines attached.
func TestCompleteTask_Multiline_FlipsBox(t *testing.T) {
block := "- [ ] `00:00` First task\nsome continuation"
got := completeTask(t, block, block)
require.Contains(t, got, "- [x] `00:00` First task")
require.NotContains(t, got, "- [ ] `00:00` First task")
}
func TestCompleteTask_AlreadyCompleted_IsNoOp(t *testing.T) {
got := completeTask(t, "- [x] Done task", "- [x] Done task")
require.Equal(t, "- [x] Done task", got)
}
func TestCompleteTask_Pomodoro_AddsSchedule(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
cfg := fakeConfig()
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), cfg)
r.NoError(bot.togglePomodoro(nil))
pomodoroBlock := "- [ ] " + fs.PomodoroTask
before := time.Now().Unix()
r.NoError(bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("c", []string{chatBlockHash(pomodoroBlock)}))))
after := time.Now().Unix()
todayMD, err := userFS.Read("/", fs.ChatFilename)
r.NoError(err)
r.Contains(todayMD, "- [x] "+fs.PomodoroTask)
schedules, err := cfg.Schedules()
r.NoError(err)
r.Len(schedules, 1)
r.Equal(fs.PomodoroTask, schedules[0].Filename)
durationSec := int64(cfg.PomodoroDuration().Seconds())
r.GreaterOrEqual(schedules[0].ScheduledAt, before+durationSec)
r.LessOrEqual(schedules[0].ScheduledAt, after+durationSec)
}
func TestCompleteTask_NonPomodoro_DoesNotSchedule(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write("/", fs.ChatFilename, "- [ ] Regular task"))
cfg := fakeConfig()
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), cfg)
r.NoError(bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("c", []string{chatBlockHash("- [ ] Regular task")}))))
schedules, err := cfg.Schedules()
r.NoError(err)
r.Empty(schedules)
}
func TestCompleteChecklistItem_Pomodoro_DoesNotSchedule(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write("/", fs.ChatFilename, "- [ ] "+fs.PomodoroTask))
cfg := fakeConfig()
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), cfg)
r.NoError(bot.completeChecklistItem([]string{fs.Hash(fs.ChatFilename), fs.Hash(fs.PomodoroTask)}))
schedules, err := cfg.Schedules()
r.NoError(err)
r.Empty(schedules)
}
func TestToday(t *testing.T) {
r := require.New(t)
savedCtime := fs.Ctime
defer func() {
fs.Ctime = savedCtime
}()
fs.Ctime = func(fi os.FileInfo) int64 {
return 0
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Chat.md", "- [ ] `00:00` First task\n- [ ] `00:00` Second task")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("home", nil)))
r.NoError(err)
r.Equal("<b>2</b> items"+wideSpacer, tgram.LastSentText)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("First task", tg.NewCmd("c", []string{"060f6b7c9c8"})),
tg.NewBtn("🥈 Second task", tg.NewCmd("c", []string{"083d6c37d07"})),
},
), tgram.LastSentKeyboard)
}
func TestTodayQuickMenuFilled(t *testing.T) {
savedCtime := fs.Ctime
defer func() {
fs.Ctime = savedCtime
}()
fs.Ctime = func(fi os.FileInfo) int64 {
return 0
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
cfg := fakeConfig()
_ = cfg.AddQuickCmd("files")
_ = cfg.AddQuickCmd("checklists")
_ = cfg.AddQuickCmd("postpone")
bot, tgram, r := makeBot(t, cfg)
err := bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("home", nil)))
r.NoError(err)
r.Equal("<b>1</b> item"+wideSpacer, tgram.LastSentText)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("First task", tg.NewCmd("c", []string{"832a6c2a713"})),
tg.NewRow(
tg.NewBtn("📄", tg.NewCmd("files", nil)),
tg.NewBtn("☑️", tg.NewCmd("checklists", nil)),
tg.NewBtn("🦥", tg.NewCmd("postpone", nil)),
),
},
), tgram.LastSentKeyboard)
}
func TestTodayMultilineTaskShownAsLong(t *testing.T) {
r := require.New(t)
savedCtime := fs.Ctime
defer func() { fs.Ctime = savedCtime }()
fs.Ctime = func(fi os.FileInfo) int64 { return 0 }
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() { userconfig.DefaultConfig.Mode = mode }()
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time { return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) }
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Chat.md", "- [ ] `00:00` First task\nsome continuation")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("home", nil)))
r.NoError(err)
hash := inboxMsgHash(t, userFS, 0)
r.Equal("<b>1</b> item"+wideSpacer, tgram.LastSentText)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("👀 First task", tg.NewCmd(CmdShowLongItem, []string{hash})),
}), tgram.LastSentKeyboard)
}
func TestTodayMixedSingleAndMultilineTasks(t *testing.T) {
r := require.New(t)
savedCtime := fs.Ctime
defer func() { fs.Ctime = savedCtime }()
fs.Ctime = func(fi os.FileInfo) int64 { return 0 }
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() { userconfig.DefaultConfig.Mode = mode }()
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time { return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) }
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
content := "- [ ] `00:00` First task\n" +
"- [ ] `00:00` Second task\nextra detail line"
err = userFS.Write("/", "Chat.md", content)
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("home", nil)))
r.NoError(err)
first := inboxMsgHash(t, userFS, 0)
second := inboxMsgHash(t, userFS, 1)
r.Equal("<b>2</b> items"+wideSpacer, tgram.LastSentText)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("First task", tg.NewCmd(CmdComplete, []string{first})),
tg.NewBtn("👀 Second task", tg.NewCmd(CmdShowLongItem, []string{second})),
}), tgram.LastSentKeyboard)
}
// TODO Chat.md
//func TestTodayWithMultilineTasks(t *testing.T) {
// r := require.New(t)
//
// savedCtime := fs.Ctime
// defer func() {
// fs.Ctime = savedCtime
// }()
// fs.Ctime = func(fi os.FileInfo) int64 {
// return 0
// }
//
// savedNow := now
// defer func() {
// now = savedNow
// }()
// now = func() time.Time {
// return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
// }
//
// mode := userconfig.DefaultConfig.Mode
// userconfig.DefaultConfig.Mode = userconfig.ModeTasks
// defer func() {
// userconfig.DefaultConfig.Mode = mode
// }()
//
// userFS, err := fs.NewFS("/", afero.NewMemMapFs())
// r.NoError(err)
// err = userFS.Write("home", "First task.md", "content")
// r.NoError(err)
// err = userFS.Write("home", "Second task.md", "")
// r.NoError(err)
//
// tgram := tg.NewFakeTG()
//
// upd := tg.NewUpdCmd(-1, tg.NewCmd("home", nil))
// bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
// err = bot.Reply(upd)
// r.NoError(err)
//
// r.Equal("<b>2</b> items"+wideSpacer, tgram.LastSentText)
// r.Equal(tg.NewKeyboard([]tg.Row{
// tg.NewBtn("👀 First task", tg.NewCmd("task", []string{"home", "0824149b387"})),
// tg.NewBtn("🥈 Second task", tg.NewCmd("c", []string{"home", "4eb62f93b3e"})),
// },
// ), tgram.LastSentKeyboard)
//}
func TestFiles(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("", "Doc1.md", "")
r.NoError(err)
err = userFS.Write("", "Doc2.md", "")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("files", nil)))
r.NoError(err)
r.Equal("📄 Your files:"+wideSpacer, tgram.SentTexts[0])
r.Equal(tg.NewKeyboard([]tg.Row{
[]tg.Btn{
tg.NewBtn("Doc1", tg.NewCmd("file", []string{fs.DirUserRoot, "c1253521ac7"})),
tg.NewBtn("Doc2", tg.NewCmd("file", []string{fs.DirUserRoot, "64572c3093f"})),
},
[]tg.Btn{
tg.NewBtn("🔎 Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewBtn("🏠 Home", tg.NewCmd("home", nil)),
},
}), tgram.LastSentKeyboard)
}
func TestChecklists(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Checklist1_.md", "")
r.NoError(err)
err = userFS.Write("/", "Checklist2_.md", "")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("checklists", nil)))
r.NoError(err)
r.Equal("☑️ Checklists", tgram.LastSentText)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("Checklist1", tg.NewCmd("checklist", []string{"3755fd335a8"})),
tg.NewBtn("Checklist2", tg.NewCmd("checklist", []string{"c54b67d1d3d"})),
tg.NewBtn("🏠 Home", tg.NewCmd("home", nil)),
},
), tgram.LastSentKeyboard)
}
func TestAddSingleItemToChecklist(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.MakeDir("-checklist1-")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, "Item"))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv_to_chk", []string{inboxMsgHash(t, userFS, 0), "-checklist1-"})))
r.NoError(err)
files, err := userFS.FilesAndDirs("-checklist1-")
r.NoError(err)
r.Len(files, 1)
r.Equal("Item.md", files[0].Name)
files, err = userFS.FilesAndDirs("home")
r.NoError(err)
r.Len(files, 0)
}
func TestAddMultipleItemsToChecklist(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.MakeDir("-checklist1-")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, "Item\nItem2\nItem3"))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv_to_chk", []string{inboxMsgHash(t, userFS, 0), "-checklist1-"})))
r.NoError(err)
files, err := userFS.FilesAndDirs("-checklist1-")
r.NoError(err)
r.Len(files, 3)
r.ElementsMatch([]string{"Item.md", "Item2.md", "Item3.md"}, []string{files[0].Name, files[1].Name, files[2].Name})
}
func TestShowChecklist(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Checklist1_.txt", "- [ ] Item")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("checklist", []string{"Checklist1_.txt"})))
r.NoError(err)
r.Equal("Checklist1"+wideSpacer, tgram.LastSentText)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("Item", tg.NewCmd("check_item", []string{"Checklist1_.txt", "7d74f3b92b1"})),
tg.NewBtn("🏠 Home", tg.NewCmd("home", nil)),
},
), tgram.LastSentKeyboard)
}
func TestCompleteItemInChecklist(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.MakeDir("_checklist1_")
r.NoError(err)
err = userFS.Write("_checklist1_", "Item.md", "")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("check_comp", []string{"49d872c025c", "7b72407ca70"})))
r.NoError(err)
r.Equal("Checklist1"+wideSpacer, tgram.LastSentText)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("🏠 Home", tg.NewCmd("home", nil)),
},
), tgram.LastSentKeyboard)
items, err := bot.fs.FilesAndDirs("_checklist1_")
r.NoError(err)
r.Empty(items)
items, err = bot.fs.FilesAndDirs("archive")
r.NoError(err)
r.Len(items, 1)
r.Equal("Item.md", items[0].Name)
}
func TestBotTodayLabelIcons(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
b := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
// Pomodoro is the only task in today
r.Nil(b.togglePomodoro(nil))
label := b.homeLabel()
r.Contains(label, "🌴")
r.Contains(label, "🍅")
// Pomodoro and another task in today
todayMD, err := b.fs.Read("", "Chat.md")
r.NoError(err)
r.Nil(b.fs.Write("", "Chat.md", txt.AddChecklistItem(todayMD, "Task", false)))
label = b.homeLabel(1)
r.NotContains(label, "🌴")
r.Contains(label, "🍅")
// No pomodoro, but there is another task in today
r.Nil(b.completeChecklistItem([]string{fs.Hash(fs.ChatFilename), fs.Hash(fs.PomodoroTask)}))
label = b.homeLabel(1)
r.NotContains(label, "🌴")
r.NotContains(label, "🍅")
// No pomodoro, no other tasks in today
r.Nil(b.completeChecklistItem([]string{fs.Hash(fs.ChatFilename), fs.Hash("Task")}))
label = b.homeLabel()
r.NoError(err)
r.Contains(label, "🌴")
r.NotContains(label, "🍅")
}
func makeBot(t *testing.T, cfg *userconfig.Config) (*Bot, *tg.FakeTG, *require.Assertions) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Chat.md", "- [ ] First task")
r.NoError(err)
err = userFS.Write("/", "Later.md", "- [ ] Second task")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
return bot, tgram, r
}
func TestSettingsMainPanel(t *testing.T) {
bot, tgram, r := makeBot(t, fakeConfig())
err := bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("settings", nil)))
r.NoError(err)
r.Equal("Settings:", tgram.LastSentText)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("🧠 Full mode", tg.NewCmd("full", nil)),
//tg.NewBtn("💬 Inbox mode", tg.NewCmd("chat", nil)),
tg.NewBtn("📌 Notes mode", tg.NewCmd("notes_only", nil)),
tg.NewBtn("✅ Tasks mode", tg.NewCmd("tasks_only", nil)),
//tg.NewBtn("💚 Journal mode", tg.NewCmd("journal_only", nil)),
tg.NewBtn("-", tg.NewCmd("nothing", nil)),
tg.NewBtn("⚡️ Quick buttons", tg.NewCmd("c_quick_btns", nil)),
tg.NewBtn("➡️ Move to buttons", tg.NewCmd("c_move_btns", nil)),
tg.NewBtn("🌎 Timezone", tg.NewCmd("timezone", nil)),
tg.NewBtn("🏠 Home", tg.NewCmd("home", nil)),
},
), tgram.LastSentKeyboard)
}
// Quick Panel Data-driven tests
var (
btnFilesDel = tg.NewBtn("📄 Files ", tg.NewCmd("del_quick", []string{"files"}))
btnChecklistsDel = tg.NewBtn("☑️ Checklists ", tg.NewCmd("del_quick", []string{"checklists"}))
btnPostponeDel = tg.NewBtn("🦥 Postpone ", tg.NewCmd("del_quick", []string{"postpone"}))
)
var (
delimiter = tg.NewBtn("-", tg.NewCmd("nothing", nil))
homeBtn = tg.NewBtn("🏠 Home", tg.NewCmd("home", nil))
)
var (
btnLater = tg.NewBtn("⏳ Later ", tg.NewCmd("add_quick", []string{"later"}))
btnSearch = tg.NewBtn("🔎 Search ", tg.NewCmd("add_quick", []string{"search"}))
btnFilesAdd = tg.NewBtn("📄 Files ", tg.NewCmd("add_quick", []string{"files"}))
btnChecklistsAdd = tg.NewBtn("☑️ Checklists ", tg.NewCmd("add_quick", []string{"checklists"}))
btnPostponeAdd = tg.NewBtn("🦥 Postpone ", tg.NewCmd("add_quick", []string{"postpone"}))
btnReadChecklist = tg.NewBtn("📚 Read ", tg.NewCmd("add_quick", []string{"read"}))
btnWatchChecklist = tg.NewBtn("📺 Watch ", tg.NewCmd("add_quick", []string{"watch"}))
btnShopChecklist = tg.NewBtn("🛒 Shop ", tg.NewCmd("add_quick", []string{"shop"}))
btnSchedule = tg.NewBtn("🗓 Schedule ", tg.NewCmd("add_quick", []string{"schedule"}))
btnHabits = tg.NewBtn("🌱 Habits ", tg.NewCmd("add_quick", []string{"habits"}))
btnRandom = tg.NewBtn("🎲 Random ", tg.NewCmd("add_quick", []string{"random_note"}))
)
func TestConfigureQP_Empty_Default(t *testing.T) {
RunQuickPanelTc(PrefTableTestCase{
[]string{""},
tg.NewUpdCmd(-1, tg.NewCmd("c_quick_btns", nil)),
[]tg.Row{
delimiter,
btnLater,
btnSearch,
btnFilesAdd,
btnChecklistsAdd,
btnPostponeAdd,
btnReadChecklist,
btnWatchChecklist,
btnShopChecklist,
btnSchedule,
btnHabits,
btnRandom,
homeBtn,
},
}, t)
}
func TestConfigureQP_Empty_AddFiles(t *testing.T) {
RunQuickPanelTc(PrefTableTestCase{
[]string{""},
tg.NewUpdCmd(-1, tg.NewCmd("add_quick", []string{"files"})),
[]tg.Row{
btnFilesDel,
delimiter,
btnLater,
btnSearch,
btnChecklistsAdd,
btnPostponeAdd,
btnReadChecklist,
btnWatchChecklist,
btnShopChecklist,
btnSchedule,
btnHabits,
btnRandom,
homeBtn,
},
}, t)
}
func TestConfigureQP_Doc_AddCheckList(t *testing.T) {
RunQuickPanelTc(PrefTableTestCase{
[]string{"files"},
tg.NewUpdCmd(-1, tg.NewCmd("add_quick", []string{"checklists"})),
[]tg.Row{
btnFilesDel,
btnChecklistsDel,
delimiter,
btnLater,
btnSearch,
btnPostponeAdd,
btnReadChecklist,
btnWatchChecklist,
btnShopChecklist,
btnSchedule,
btnHabits,
btnRandom,
homeBtn,
},
}, t)
}
func TestConfigureQP_DocChecklists_AddPostpone(t *testing.T) {
RunQuickPanelTc(PrefTableTestCase{
[]string{"files", "checklists"},
tg.NewUpdCmd(-1, tg.NewCmd("add_quick", []string{"postpone"})),
[]tg.Row{
btnFilesDel,
btnChecklistsDel,
btnPostponeDel,
delimiter,
btnLater,
btnSearch,
btnReadChecklist,
btnWatchChecklist,
btnShopChecklist,
btnSchedule,
btnHabits,
btnRandom,
homeBtn,
},
}, t)
}
func TestConfigureQP_DocChecklistsPostpone_Show(t *testing.T) {
RunQuickPanelTc(PrefTableTestCase{
[]string{"files", "checklists", "postpone"},
tg.NewUpdCmd(-1, tg.NewCmd("c_quick_btns", nil)),
[]tg.Row{
btnFilesDel,
btnChecklistsDel,
btnPostponeDel,
delimiter,
btnLater,
btnSearch,
btnReadChecklist,
btnWatchChecklist,
btnShopChecklist,
btnSchedule,
btnHabits,
btnRandom,
homeBtn,
},
}, t)
}
func TestConfigureQP_DocChecklistsPostpone_DelChecklists(t *testing.T) {
RunQuickPanelTc(PrefTableTestCase{
[]string{"files", "checklists", "postpone"},
tg.NewUpdCmd(-1, tg.NewCmd("del_quick", []string{"checklists"})),
[]tg.Row{
btnFilesDel,
btnPostponeDel,
delimiter,
btnLater,
btnSearch,
btnChecklistsAdd,
btnReadChecklist,
btnWatchChecklist,
btnShopChecklist,
btnSchedule,
btnHabits,
btnRandom,
homeBtn,
},
}, t)
}
func TestConfigureQP_DocPostpone_DelDoc(t *testing.T) {
RunQuickPanelTc(PrefTableTestCase{
[]string{"files", "postpone"},
tg.NewUpdCmd(-1, tg.NewCmd("del_quick", []string{"files"})),
[]tg.Row{
btnPostponeDel,
delimiter,
btnLater,
btnSearch,
btnFilesAdd,
btnChecklistsAdd,
btnReadChecklist,
btnWatchChecklist,
btnShopChecklist,
btnSchedule,
btnHabits,
btnRandom,
homeBtn,
},
}, t)
}
func TestConfigureQP_Postpone_DelPostpone(t *testing.T) {
RunQuickPanelTc(PrefTableTestCase{
[]string{"postpone"},
tg.NewUpdCmd(-1, tg.NewCmd("del_quick", []string{"postpone"})),
[]tg.Row{
delimiter,
btnLater,
btnSearch,
btnFilesAdd,
btnChecklistsAdd,
btnPostponeAdd,
btnReadChecklist,
btnWatchChecklist,
btnShopChecklist,
btnSchedule,
btnHabits,
btnRandom,
homeBtn,
},
}, t)
}
func TestConfigureQP_Empty_AddUnknown(t *testing.T) {
RunquickpaneltcError(PrefTableTestCase{
[]string{""},
tg.NewUpdCmd(-1, tg.NewCmd("add_quick", []string{"wrong"})),
[]tg.Row{},
}, "unknown command: wrong", t)
}
func RunQuickPanelTc(tc PrefTableTestCase, t *testing.T) {
cnf := fakeConfig()
for _, cmd := range tc.existingCmds {
_ = cnf.AddQuickCmd(cmd)
}
bot, tgram, r := makeBot(t, cnf)
err := bot.Reply(tc.updToAnswer)
r.NoError(err)
r.Equal("Configure quick buttons ( = add to quick buttons, = to remove from quick buttons):", tgram.LastSentText)
r.Equal(tg.NewKeyboard(tc.buttons), tgram.LastSentKeyboard)
}
func RunquickpaneltcError(tc PrefTableTestCase, expectedErr string, t *testing.T) {
cnf := fakeConfig()
for _, cmd := range tc.existingCmds {
_ = cnf.AddQuickCmd(cmd)
}
bot, _, r := makeBot(t, cnf)
actualErr := bot.Reply(tc.updToAnswer)
r.EqualError(actualErr, expectedErr)
}
type PrefTableTestCase struct {
existingCmds []string
updToAnswer *tg.Upd
buttons []tg.Row
}
func TestShowToFileNoDirs(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Note.md", "")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
const h = "fakehash"
err = bot.showMoveToFileOrDir([]string{h})
r.NoError(err)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewRow(tg.NewBtn("Note", tg.NewCmd("mf", []string{"345fb", h}))),
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{h}))),
},
), tgram.LastSentKeyboard)
}
func TestShowMoveToFile(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("notes", "Note.md", "")
r.NoError(err)
err = userFS.MakeDir("dir")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
const h = "fakehash"
err = bot.showMoveToFileOrDir([]string{h})
r.NoError(err)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(
tg.NewBtn("🗂️ Dir", tg.NewCmd("mv", []string{"73600", h})),
tg.NewBtn("🗂️ Notes", tg.NewCmd("mv", []string{"4358b", h})),
tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{h})),
),
}), tgram.LastSentKeyboard)
}
func TestShow(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.showHTML("text", nil)
r.NoError(err)
r.Equal("text", tgram.LastSentText)
}
func TestShowMDLongMessage(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.showMD(strings.Repeat("a", 4096)+"b", nil)
r.NoError(err)
r.Len(tgram.SentTexts, 2)
r.Equal("b", tgram.LastSentText)
}
// When utf8.RuneCountInString(textChunk) == 4096, tg sends the message (len(textChunk) => 7003)
// if I have 4095 chars and add 🟢, we have 4096 chars, and it is ok
// if I have 4095 chars and add ⚪️, we have 4097 chars, and we fail, so tg doesn't operate on glyph clusters
func TestShowMDLongMessageWithColoredEmojis(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.showMD(strings.Repeat("a", 4095)+"🟢", nil)
r.NoError(err)
r.Len(tgram.SentTexts, 1)
}
func TestShowMDLongMessageWithColoredEmoji(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.showMD(strings.Repeat("a", 4095)+"⚪️", nil)
r.NoError(err)
r.Len(tgram.SentTexts, 2)
}
func TestShowMDLongMessageSplitByNewLine(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.showMD(strings.Repeat("a", 4094)+"\nabc", nil)
r.NoError(err)
r.Len(tgram.SentTexts, 2)
r.Equal("abc", tgram.LastSentText)
}
func TestShowMDLongMessageAttachKeyboardToTheLast(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.showMD(strings.Repeat("a", 4094)+"\nabc", tg.NewKeyboard([]tg.Row{tg.NewBtn("btn", tg.NewCmd("cmd", nil))}))
r.NoError(err)
r.Len(tgram.SentTexts, 2)
r.Equal("abc", tgram.LastSentText)
r.NotNil(tgram.LastSentKeyboard)
r.Len(tgram.LastSentKeyboard.Btns, 1)
}
func TestMoveToExistingFile(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Chat.md", "#### 27 June, Friday\n`12:00` New message")
r.NoError(err)
err = userFS.Write("", "Existing file.md", "")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpdCmd(-1, tg.NewCmd("mf", []string{"1c8f819d075", inboxMsgHash(t, userFS, 0)}))
err = bot.Reply(upd)
r.NoError(err)
content, err := userFS.Read("", "Existing file.md")
r.NoError(err)
r.Equal("#### 11 August 2024, Sunday\nNew message", content)
}
func TestMoveToExistingFileExistingRecord(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Chat.md", "#### 27 June, Friday\n`12:00` New message")
r.NoError(err)
err = userFS.Write("", "Existing file.md", "### 11.08.2024 Sunday\nContent")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpdCmd(-1, tg.NewCmd("mf", []string{"1c8f819d075", inboxMsgHash(t, userFS, 0)}))
err = bot.Reply(upd)
r.NoError(err)
content, err := userFS.Read("", "Existing file.md")
r.NoError(err)
r.Equal("#### 11 August 2024, Sunday\nNew message\n\n### 11.08.2024 Sunday\nContent", content)
}
func TestShowMoveTo(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("", "file", "")
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.Reply(tg.NewUpd(-1, "New task\nContent"))
r.NoError(err)
r.Equal("Saved!", tgram.SentTexts[0])
h := inboxMsgHash(t, userFS, 0)
kb := tg.NewKeyboard([]tg.Row{
[]tg.Btn{
{Name: "🌚 To tmrw", Cmd: tg.Cmd{Name: "sc_tmrw", Params: []string{h}, Type: "cmd"}},
{Name: "⏳ To later", Cmd: tg.Cmd{Name: "mv_later", Params: []string{h}, Type: "cmd"}},
{Name: "📆 To a day", Cmd: tg.Cmd{Name: "sc_day", Params: []string{h}, Type: "cmd"}},
},
[]tg.Btn{
{Name: "📄 To File", Cmd: tg.Cmd{Name: "to_file", Params: []string{h}, Type: "cmd"}},
{Name: "💚 To Journal", Cmd: tg.Cmd{Name: "mv_to_journal", Params: []string{h}, Type: "cmd"}},
},
},
)
r.Equal(kb, tgram.LastSentKeyboard)
}
func TestShowMoveFromTodayAndInbox(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time {
return time.Date(2025, 6, 29, 9, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write(
fs.DirUserRoot, fs.ChatFilename,
"#### 29 June, Sunday\n- [ ] `09:00` Inbox body\n- [x] `09:05` Completed body\n",
))
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("move", nil)))
r.NoError(err)
inboxHash := inboxMsgHash(t, userFS, 0)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("💬 Inbox body", tg.NewCmd("s_move", []string{inboxHash})),
tg.NewRow(
tg.NewBtn("Rename", tg.NewCmd("rename", []string{})),
tg.NewBtn("OK", tg.NewCmd("home", []string{})),
),
}), tgram.LastSentKeyboard)
}
func TestMoveFromTodayAndInbox_ToLater(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time {
return time.Date(2025, 6, 29, 9, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write(
fs.DirUserRoot, fs.ChatFilename,
"- [ ] Today task\n#### 29 June, Sunday\n- [ ] `09:00` Inbox body",
))
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
// Click the inbox button → moveToLater ships the entry to Later.md.
// Block 0 is the top-level "Today task"; the inbox body is block 1.
inboxHash := inboxMsgHash(t, userFS, 1)
r.NoError(bot.moveToLater([]string{inboxHash}))
inboxMD, _ := userFS.Read(fs.DirUserRoot, fs.ChatFilename)
r.NotContains(inboxMD, "Inbox body")
laterMD, _ := userFS.Read(fs.DirUserRoot, fs.LaterFilename)
r.Contains(laterMD, "- [ ] Inbox body")
}
func TestShowPostpone_WithInbox(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time {
return time.Date(2025, 6, 29, 9, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write(
fs.DirUserRoot, fs.ChatFilename,
"#### 29 June, Sunday\n- [ ] `09:00` Inbox body\n- [x] `09:05` Completed body\n",
))
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("postpone", nil)))
r.NoError(err)
inboxHash := inboxMsgHash(t, userFS, 0)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("💬 Inbox body", tg.NewCmd("post", []string{inboxHash})),
tg.NewRow(
tg.NewBtn("Rename", tg.NewCmd("rename", []string{})),
tg.NewBtn("OK", tg.NewCmd("home", []string{})),
),
}), tgram.LastSentKeyboard)
}
func TestPostponeTodayTask(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write(fs.DirUserRoot, fs.ChatFilename, "- [ ] Today task"))
r.NoError(userFS.Write(fs.DirUserRoot, fs.LaterFilename, ""))
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.postpone([]string{fs.Hash("Today task")})
r.NoError(err)
todayMD, _ := userFS.Read(fs.DirUserRoot, fs.ChatFilename)
r.NotContains(todayMD, "Today task")
laterMD, _ := userFS.Read(fs.DirUserRoot, fs.LaterFilename)
r.Contains(laterMD, "- [ ] Today task")
}
func TestPostponeInboxEntry(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time {
return time.Date(2025, 6, 29, 9, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write(
fs.DirUserRoot, fs.ChatFilename,
"#### 29 June, Sunday\n- [ ] `09:00` Inbox body",
))
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
h := inboxMsgHash(t, userFS, 0)
err = bot.postpone([]string{h})
r.NoError(err)
inboxMD, _ := userFS.Read(fs.DirUserRoot, fs.ChatFilename)
r.NotContains(inboxMD, "Inbox body")
laterMD, _ := userFS.Read(fs.DirUserRoot, fs.LaterFilename)
r.Contains(laterMD, "- [ ] Inbox body")
}
func TestShowRename_WithInbox(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time {
return time.Date(2025, 6, 29, 9, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write(
fs.DirUserRoot, fs.ChatFilename,
"#### 29 June, Sunday\n- [ ] `09:00` Inbox body\n- [x] `09:05` Completed body\n",
))
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
r.NoError(bot.showRename(nil))
inboxHash := inboxMsgHash(t, userFS, 0)
r.Equal(tg.NewKeyboard([]tg.Row{
tg.NewBtn("💬 Inbox body", tg.NewCmd("rename_file", []string{fs.ChatFilename, inboxHash})),
tg.NewBtn("🏠 Home", tg.NewCmd("home", nil)),
}), tgram.LastSentKeyboard)
}
func TestRenameInboxEntry(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time {
return time.Date(2025, 6, 29, 9, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write(
fs.DirUserRoot, fs.ChatFilename,
"#### 29 June, Sunday\n- [ ] `09:00` Inbox body\n- [ ] `09:05` Second entry",
))
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
h := inboxMsgHash(t, userFS, 0)
r.NoError(bot.rename([]string{fs.ChatFilename, h, "Renamed body"}))
inboxMD, _ := userFS.Read(fs.DirUserRoot, fs.ChatFilename)
// Timestamp and marker preserved, body replaced.
r.Contains(inboxMD, "- [ ] `09:00` Renamed body")
r.NotContains(inboxMD, "Inbox body")
// Untouched entry stays intact.
r.Contains(inboxMD, "- [ ] `09:05` Second entry")
}
func TestRenameInboxEntry_PreservesCompletedMarker(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
r.NoError(userFS.Write(
fs.DirUserRoot, fs.ChatFilename,
"#### 29 June, Sunday\n- [x] `09:00` Done body",
))
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
h := inboxMsgHash(t, userFS, 0)
r.NoError(bot.rename([]string{fs.ChatFilename, h, "Renamed done"}))
inboxMD, _ := userFS.Read(fs.DirUserRoot, fs.ChatFilename)
r.Contains(inboxMD, "- [x] `09:00` Renamed done")
}
// Reproduces the "msgHash ... not found in inbox" error that users hit when
// forwarding multiple messages: message 1 produces a keyboard carrying
// msgHash1, message 2 is collapsed into the same inbox block as a
// continuation line, which shifts the block's hash. The original msgHash1
// from the keyboard then fails to resolve.
func TestForwardCollapse_HashStableAcrossContinuations(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time {
return time.Date(2025, 6, 29, 14, 35, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
u1 := tg.NewUpd(-1, "one\ntwo\nthree")
u1.TimeVal = 100
u1.HasTimeVal = true
r.NoError(bot.Reply(u1))
msgHash1, ok := firstMsgHash(-1)
r.True(ok, "firstMsgHash should be stored for msg 1")
u2 := tg.NewUpd(-1, "two")
u2.TimeVal = 100
u2.HasTimeVal = true
r.NoError(bot.Reply(u2))
// The keyboard on msg 1 still carries msgHash1. After the collapsed append,
// moveFromInbox must still resolve it to the first block.
var gotContent string
err = bot.moveFromChat(func(content string, _ time.Time) error {
gotContent = content
return nil
}, false, msgHash1)
r.NoError(err)
r.Contains(gotContent, "One")
r.Contains(gotContent, "two")
r.Contains(gotContent, "three")
}
func TestShowScheduleEmpty(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("schedule", nil)))
r.NoError(err)
r.Equal("You don't have any scheduled tasks! 🌴", tgram.SentTexts[0])
}
func TestShowSchedule(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
savedSchedNow := now
defer func() {
now = savedSchedNow
}()
now = func() time.Time {
return time.Date(1970, 10, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
err = cfg.AddToSchedule("filename.md", 0, "")
r.NoError(err)
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("schedule", nil)))
r.NoError(err)
r.Equal("<b>01 January, Thursday</b>\n- Filename", tgram.SentTexts[0])
}
func TestMoveToChecklistSplittable(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.MakeDir("-checklist-")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, "item1\nitem2"))
r.NoError(err)
content, err := userFS.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 11 August, Sunday\n- [ ] `09:54` Item1\nitem2\n", content)
err = bot.moveToDirChecklist([]string{inboxMsgHash(t, userFS, 0), "-checklist-"})
r.NoError(err)
files, err := userFS.FilesAndDirs("-checklist-")
r.NoError(err)
items := fs.OnlyFilenames(files)
r.ElementsMatch([]string{"Item1.md", "Item2.md"}, items)
}
func fakeConfig() *userconfig.Config {
userFS, _ := fs.NewFS("/-1", afero.NewMemMapFs())
cfg := userconfig.NewConfig(userFS, -1, "config.json")
_ = cfg.CreateDefaultIfNotExists()
return cfg
}
func TestExtractCmd(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "jj journal record")
cmd, err := bot.extractCmd(upd)
r.NoError(err)
r.NotNil(cmd)
r.Equal("j", cmd.Name)
r.Equal([]string{"Journal record"}, cmd.Params)
}
func TestExtractCmdRu(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "жж запись в журнал")
cmd, err := bot.extractCmd(upd)
r.NoError(err)
r.NotNil(cmd)
r.Equal("j", cmd.Name)
r.Equal([]string{"Запись в журнал"}, cmd.Params)
}
func TestExtractCmdSkipsInTheBeginning(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "jjj task for tomorrow")
cmd, err := bot.extractCmd(upd)
r.NoError(err)
r.Nil(cmd)
}
func TestExtractCmdSkipsAtTheMiddle(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "journal jj record")
cmd, err := bot.extractCmd(upd)
r.NoError(err)
r.Nil(cmd)
}
func TestExtractCmdSkipsInTheEnd(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "task for tomorrow jjj")
cmd, err := bot.extractCmd(upd)
r.NoError(err)
r.Nil(cmd)
}
func TestExtractCmdAtTheEnd(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, tg.NewFakeTG(), userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "journal record jj")
cmd, err := bot.extractCmd(upd)
r.NoError(err)
r.NotNil(cmd)
r.Equal("j", cmd.Name)
r.Equal([]string{"Journal record"}, cmd.Params)
}
func TestMoveToJournal(t *testing.T) {
r := require.New(t)
savedNow := journal.Now
defer func() {
journal.Now = savedNow
}()
journal.Now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("/", "Chat.md", "#### 27 June, Friday\n`01:01` Multiline\ncontent")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
files, err := userFS.FilesAndDirs("journal")
r.NoError(err)
r.Len(files, 1)
content, err := userFS.Read("journal", files[0].Name)
r.NoError(err)
r.Equal("## 1 January, Thursday\n`00:00` Multiline\ncontent\n", content)
content, err = userFS.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 27 June, Friday", content)
}
func TestAddToJournalFromShortcut(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, "jj record"))
r.NoError(err)
files, err := userFS.FilesAndDirs("journal")
r.NoError(err)
r.Len(files, 1)
}
func TestAddToJournalFromShortcutRu(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, "/ж запись"))
r.NoError(err)
files, err := userFS.FilesAndDirs("journal")
r.NoError(err)
r.Len(files, 1)
}
func TestAddToJournalFromShortcutRuCases(t *testing.T) {
r := require.New(t)
savedNow := journal.Now
defer func() {
journal.Now = savedNow
}()
journal.Now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, "жЖ запись"))
r.NoError(err)
files, err := userFS.FilesAndDirs("journal")
r.NoError(err)
r.Len(files, 1)
content, err := userFS.Read("journal", files[0].Name)
r.NoError(err)
r.Equal("## 1 January, Thursday\n`00:00` Запись\n", content)
err = bot.Reply(tg.NewUpd(-1, "Запись2 ЖЖ"))
r.NoError(err)
content, err = userFS.Read("journal", files[0].Name)
r.NoError(err)
r.Equal("## 1 January, Thursday\n`00:00` Запись\n`00:00` Запись2\n", content)
}
func TestShowForADay(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("sc_day", []string{"1c8f819d075"})))
r.NoError(err)
r.Equal(tg.NewKeyboard([]tg.Row{
[]tg.Btn{{Name: "🔄️ Repeat the task", Cmd: tg.Cmd{Name: "sc_day_r", Params: []string{"1c8f819d075"}, Type: "cmd"}}},
[]tg.Btn{
{Name: "Mon", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "345600", ""}, Type: "cmd"}},
{Name: "Tue", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "432000", ""}, Type: "cmd"}},
{Name: "Wed", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "518400", ""}, Type: "cmd"}},
{Name: "Thu", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "604800", ""}, Type: "cmd"}},
},
[]tg.Btn{
{Name: "Fri", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "86400", ""}, Type: "cmd"}},
{Name: "Sat", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "172800", ""}, Type: "cmd"}},
{Name: "Sun", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "259200", ""}, Type: "cmd"}},
},
[]tg.Btn{
{Name: "1", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "2678400", ""}, Type: "cmd"}},
{Name: "2", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "86400", ""}, Type: "cmd"}},
{Name: "3", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "172800", ""}, Type: "cmd"}},
{Name: "4", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "259200", ""}, Type: "cmd"}},
{Name: "5", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "345600", ""}, Type: "cmd"}},
{Name: "6", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "432000", ""}, Type: "cmd"}},
{Name: "7", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "518400", ""}, Type: "cmd"}},
{Name: "8", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "604800", ""}, Type: "cmd"}},
},
[]tg.Btn{
{Name: "9", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "691200", ""}, Type: "cmd"}},
{Name: "10", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "777600", ""}, Type: "cmd"}},
{Name: "11", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "864000", ""}, Type: "cmd"}},
{Name: "12", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "950400", ""}, Type: "cmd"}},
{Name: "13", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1036800", ""}, Type: "cmd"}},
{Name: "14", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1123200", ""}, Type: "cmd"}},
{Name: "15", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1209600", ""}, Type: "cmd"}},
{Name: "16", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1296000", ""}, Type: "cmd"}},
},
[]tg.Btn{
{Name: "17", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1382400", ""}, Type: "cmd"}},
{Name: "18", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1468800", ""}, Type: "cmd"}},
{Name: "19", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1555200", ""}, Type: "cmd"}},
{Name: "20", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1641600", ""}, Type: "cmd"}},
{Name: "21", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1728000", ""}, Type: "cmd"}},
{Name: "22", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1814400", ""}, Type: "cmd"}},
{Name: "23", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1900800", ""}, Type: "cmd"}},
{Name: "24", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "1987200", ""}, Type: "cmd"}},
},
[]tg.Btn{
{Name: "25", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "2073600", ""}, Type: "cmd"}},
{Name: "26", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "2160000", ""}, Type: "cmd"}},
{Name: "27", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "2246400", ""}, Type: "cmd"}},
{Name: "28", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "2332800", ""}, Type: "cmd"}},
{Name: "29", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "2419200", ""}, Type: "cmd"}},
{Name: "30", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "2505600", ""}, Type: "cmd"}},
{Name: "31", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f819d075", "2592000", ""}, Type: "cmd"}},
},
tg.Btn{Name: "➡️ Move to today", Cmd: tg.Cmd{Name: "home", Params: []string(nil), Type: "cmd"}},
}), tgram.LastSentKeyboard)
}
func TestShowForADayRecurring(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("sc_day_r", []string{"1c8f819d075"})))
r.NoError(err)
r.Equal(tg.NewKeyboard([]tg.Row{
[]tg.Btn{
{Name: "Weekdays", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "86400", "0 0 * * 1-5"}, Type: "cmd"}},
{Name: "Every day", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "86400", "0 0 * * *"}, Type: "cmd"}},
}, []tg.Btn{
{Name: "Mon", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "345600", "0 0 * * 1"}, Type: "cmd"}},
{Name: "Tue", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "432000", "0 0 * * 2"}, Type: "cmd"}},
{Name: "Wed", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "518400", "0 0 * * 3"}, Type: "cmd"}},
{Name: "Thu", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "604800", "0 0 * * 4"}, Type: "cmd"}},
}, []tg.Btn{
{Name: "Fri", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "86400", "0 0 * * 5"}, Type: "cmd"}},
{Name: "Sat", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "172800", "0 0 * * 6"}, Type: "cmd"}},
{Name: "Sun", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "259200", "0 0 * * 0"}, Type: "cmd"}},
}, []tg.Btn{
{Name: "1", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "2678400", "0 0 1 * *"}, Type: "cmd"}},
{Name: "2", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "86400", "0 0 2 * *"}, Type: "cmd"}},
{Name: "3", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "172800", "0 0 3 * *"}, Type: "cmd"}},
{Name: "4", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "259200", "0 0 4 * *"}, Type: "cmd"}},
{Name: "5", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "345600", "0 0 5 * *"}, Type: "cmd"}},
{Name: "6", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "432000", "0 0 6 * *"}, Type: "cmd"}},
{Name: "7", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "518400", "0 0 7 * *"}, Type: "cmd"}},
}, []tg.Btn{
{Name: "8", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "604800", "0 0 8 * *"}, Type: "cmd"}},
{Name: "9", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "691200", "0 0 9 * *"}, Type: "cmd"}},
{Name: "10", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "777600", "0 0 10 * *"}, Type: "cmd"}},
{Name: "11", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "864000", "0 0 11 * *"}, Type: "cmd"}},
{Name: "12", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "950400", "0 0 12 * *"}, Type: "cmd"}},
{Name: "13", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1036800", "0 0 13 * *"}, Type: "cmd"}},
{Name: "14", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1123200", "0 0 14 * *"}, Type: "cmd"}},
}, []tg.Btn{
{Name: "15", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1209600", "0 0 15 * *"}, Type: "cmd"}},
{Name: "16", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1296000", "0 0 16 * *"}, Type: "cmd"}},
{Name: "17", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1382400", "0 0 17 * *"}, Type: "cmd"}},
{Name: "18", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1468800", "0 0 18 * *"}, Type: "cmd"}},
{Name: "19", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1555200", "0 0 19 * *"}, Type: "cmd"}},
{Name: "20", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1641600", "0 0 20 * *"}, Type: "cmd"}},
{Name: "21", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1728000", "0 0 21 * *"}, Type: "cmd"}},
}, []tg.Btn{
{Name: "22", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1814400", "0 0 22 * *"}, Type: "cmd"}},
{Name: "23", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1900800", "0 0 23 * *"}, Type: "cmd"}},
{Name: "24", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "1987200", "0 0 24 * *"}, Type: "cmd"}},
{Name: "25", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "2073600", "0 0 25 * *"}, Type: "cmd"}},
{Name: "26", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "2160000", "0 0 26 * *"}, Type: "cmd"}},
{Name: "27", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "2246400", "0 0 27 * *"}, Type: "cmd"}},
{Name: "28", Cmd: tg.Cmd{Name: "sc", Params: []string{"1c8f", "2332800", "0 0 28 * *"}, Type: "cmd"}},
}, tg.Btn{Name: "➡️ Move to today", Cmd: tg.Cmd{Name: "home", Params: []string(nil), Type: "cmd"}},
}), tgram.LastSentKeyboard)
}
func TestSchedule(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.Reply(tg.NewUpd(-1, "Task"))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("sc", []string{inboxMsgHash(t, userFS, 0), "345600", "0 0 * * 1-5"})))
r.NoError(err)
tasksForToday, err := userFS.FilesAndDirs("home")
r.NoError(err)
r.Empty(tasksForToday)
laterMD, err := userFS.Read(fs.DirUserRoot, fs.LaterFilename)
r.NoError(err)
items, _ := txt.ChecklistItems(laterMD)
r.Contains(items, "Task")
sc, err := cfg.Schedules()
r.NoError(err)
r.Len(sc, 1)
r.Equal("Task", sc[0].Filename)
r.Equal(int64(345600), sc[0].ScheduledAt)
r.Equal("0 0 * * 1-5", sc[0].Cron)
}
// The recurring picker truncates the chat msgHash to its 4-char prefix to fit
// Telegram's 64-byte callback_data limit when a cron expression is also packed
// in. moveFromChat must accept the prefix and still locate the inbox entry.
func TestScheduleRepeatedWithTruncatedHash(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.Reply(tg.NewUpd(-1, "Task"))
r.NoError(err)
fullHash := inboxMsgHash(t, userFS, 0)
truncated := fullHash[:4]
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("sc", []string{truncated, "345600", "0 0 * * 1-5"})))
r.NoError(err)
laterMD, err := userFS.Read(fs.DirUserRoot, fs.LaterFilename)
r.NoError(err)
items, _ := txt.ChecklistItems(laterMD)
r.Contains(items, "Task")
sc, err := cfg.Schedules()
r.NoError(err)
r.Len(sc, 1)
r.Equal("Task", sc[0].Filename)
r.Equal("0 0 * * 1-5", sc[0].Cron)
}
func TestScheduleNoRepeat(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.Reply(tg.NewUpd(-1, "Task"))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("sc", []string{inboxMsgHash(t, userFS, 0), "345600", ""})))
r.NoError(err)
tasksForToday, err := userFS.FilesAndDirs("home")
r.NoError(err)
r.Empty(tasksForToday)
laterMD, err := userFS.Read(fs.DirUserRoot, fs.LaterFilename)
r.NoError(err)
items, _ := txt.ChecklistItems(laterMD)
r.Contains(items, "Task")
sc, err := cfg.Schedules()
r.NoError(err)
r.Len(sc, 1)
r.Equal("Task", sc[0].Filename)
r.Equal(int64(345600), sc[0].ScheduledAt)
}
func TestInlineRequestTask(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("dir", "File.md", "File content")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, " dir / File ")
upd.IsSentViaBotVal = true
err = bot.Reply(upd)
r.NoError(err)
r.Equal("<b>File</b>\n\nFile content", tgram.LastSentText)
}
func TestInlineRequestFile(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, " File ")
upd.IsSentViaBotVal = true
err = bot.Reply(upd)
r.NoError(err)
r.Equal("<b>File</b>\n\nFile content", tgram.LastSentText)
}
func TestInlineRequestFileOutsideTheDirectory(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "../File")
upd.IsSentViaBotVal = true
err = bot.Reply(upd)
r.Error(err)
r.EqualError(err, "insecure input '../File': invalid request from inline query")
}
func TestInlineRequestFileOutsideTheDirectoryTwoSlashes(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "..//File")
upd.IsSentViaBotVal = true
err = bot.Reply(upd)
r.Error(err)
r.EqualError(err, "insecure input '..//File': invalid request from inline query")
}
func TestInlineRequestFileListOutsideDirs(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "../")
upd.IsSentViaBotVal = true
err = bot.Reply(upd)
r.Error(err)
r.EqualError(err, "insecure input '../': invalid request from inline query")
}
func TestInlineRequestFileListRootDirs(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
tgram := tg.NewFakeTG()
// cd /tmp//.. would lead to root
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "/..")
upd.IsSentViaBotVal = true
err = bot.Reply(upd)
r.Error(err)
r.EqualError(err, "insecure input '/..': invalid request from inline query")
}
func TestInlineRequestFileListRootDirsWithoutSlash(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
tgram := tg.NewFakeTG()
// cd /tmp/.. would lead to root
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "..")
upd.IsSentViaBotVal = true
err = bot.Reply(upd)
r.Error(err)
r.EqualError(err, "show file: can't find file: can't unhash '..' in '/': cannot unhash, maybe the file is missing")
}
func TestAnswerSearch(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
u := tg.NewUpd(-1, "")
u.InlineQueryVal = "File"
u.IsInlineQueryVal = true
err = bot.answerSearch(u)
r.NoError(err)
var results []interface{}
article := tgbotapi.NewInlineQueryResultArticleHTML("0", "File", "File.md")
results = append(results, article)
r.Equal(results, tgram.InlineQueryResults)
}
func TestAnswerSearchShowAllRoot(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
err = userFS.MakeDir("Dir")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
u := tg.NewUpd(-1, "")
u.InlineQueryVal = "File"
u.IsInlineQueryVal = true
err = bot.answerSearch(u)
r.NoError(err)
var results []interface{}
article := tgbotapi.NewInlineQueryResultArticleHTML("0", "File", "File.md")
results = append(results, article)
r.Equal(results, tgram.InlineQueryResults)
}
func TestAnswerSearchShowOutsideTheRoot(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
err = userFS.MakeDir("Dir")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
u := tg.NewUpd(-1, "")
u.InlineQueryVal = "../"
u.IsInlineQueryVal = true
err = bot.answerSearch(u)
r.Error(err)
r.EqualError(err, "insecure input '../': invalid inline query")
}
func TestAnswerSearchShowOutsideTheRootNoSlash(t *testing.T) {
r := require.New(t)
memFS := afero.NewMemMapFs()
err := afero.WriteFile(memFS, "/secret", []byte("secret"), 0o644)
r.NoError(err)
userFS, err := fs.NewFS("/-1", memFS)
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "File.md", "File content")
r.NoError(err)
err = userFS.MakeDir("Dir")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
u := tg.NewUpd(-1, "")
u.InlineQueryVal = ".."
u.IsInlineQueryVal = true
err = bot.answerSearch(u)
r.Error(err)
r.EqualError(err, "inline reply: search notes: exists: unsafe path '/': unsafe path, possible security issue")
}
func TestShowFileEscapesHTML(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("", "File.md", "<b>bold*italic*")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.showFile([]string{"/", "File.md"})
r.NoError(err)
r.Equal("<b>File</b>\n\n&lt;b&gt;bold<i>italic</i>", tgram.LastSentText)
}
func TestSaveToNewTask(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "New task"))
r.NoError(err)
kb := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 0)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
content, err := userFS.Read("notes", "New task.md")
r.NoError(err)
r.Equal("", content)
r.Nil(database.InputExpectation())
msgID, ok := database.LastKeyboardMsgID()
r.True(ok)
r.Equal(3, msgID)
r.Equal(msgID, tgram.LastSentMessageID)
}
func TestSaveToExistingFile(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("/", "Chat.md", "#### 27 June, Friday\n`01:01` Existing\nmessage")
r.NoError(err)
err = userFS.Write("", "File.md", "#### 1 January 1970, Thursday\nExisting content")
r.NoError(err)
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "New content"))
r.NoError(err)
kb := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 1)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 1)})))
r.NoError(err)
selectFileKB := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("File", tg.NewCmd("mf", []string{"7595e", inboxMsgHash(t, userFS, 1)})),
),
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(
tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 1)})),
),
})
r.Equal(selectFileKB, tgram.LastEditedKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mf", []string{"7595e", inboxMsgHash(t, userFS, 1)})))
r.NoError(err)
r.Nil(tgram.LastEditedKeyboard)
content, err := userFS.Read("", "File.md")
r.NoError(err)
r.Equal("#### 1 January 1970, Thursday\nExisting content\nNew content", content)
r.Nil(database.InputExpectation())
keybdMsgID, ok := database.LastKeyboardMsgID()
r.True(ok)
r.Equal(3, keybdMsgID)
r.Equal(3, tgram.LastSentMessageID)
}
func TestSaveToExistingFileModeTasks(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("/", "Chat.md", "#### 27 June, Friday\n`01:01` New\ncontent")
r.NoError(err)
err = userFS.Write("", "File.md", "#### 1 January 1970, Thursday\nExisting\ncontent")
r.NoError(err)
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "Text"))
r.NoError(err)
kb := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 1)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 1)})))
r.NoError(err)
selectFileKB := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("File", tg.NewCmd("mf", []string{"7595e", inboxMsgHash(t, userFS, 1)})),
),
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(
tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 1)})),
),
})
r.Equal(selectFileKB, tgram.LastEditedKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mf", []string{"7595e", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
r.Nil(tgram.LastEditedKeyboard)
content, err := userFS.Read("", "File.md")
r.NoError(err)
r.Equal("#### 1 January 1970, Thursday\nExisting\ncontent\nNew\ncontent", content)
r.Nil(database.InputExpectation())
keybdMsgID, ok := database.LastKeyboardMsgID()
r.True(ok)
r.Equal(3, keybdMsgID)
r.Equal(3, tgram.LastSentMessageID)
}
func TestSaveToNewFile(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Chat.md", "#### 1 January, Thursday\n`01:01` New\ncontent")
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "Text"))
r.NoError(err)
content, err := userFS.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 1 January, Thursday\n`01:01` New\ncontent\n- [ ] `00:00` Text\n", content)
kb := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 1)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 1)})),
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 1)})))
r.NoError(err)
selectFileKB := tg.NewKeyboard([]tg.Row{
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(
tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 1)})),
),
})
r.Equal(selectFileKB, tgram.LastEditedKeyboard)
//err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mf", []string{"23200", inboxMsgHash(t, userFS, 0)})))
//r.NoError(err)
err = bot.Reply(tg.NewUpd(-1, "Myfile"))
r.NoError(err)
content, err = userFS.Read("/", "Myfile.md")
r.NoError(err)
r.Equal("Text", content)
r.Nil(database.InputExpectation())
msgID, ok := database.LastKeyboardMsgID()
r.True(ok)
r.Equal(1, msgID)
r.Equal(2, tgram.LastSentMessageID)
}
func TestSaveToNewDirFull(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("/", "Chat.md", "#### 1 January, Thursday\n")
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "Text"))
r.NoError(err)
kb := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 0)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
selectFileKB := tg.NewKeyboard([]tg.Row{
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(
tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 0)})),
),
})
r.Equal(selectFileKB, tgram.LastEditedKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
r.Equal("OK. Send me the name for your new dir", tgram.LastEditedText)
r.Nil(tgram.LastEditedKeyboard)
r.Equal(tg.NewCmd("mv_to_new_dir", []string{inboxMsgHash(t, userFS, 0), "%s"}), *database.InputExpectation())
err = bot.Reply(tg.NewUpd(-1, "My dir"))
r.NoError(err)
r.Equal("🌴 Nothing here yet - send me something!", tgram.LastSentText)
content, err := userFS.Read("my dir", "Text.md")
r.NoError(err)
r.Empty(content)
r.Nil(database.InputExpectation())
msgID, ok := database.LastKeyboardMsgID()
r.True(ok)
r.Equal(3, msgID)
r.Equal(3, tgram.LastSentMessageID)
}
func TestSaveToNewDir(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "Text"))
r.NoError(err)
kb := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 0)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
selectFileKB := tg.NewKeyboard([]tg.Row{
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(
tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 0)})),
),
})
r.Equal(selectFileKB, tgram.LastEditedKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
r.Equal("OK. Send me the name for your new dir", tgram.LastEditedText)
r.Nil(tgram.LastEditedKeyboard)
r.Equal(tg.NewCmd("mv_to_new_dir", []string{inboxMsgHash(t, userFS, 0), "%s"}), *database.InputExpectation())
err = bot.Reply(tg.NewUpd(-1, "My dir"))
r.NoError(err)
r.Equal("🌴 Nothing here yet - send me something!", tgram.LastSentText)
content, err := userFS.Read("my dir", "Text.md")
r.NoError(err)
r.Empty(content)
r.Nil(database.InputExpectation())
msgID, ok := database.LastKeyboardMsgID()
r.True(ok)
r.Equal(3, msgID)
r.Equal(3, tgram.LastSentMessageID)
}
func TestSaveToNewMultilineFile(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("", "Text.md", "")
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "Multiline\ncontent"))
r.NoError(err)
kb := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 0)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
selectFileKB := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("Text", tg.NewCmd("mf", []string{"23200", inboxMsgHash(t, userFS, 0)})),
),
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(
tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 0)})),
),
})
r.Equal(selectFileKB, tgram.LastEditedKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mf", []string{"23200", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
r.Empty(tgram.LastEditedKeyboard)
content, err := userFS.Read("/", "Text.md")
r.NoError(err)
r.Equal("#### 1 January 1970, Thursday\nMultiline\ncontent", content)
r.Nil(database.InputExpectation())
msgID, ok := database.LastKeyboardMsgID()
r.True(ok)
r.Equal(3, msgID)
r.Equal(msgID, tgram.LastSentMessageID)
}
func TestSaveToNewCustomFile(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "Text"))
r.NoError(err)
kb := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 0)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
selectFileKB := tg.NewKeyboard([]tg.Row{
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(
tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 0)})),
),
})
r.Equal(selectFileKB, tgram.LastEditedKeyboard)
err = bot.Reply(tg.NewUpd(-1, "new file"))
r.NoError(err)
r.Empty(tgram.LastEditedKeyboard.Btns)
content, err := userFS.Read("", "New file.md")
r.NoError(err)
r.Equal("Text", content)
r.Nil(database.InputExpectation())
msgID, ok := database.LastKeyboardMsgID()
r.True(ok)
r.Equal(1, msgID)
r.Equal(2, tgram.LastSentMessageID)
}
func TestSaveToRecentFile(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("", "Text.md", "Text")
r.NoError(err)
err = userFS.MakeDir("dir")
r.NoError(err)
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
_ = cfg.AddMoveToCmd(CmdScheduleForTmrw)
_ = cfg.AddMoveToCmd(CmdMoveToLater)
_ = cfg.AddMoveToCmd(CmdShowScheduleForDay)
_ = cfg.AddMoveToCmd(CmdShowMoveToDirOrFile)
_ = cfg.AddMoveToCmd(CmdMoveToJournal)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "New text"))
r.NoError(err)
kb := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 0)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
selectFileKeyboard := tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("Text", tg.NewCmd("mf", []string{"23200", inboxMsgHash(t, userFS, 0)})),
),
tg.NewBtn("Search", tg.NewCustomCmd("search", nil, "iq")),
tg.NewRow(
tg.NewBtn("🗂️ Dir", tg.NewCmd("mv", []string{"73600", inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("🗂 New Dir", tg.NewCmd("new_dir", []string{inboxMsgHash(t, userFS, 0)})),
),
})
r.Equal(selectFileKeyboard, tgram.LastEditedKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mf", []string{"23200", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
r.Empty(tgram.LastEditedKeyboard)
content, err := userFS.Read("", "Text.md")
r.NoError(err)
r.Equal("#### 1 January 1970, Thursday\nNew text\n\nText", content)
recentCMD, ok := database.RecentCommand()
r.Equal("mf", recentCMD)
r.True(ok)
// Adding text again to see if we have a recent file
err = bot.Reply(tg.NewUpd(-1, "Text2"))
r.NoError(err)
kb = tg.NewKeyboard([]tg.Row{
tg.NewRow(
tg.NewBtn("🌚 To tmrw", tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("⏳ To later", tg.NewCmd("mv_later", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("📆 To a day", tg.NewCmd("sc_day", []string{inboxMsgHash(t, userFS, 0)})),
),
tg.NewRow(
tg.NewBtn("📄 To File", tg.NewCmd("to_file", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("💚 To Journal", tg.NewCmd("mv_to_journal", []string{inboxMsgHash(t, userFS, 0)})),
tg.NewBtn("⭐️ Text", tg.NewCmd("mf", []string{"23200", inboxMsgHash(t, userFS, 0)})),
),
tg.NewRow(
tg.NewBtn("👌", tg.NewCmd("home", []string{})),
),
})
r.Equal(kb, tgram.LastSentKeyboard)
r.Nil(database.InputExpectation())
msgID, ok := database.LastKeyboardMsgID()
r.True(ok)
r.Equal(4, msgID)
r.Equal(msgID, tgram.LastSentMessageID)
}
func TestSaveToTodayTask(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("", "Chat.md", txt.AddChecklistItem("", "Existing task", false))
r.NoError(err)
cfg := userconfig.NewConfig(userFS, -1, "config.json")
err = cfg.CreateDefaultIfNotExists()
r.NoError(err)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
bot := NewBot(-1, tgram, userFS, database, cfg)
err = bot.Reply(tg.NewUpd(-1, "New task"))
r.NoError(err)
// Click "move to checklist
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("home", nil)))
r.NoError(err)
// Block 0 is the top-level "Existing task"; the inbox entry "New task" is block 1.
kb := tg.NewKeyboard([]tg.Row{
tg.NewBtn("Existing task", tg.NewCmd("c", []string{"04fcfb3c2ef"})),
tg.NewBtn("New task", tg.NewCmd("c", []string{"58d765d4752"})),
})
r.Equal(kb, tgram.LastEditedKeyboard)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("c", []string{"58d765d4752"})))
r.NoError(err)
kb = tg.NewKeyboard([]tg.Row{
tg.NewBtn("Existing task", tg.NewCmd("c", []string{"04fcfb3c2ef"})),
})
r.Equal(kb, tgram.LastEditedKeyboard)
}
func TestCollapseToMsg(t *testing.T) {
var userID int64 = -1
r := require.New(t)
clean := func() {
firstMsgHashes.Range(func(key, value interface{}) bool {
firstMsgTimes.Delete(key)
return true
})
firstMsgTimes.Range(func(key, value interface{}) bool {
firstMsgTimes.Delete(key)
return true
})
}
clean()
// Collapse same second messages
setFirstMsgHash(userID, "h", 100)
setFirstMsgTime(userID, 100)
_, shouldCollapse := collapseToMsg(userID, 100)
r.True(shouldCollapse)
//r.Equal("file1.md", filename)
clean()
// Collapse next second messages
setFirstMsgHash(userID, "h", 100)
setFirstMsgTime(userID, 100)
_, shouldCollapse = collapseToMsg(userID, 101)
require.True(t, shouldCollapse, "Expected to collapse the message")
//require.Equal(t, "file2.md", filename, "Expected filename to match the first message")
clean()
// Do not collapse distant messages
setFirstMsgHash(userID, "h", 100)
setFirstMsgTime(userID, 100)
_, shouldCollapse = collapseToMsg(userID, 103)
require.False(t, shouldCollapse, "Expected not to collapse the message")
//require.Empty(t, filename, "Expected no filename for non-collapsing messages")
clean()
// Collapse consecutive batch messages
setFirstMsgHash(userID, "h", 200)
setFirstMsgTime(userID, 200)
// Loop to simulate a series of consecutive messages within a one-second interval
for i := 0; i < 5; i++ {
_, shouldCollapse = collapseToMsg(userID, 200+i)
require.True(t, shouldCollapse, "Expected to collapse the message in the batch")
//require.Equal(t, "file4.md", filename, "Expected filename to match the initial message")
setFirstMsgTime(userID, 200+i)
}
clean()
}
func TestCollapseForwardedMessages(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "First msg")
upd.TimeVal = 0
upd.HasTimeVal = true
err = bot.Reply(upd)
r.NoError(err)
upd = tg.NewUpd(-1, "Second msg")
upd.TimeVal = 0
upd.HasTimeVal = true
err = bot.Reply(upd)
r.NoError(err)
upd = tg.NewUpd(-1, "Third msg")
upd.TimeVal = 1
upd.HasTimeVal = true
err = bot.Reply(upd)
r.NoError(err)
upd = tg.NewUpd(-1, "Fourth msg")
upd.TimeVal = 3
upd.HasTimeVal = true
err = bot.Reply(upd)
r.NoError(err)
content, err := userFS.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 1 January, Thursday\n- [ ] `00:00` First msg\nSecond msg\nThird msg\n- [ ] `00:00` Fourth msg\n", content)
// Clean
firstMsgHashes.Range(func(key, value interface{}) bool {
firstMsgTimes.Delete(key)
return true
})
firstMsgTimes.Range(func(key, value interface{}) bool {
firstMsgTimes.Delete(key)
return true
})
}
func TestTitleChecklist(t *testing.T) {
r := require.New(t)
title := checklistTitle("_checklist_")
r.Equal("Checklist", title)
}
func TestTitleChecklistItem(t *testing.T) {
r := require.New(t)
title := checklistTitle("_checklist_item")
r.Equal("Item", title)
}
func TestRestoreMsg_EmptyMessage(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, nil, userFS, nil, nil)
filename := "Empty.md"
err = userFS.Write("home", filename, "")
r.NoError(err)
title, err := bot.restoreMsg("home", filename)
r.NoError(err)
r.Equal("Empty", title)
}
func TestRestoreMsg_ContentWithoutTitle(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, nil, userFS, nil, nil)
filename := "NewTask.md"
content := "Some content that doesn't include the title"
err = userFS.Write("home", filename, content)
r.NoError(err)
msg, err := bot.restoreMsg("home", filename)
r.NoError(err)
r.Equal("NewTask\nSome content that doesn't include the title", msg)
}
func TestRestoreMsg_ContentWithTitle(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, nil, userFS, nil, nil)
filename := "Task.md"
content := "Task\nDetails about the task"
err = userFS.Write("home", filename, content)
r.NoError(err)
msg, err := bot.restoreMsg("home", filename)
r.NoError(err)
r.Equal(content, msg)
}
func TestRestoreMsg_ContentWithSanitizedTitle(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, nil, userFS, nil, nil)
filename := "TaskSlash.md"
content := "Task/Slash\nDetails about the task"
err = userFS.Write("home", filename, content)
r.NoError(err)
msg, err := bot.restoreMsg("home", filename)
r.NoError(err)
r.Equal(content, msg)
}
func TestRestoreMsg_WithImage(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, nil, userFS, nil, nil)
filename := "Caption.md"
content := "![img](tg_url.jpg)\nCaption"
err = userFS.Write("home", filename, content)
r.NoError(err)
msg, err := bot.restoreMsg("home", filename)
r.NoError(err)
r.Equal(content, msg)
}
func TestRestoreMsg_WithImageSanitizedFilename(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, nil, userFS, nil, nil)
filename := "CaptionFile"
content := "![img](tg_url.jpg)\nCaption/File"
err = userFS.Write("home", filename, content)
r.NoError(err)
msg, err := bot.restoreMsg("home", filename)
r.NoError(err)
r.Equal(content, msg)
}
func TestSaveFromImage_NewFile(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
upd.PhotoCaption = "New Image"
err = bot.saveFromImage(upd)
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
files, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(files, 1)
r.Equal("New Image.md", files[0].Name)
content, err := bot.fs.Read("notes", "New Image.md")
r.NoError(err)
r.Equal("![](media/tg_PHOTO_ID)\nNew Image", content)
}
func TestSaveFromImage_LongCaption(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
upd.PhotoCaption = strings.Repeat("a", 34)
err = bot.saveFromImage(upd)
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
filename := "A" + strings.Repeat("a", 33) + ".md"
content, err := bot.fs.Read("notes", filename)
r.NoError(err)
r.Equal(fmt.Sprintf("![](media/tg_PHOTO_ID)\nA%s", strings.Repeat("a", 33)), content)
}
func TestSaveFromImage_MultilineCaption(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
upd.PhotoCaption = "abc\ndef"
err = bot.saveFromImage(upd)
r.NoError(err)
content, err := userFS.Read("/", "Chat.md")
r.NoError(err)
r.Equal("#### 11 August, Sunday\n- [ ] `09:54` ![](media/tg_PHOTO_ID)\nAbc\ndef\n", content)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
filename := fmt.Sprintf("Abc.md")
content, err = bot.fs.Read("notes", filename)
r.NoError(err)
r.Equal("![](media/tg_PHOTO_ID)\nAbc\ndef", content)
}
func TestSaveFromImage_ReplyToExistingFile(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
// Setup in-memory filesystem and add an existing file
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.Write("home", "Existing file.md", "Existing content")
r.NoError(err)
tgram := tg.NewFakeTG()
database := db.NewFakeDB()
database.SetHashOrPathByMsgID(255, "home/Existing file.md")
bot := NewBot(-1, tgram, userFS, database, fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
upd.PhotoCaption = "Image Caption"
upd.ReplyToMessageID = 255
err = bot.saveFromImage(upd)
r.NoError(err)
content, err := bot.fs.Read("home", "Existing file.md")
r.NoError(err)
r.Equal("Existing content\n\n![](media/tg_PHOTO_ID)\nImage Caption\n", content)
}
func TestSaveFromImage_EmptyCaption(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
userFS.CreateDirsIfNotExist("notes")
tgram := tg.NewFakeTG()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
upd := tg.NewUpd(-1, "")
upd.PhotoID = "PHOTO_ID"
err = bot.saveFromImage(upd)
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
files, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
r.Len(files, 1)
r.Equal("Img 01.01.70 0000.md", files[0].Name)
content, err := bot.fs.Read("notes", "Img 01.01.70 0000.md")
r.NoError(err)
r.Equal("![](media/tg_PHOTO_ID)", content)
}
func TestCreateOrAdd_NewFile(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, nil, userFS, nil, nil)
dir := "home"
filename := "NewFile.md"
content := "This is new content"
err = bot.createOrAdd(dir, filename, content)
r.NoError(err)
storedContent, err := bot.fs.Read(dir, filename)
r.NoError(err)
r.Equal(content, storedContent)
}
func TestCreateOrAdd_AppendToExistingFile(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, nil, userFS, nil, nil)
dir := "home"
filename := "ExistingFile.md"
existingContent := "Existing content"
newContent := "New content"
err = userFS.Write(dir, filename, existingContent)
r.NoError(err)
err = bot.createOrAdd(dir, filename, newContent)
r.NoError(err)
expectedContent := "Existing content\nNew content"
storedContent, err := bot.fs.Read(dir, filename)
r.NoError(err)
r.Equal(expectedContent, storedContent)
}
func TestCreateOrAdd_ReplaceEmptyContent(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
bot := NewBot(-1, nil, userFS, nil, nil)
dir := "home"
filename := "EmptyFile.md"
newContent := "New content"
err = userFS.Write(dir, filename, "\n")
r.NoError(err)
err = bot.createOrAdd(dir, filename, newContent)
r.NoError(err)
storedContent, err := bot.fs.Read(dir, filename)
r.NoError(err)
r.Equal(newContent, storedContent)
}
func TestExtractTitleAndContent_EmptyMessage(t *testing.T) {
r := require.New(t)
bot := NewBot(-1, nil, nil, nil, nil)
title, content, err := bot.extractHeaderAndBody("", 33)
r.Error(err)
r.Contains(err.Error(), "extract title: empty msg")
r.Equal("", title)
r.Equal("", content)
}
func TestExtractTitleAndContent_SimpleMessage(t *testing.T) {
r := require.New(t)
bot := NewBot(-1, nil, nil, nil, nil)
msg := "Simple DisplayName"
title, content, err := bot.extractHeaderAndBody(msg, 33)
r.NoError(err)
r.Equal("Simple DisplayName", title)
r.Equal("", content)
}
func TestExtractTitleAndContent_MultilineMessage(t *testing.T) {
r := require.New(t)
bot := NewBot(-1, nil, nil, nil, nil)
msg := "DisplayName Line\nThis is the content"
title, content, err := bot.extractHeaderAndBody(msg, 33)
r.NoError(err)
r.Equal("DisplayName Line", title)
r.Equal("This is the content", content)
}
func TestExtractTitleAndContent_TitleExceedsMaxLength(t *testing.T) {
r := require.New(t)
bot := NewBot(-1, nil, nil, nil, nil)
longTitle := strings.Repeat("a", 33+1)
msg := longTitle + "\nContent below"
expectedTitle := txt.Substr(txt.Ucfirst(longTitle), 0, 33) + "..."
title, content, err := bot.extractHeaderAndBody(msg, 33)
r.NoError(err)
r.Equal(expectedTitle, title)
r.Equal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nContent below", content)
}
func TestExtractTitleAndContent_TitleSameAsContent(t *testing.T) {
r := require.New(t)
bot := NewBot(-1, nil, nil, nil, nil)
msg := "Identical DisplayName"
title, content, err := bot.extractHeaderAndBody(msg, 33)
r.NoError(err)
r.Equal("Identical DisplayName", title)
r.Equal("", content)
}
func TestExtractTitleAndContent_ContentStartsWithTitle(t *testing.T) {
r := require.New(t)
bot := NewBot(-1, nil, nil, nil, nil)
msg := "DisplayName Line\nDisplayName Line\nAdditional content"
title, content, err := bot.extractHeaderAndBody(msg, 33)
r.NoError(err)
r.Equal("DisplayName Line", title)
r.Equal("DisplayName Line\nAdditional content", content)
}
func TestExtractTitleAndContent_TitleNeedsSanitization(t *testing.T) {
r := require.New(t)
bot := NewBot(-1, nil, nil, nil, nil)
msg := "Invalid/DisplayName?Name\nContent here"
title, content, err := bot.extractHeaderAndBody(msg, 33)
r.NoError(err)
r.Equal("InvalidDisplayName?Name", title)
r.Equal("Invalid/DisplayName?Name\nContent here", content)
}
func TestMoveToExistingNote_Success(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.MakeDir("notes")
r.NoError(err)
err = userFS.Write("notes", "ExistingNote.md", "Existing content\n")
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.Reply(tg.NewUpd(-1, "Task content"))
r.NoError(err)
toDirHash := fs.Hash("notes")
toFilenameHash := fs.Hash("ExistingNote.md")
err = bot.moveToExistingNote([]string{toFilenameHash, toDirHash, inboxMsgHash(t, userFS, 0)})
r.NoError(err)
content, err := userFS.Read("notes", "ExistingNote.md")
r.NoError(err)
r.Equal("#### 1 January 1970, Thursday\nTask content\n\nExisting content\n", content)
_, err = userFS.Read("home", "Task.md")
r.Error(err)
}
func TestMoveToExistingNote_FileNotFound(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
toDirHash := fs.Hash("notes")
toFilenameHash := fs.Hash("ExistingNote.md")
fromFilenameHash := fs.Hash("Task.md")
err = bot.moveToExistingNote([]string{toFilenameHash, toDirHash, fromFilenameHash})
r.Error(err)
}
func TestMoveToExistingNote_InvalidIndex(t *testing.T) {
r := require.New(t)
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeFull
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
toDirHash := "invalidHash"
toFilenameHash := "invalidHash"
msgIndex := "-1"
err = bot.moveToExistingNote([]string{toFilenameHash, toDirHash, msgIndex})
r.Error(err)
r.Contains(err.Error(), "move to existing note")
}
// TODO move to Chat.md
func FuzzSaveFromTextMsg(f *testing.F) {
mode := userconfig.DefaultConfig.Mode
userconfig.DefaultConfig.Mode = userconfig.ModeTasks
defer func() {
userconfig.DefaultConfig.Mode = mode
}()
seedInputs := []string{
"Normal task",
"Special char /\\:*?|<>",
"Emoji 😃🚀",
strings.Repeat("a", 5000),
}
for _, input := range seedInputs {
f.Add(input)
}
f.Add(".")
f.Add("..")
f.Add("/today/..md")
f.Add("/valid/path")
f.Add("../file")
f.Add("../../file")
f.Add("../../../file")
f.Fuzz(func(t *testing.T, input string) {
if len(strings.TrimSpace(input)) == 0 {
return
}
// Telegram trims space
input = strings.TrimSpace(input)
// Ignore input with shortcuts like adding to journal or to recent file.
// Because in that case file won't be created in "home" folder
shortcuts := []string{"jj", "жж", "++"}
for _, shortcut := range shortcuts {
lowerInput := strings.ToLower(input)
if strings.HasPrefix(lowerInput, shortcut) || strings.HasSuffix(lowerInput, shortcut) {
return
}
}
// fs.sanitizeName will strip away \0 characters as they're not allowed in filenames
if strings.Contains(input, "\x00") {
fmt.Println("Skipping string with null character")
return
}
r := require.New(t)
memfs := afero.NewMemMapFs()
_ = memfs.Mkdir("/user", 0o755)
userFS, err := fs.NewFS("/user", memfs)
userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.CreateDirsIfNotExist("notes")
r.NoError(err)
tgram := tg.NewFakeTG()
savedPlugins := BotPlugins
BotPlugins = []BotPlugin{}
defer func() {
BotPlugins = savedPlugins
}()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), fakeConfig())
err = bot.Reply(tg.NewUpd(-1, input))
if err != nil {
// Check that no entries are created besides our user folder
entries, err := afero.ReadDir(memfs, "/")
r.NoError(err)
r.Len(entries, 1)
return
}
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("mv", []string{"4358b5009c6", inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
tasks, err := bot.fs.FilesAndDirs("notes")
r.NoError(err)
if input == "" {
r.Len(tasks, 0)
return
}
r.Len(tasks, 1, "No tasks created for input %q", input)
title := txt.Ucfirst(strings.TrimSpace(strings.SplitN(strings.TrimSpace(input), "\n", 2)[0]))
if utf8.RuneCountInString(title) > 100 {
title = txt.Substr(title, 0, 100) + "..."
}
filename := fs.SanitizeFilename(title) + ".md"
r.Equal(filename, tasks[0].Name)
_, err = bot.fs.Read("notes", filename)
r.NoError(err)
})
}
func TestJournalOnlyMode_SaveTextMessage(t *testing.T) {
r := require.New(t)
savedNow := journal.Now
defer func() {
journal.Now = savedNow
}()
journal.Now = func() time.Time {
return time.Date(2024, 8, 11, 9, 54, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
err = cfg.SetMode(userconfig.ModeJournal)
r.NoError(err)
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.Reply(tg.NewUpd(-1, "Journal entry"))
r.NoError(err)
todayFiles, err := bot.fs.FilesAndDirs("home")
r.NoError(err)
r.Len(todayFiles, 0)
journalFiles, err := bot.fs.FilesAndDirs("journal")
r.NoError(err)
r.Len(journalFiles, 1)
content, err := bot.fs.Read("journal", journalFiles[0].Name)
r.NoError(err)
r.Contains(content, "Journal entry")
r.Contains(content, "11 August, Sunday")
}
//func TestFileOnlyMode_SaveTextMessage(t *testing.T) {
// r := require.New(t)
//
// userFS, err := fs.NewFS("/", afero.NewMemMapFs())
// r.NoError(err)
// err = userFS.CreateSystemDirs()
// r.NoError(err)
//
// tgram := tg.NewFakeTG()
//
// cfg := fakeConfig()
// err = cfg.SetMode(userconfig.ModeOneFile)
// r.NoError(err)
//
// bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
// err = bot.Reply(tg.NewUpd(-1, "File content"))
// r.NoError(err)
//
// todayFiles, err := bot.fs.FilesAndDirs("home")
// r.NoError(err)
// r.Len(todayFiles, 0)
//
// rootFiles, err := bot.fs.FilesAndDirs("/")
// r.NoError(err)
// r.True(len(rootFiles) > 0)
//
// content, err := bot.fs.Read("", "Chat.md")
// r.NoError(err)
// r.Equal("File content", content)
//}
func TestShowToday_NotesOnlyMode(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.MakeDir("test-dir")
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
err = cfg.SetMode(userconfig.ModeNotes)
r.NoError(err)
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.ShowHome(nil)
r.NoError(err)
// Should show directories instead of today tasks
// The exact message depends on showDirs implementation, but we verify it was called
r.NotEmpty(tgram.LastSentText)
// In notes-only mode, ShowToday should redirect to showing directories
}
func TestShowToday_JournalOnlyMode(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
err = cfg.SetMode(userconfig.ModeJournal)
r.NoError(err)
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.ShowHome(nil)
r.NoError(err)
// Should send "What's on your mind?" message
r.Contains(tgram.LastSentText, "What's on your mind?")
r.Nil(tgram.LastSentKeyboard) // No keyboard should be sent
}
func TestShowToday_NormalMode(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
// Default mode (not notes-only, journal-only, or one-file-only)
err = cfg.SetMode(userconfig.ModeFull)
r.NoError(err)
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.ShowHome(nil)
r.NoError(err)
// Should show empty today list
r.Equal("🌴 Nothing here yet - send me something!", tgram.LastSentText)
}
func TestShowToday_NormalModeWithTasks(t *testing.T) {
r := require.New(t)
savedCtime := fs.Ctime
defer func() {
fs.Ctime = savedCtime
}()
fs.Ctime = func(fi os.FileInfo) int64 {
return 0
}
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
err = userFS.Write("/", "Chat.md", "- [ ] test task")
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
err = cfg.SetMode(userconfig.ModeFull)
r.NoError(err)
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.ShowHome(nil)
r.NoError(err)
r.Contains(tgram.LastSentText, "1")
r.Contains(tgram.LastSentText, "item")
r.Len(tgram.LastSentKeyboard.Btns, 1)
}
// TestShowToday_InboxMixedFormat confirms ShowToday renders inbox entries in
// both the timestamped `- [ ] HH:MM body` and timestamp-less `- [ ] body`
// forms, and hides completed `- [x]` entries while keeping the on-disk index
// stable so callback params (Complete, MoveTo, ...) still point at the right line.
func TestShowToday_InboxMixedFormat(t *testing.T) {
r := require.New(t)
savedCtime := fs.Ctime
defer func() { fs.Ctime = savedCtime }()
fs.Ctime = func(fi os.FileInfo) int64 { return 0 }
savedNow := now
defer func() { now = savedNow }()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
// Inbox has three entries at on-disk positions 0, 1, 2:
// 0: no-timestamp form `- [ ] body` -> shown
// 1: timestamped form `- [ ] HH:MM body` -> shown
// 2: completed `- [x] HH:MM body` -> hidden
inbox := "#### 1 January, Thursday\n" +
"- [ ] Plain msg\n" +
"- [ ] `09:05` New msg\n" +
"- [x] `09:10` Done msg\n"
err = userFS.Write(fs.DirUserRoot, fs.ChatFilename, inbox)
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
err = cfg.SetMode(userconfig.ModeFull)
r.NoError(err)
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.ShowHome(nil)
r.NoError(err)
// Label: 2 tasks left (plain + timestamped unchecked); completed one excluded.
r.Equal("<b>2</b> items"+wideSpacer, tgram.LastSentText)
// Two rows, each with one button. The completed entry (disk position 2) is
// not rendered but its slot is preserved — the next fresh entry added to
// the inbox would be position 3, not 2.
r.Len(tgram.LastSentKeyboard.Btns, 2)
firstBtn, ok := tgram.LastSentKeyboard.Btns[0].(tg.Btn)
r.True(ok)
r.Equal(tg.Cmd{Name: CmdComplete, Params: []string{chatBlockHash("- [ ] Plain msg")}, Type: "cmd"}, firstBtn.Cmd)
r.Contains(firstBtn.Name, "Plain msg")
secondBtn, ok := tgram.LastSentKeyboard.Btns[1].(tg.Btn)
r.True(ok)
// The completed `- [x] `09:10` Done msg` entry is hidden, but the
// remaining unchecked entry keeps its own stable hash (unchanged by
// completion-toggle behaviour).
r.Equal(tg.Cmd{Name: CmdComplete, Params: []string{chatBlockHash("- [ ] `09:05` New msg")}, Type: "cmd"}, secondBtn.Cmd)
r.Contains(secondBtn.Name, "New msg")
}
func TestShowToday_TodayCommandModeJournal(t *testing.T) {
r := require.New(t)
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
tgram = tg.NewFakeTG()
err = cfg.SetMode(userconfig.ModeJournal)
r.NoError(err)
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("home", nil)))
r.NoError(err)
r.Contains(tgram.LastSentText, "What's on your mind?")
}
func TestScheduleForTmrw(t *testing.T) {
r := require.New(t)
savedNow := now
defer func() {
now = savedNow
}()
now = func() time.Time {
return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
userFS, err := fs.NewFS("/", afero.NewMemMapFs())
r.NoError(err)
err = userFS.CreateSystemDirs()
r.NoError(err)
tgram := tg.NewFakeTG()
cfg := fakeConfig()
bot := NewBot(-1, tgram, userFS, db.NewFakeDB(), cfg)
err = bot.Reply(tg.NewUpd(-1, "Task for tomorrow"))
r.NoError(err)
err = bot.Reply(tg.NewUpdCmd(-1, tg.NewCmd("sc_tmrw", []string{inboxMsgHash(t, userFS, 0)})))
r.NoError(err)
laterMD, err := userFS.Read(fs.DirUserRoot, fs.LaterFilename)
r.NoError(err)
items, _ := txt.ChecklistItems(laterMD)
r.Contains(items, "Task for tomorrow")
sc, err := cfg.Schedules()
r.NoError(err)
r.Len(sc, 1)
r.Equal("Task for tomorrow", sc[0].Filename)
r.Equal(int64(86400), sc[0].ScheduledAt)
r.Equal("", sc[0].Cron)
}