fix references

This commit is contained in:
nkanaev
2026-06-09 16:35:14 +01:00
parent dee386b586
commit 3f10371975
12 changed files with 172 additions and 156 deletions

View File

@@ -9,24 +9,24 @@ type Storage interface {
Migrate() error Migrate() error
CountItems() int CountItems() int
CreateFeed(params model.CreateFeedParams) *model.Feed CreateFeed(params model.CreateFeedParams) *model.Feed
CreateFolder(title string) *Folder CreateFolder(title string) *model.Folder
CreateItems(items []Item) bool CreateItems(items []model.Item) bool
DeleteFeed(feedId int64) bool DeleteFeed(feedId int64) bool
DeleteFolder(folderId int64) bool DeleteFolder(folderId int64) bool
DeleteOldItems() DeleteOldItems()
FeedStats() []FeedStat FeedStats() []model.FeedStat
GetFeed(id int64) *model.Feed GetFeed(id int64) *model.Feed
GetFeedState(feedID int64) (*FeedState, error) GetFeedState(feedID int64) (*model.FeedState, error)
GetItem(id int64) *Item GetItem(id int64) *model.Item
GetSettings() Settings GetSettings() model.Settings
ListFeedStates() ([]FeedState, error) ListFeedStates() ([]model.FeedState, error)
ListFeeds() []model.Feed ListFeeds() []model.Feed
ListFolders() []model.Folder ListFolders() []model.Folder
ListItems(filter ItemFilter, limit int, newestFirst bool, withContent bool) []Item ListItems(filter model.ItemFilter, limit int, newestFirst bool, withContent bool) []model.Item
MarkItemsRead(filter MarkFilter) bool MarkItemsRead(filter model.MarkFilter) bool
UpdateFeed(feedId int64, params UpdateFeedParams) (bool, error) UpdateFeed(feedId int64, params model.UpdateFeedParams) (bool, error)
UpdateFeedState(feedID int64, params UpdateFeedStateParams) (bool, error) UpdateFeedState(feedID int64, params model.UpdateFeedStateParams) (bool, error)
UpdateFolder(folderId int64, params UpdateFolderParams) (bool, error) UpdateFolder(folderId int64, params model.UpdateFolderParams) (bool, error)
UpdateItemStatus(item_id int64, status ItemStatus) bool UpdateItemStatus(item_id int64, status model.ItemStatus) bool
UpdateSettings(params UpdateSettingsParams) bool UpdateSettings(params model.UpdateSettingsParams) bool
} }

View File

@@ -108,6 +108,12 @@ type UpdateFolderParams struct {
IsExpanded *bool IsExpanded *bool
} }
type FeedStat struct {
FeedId int64 `json:"feed_id"`
UnreadCount int64 `json:"unread"`
StarredCount int64 `json:"starred"`
}
type Settings struct { type Settings struct {
Filter string `json:"filter"` Filter string `json:"filter"`

View File

@@ -30,7 +30,7 @@ func (s *SQLiteStorage) CreateFeed(params model.CreateFeedParams) *model.Feed {
log.Print(err) log.Print(err)
return nil return nil
} }
return &Feed{ return &model.Feed{
Id: id, Id: id,
Title: title, Title: title,
Description: params.Description, Description: params.Description,
@@ -56,7 +56,7 @@ func (s *SQLiteStorage) DeleteFeed(feedId int64) bool {
return nrows == 1 return nrows == 1
} }
func (s *SQLiteStorage) UpdateFeed(feedId int64, params UpdateFeedParams) (bool, error) { func (s *SQLiteStorage) UpdateFeed(feedId int64, params model.UpdateFeedParams) (bool, error) {
_, err := s.db.Exec(` _, err := s.db.Exec(`
update feeds set update feeds set
title = coalesce(:title, title), title = coalesce(:title, title),
@@ -112,8 +112,8 @@ func (s *SQLiteStorage) ListFeeds() []model.Feed {
return result return result
} }
func (s *SQLiteStorage) GetFeed(id int64) *Feed { func (s *SQLiteStorage) GetFeed(id int64) *model.Feed {
var f Feed var f model.Feed
err := s.db.QueryRow(` err := s.db.QueryRow(`
select select
id, folder_id, title, link, feed_link, id, folder_id, title, link, feed_link,

View File

@@ -3,11 +3,13 @@ package sqlite
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/nkanaev/yarr/src/storage/model"
) )
func TestCreateFeed(t *testing.T) { func TestCreateFeed(t *testing.T) {
db := testDB() db := testDB()
feed1 := db.CreateFeed(CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example.com/feed.xml"}) feed1 := db.CreateFeed(model.CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example.com/feed.xml"})
if feed1 == nil || feed1.Id == 0 { if feed1 == nil || feed1.Id == 0 {
t.Fatal("expected feed") t.Fatal("expected feed")
} }
@@ -19,16 +21,16 @@ func TestCreateFeed(t *testing.T) {
func TestCreateFeedSameLink(t *testing.T) { func TestCreateFeedSameLink(t *testing.T) {
db := testDB() db := testDB()
feed1 := db.CreateFeed(CreateFeedParams{Title: "title", FeedLink: "http://example1.com/feed.xml"}) feed1 := db.CreateFeed(model.CreateFeedParams{Title: "title", FeedLink: "http://example1.com/feed.xml"})
if feed1 == nil || feed1.Id == 0 { if feed1 == nil || feed1.Id == 0 {
t.Fatal("expected feed") t.Fatal("expected feed")
} }
for range 10 { for range 10 {
db.CreateFeed(CreateFeedParams{Title: "title", FeedLink: "http://example2.com/feed.xml"}) db.CreateFeed(model.CreateFeedParams{Title: "title", FeedLink: "http://example2.com/feed.xml"})
} }
feed2 := db.CreateFeed(CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example1.com/feed.xml"}) feed2 := db.CreateFeed(model.CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example1.com/feed.xml"})
if feed1.Id != feed2.Id { if feed1.Id != feed2.Id {
t.Fatalf("expected the same feed.\nwant: %#v\nhave: %#v", feed1, feed2) t.Fatalf("expected the same feed.\nwant: %#v\nhave: %#v", feed1, feed2)
} }
@@ -40,25 +42,25 @@ func TestReadFeed(t *testing.T) {
t.Fatal("cannot get nonexistent feed") t.Fatal("cannot get nonexistent feed")
} }
feed1 := db.CreateFeed(CreateFeedParams{Title: "feed 1", Link: "http://example1.com", FeedLink: "http://example1.com/feed.xml"}) feed1 := db.CreateFeed(model.CreateFeedParams{Title: "feed 1", Link: "http://example1.com", FeedLink: "http://example1.com/feed.xml"})
feed2 := db.CreateFeed(CreateFeedParams{Title: "feed 2", Link: "http://example2.com", FeedLink: "http://example2.com/feed.xml"}) feed2 := db.CreateFeed(model.CreateFeedParams{Title: "feed 2", Link: "http://example2.com", FeedLink: "http://example2.com/feed.xml"})
feeds := db.ListFeeds() feeds := db.ListFeeds()
if !reflect.DeepEqual(feeds, []Feed{*feed1, *feed2}) { if !reflect.DeepEqual(feeds, []model.Feed{*feed1, *feed2}) {
t.Fatalf("invalid feed list: %#v", feeds) t.Fatalf("invalid feed list: %#v", feeds)
} }
} }
func TestUpdateFeed(t *testing.T) { func TestUpdateFeed(t *testing.T) {
db := testDB() db := testDB()
feed1 := db.CreateFeed(CreateFeedParams{Title: "feed 1", Link: "http://example1.com", FeedLink: "http://example1.com/feed.xml"}) feed1 := db.CreateFeed(model.CreateFeedParams{Title: "feed 1", Link: "http://example1.com", FeedLink: "http://example1.com/feed.xml"})
folder := db.CreateFolder("test") folder := db.CreateFolder("test")
icon := []byte("icon") icon := []byte("icon")
title := "newtitle" title := "newtitle"
db.UpdateFeed(feed1.Id, UpdateFeedParams{ db.UpdateFeed(feed1.Id, model.UpdateFeedParams{
Title: &title, Title: &title,
FolderID: SetNullable(&folder.Id), FolderID: model.SetNullable(&folder.Id),
Icon: SetNullable(&icon), Icon: model.SetNullable(&icon),
}) })
feed2 := db.GetFeed(feed1.Id) feed2 := db.GetFeed(feed1.Id)
@@ -75,7 +77,7 @@ func TestUpdateFeed(t *testing.T) {
func TestDeleteFeed(t *testing.T) { func TestDeleteFeed(t *testing.T) {
db := testDB() db := testDB()
feed1 := db.CreateFeed(CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example.com/feed.xml"}) feed1 := db.CreateFeed(model.CreateFeedParams{Title: "title", Link: "http://example.com", FeedLink: "http://example.com/feed.xml"})
if db.DeleteFeed(100500) { if db.DeleteFeed(100500) {
t.Error("cannot delete what does not exist") t.Error("cannot delete what does not exist")

View File

@@ -2,10 +2,11 @@ package sqlite
import ( import (
"database/sql" "database/sql"
"time"
"github.com/nkanaev/yarr/src/storage/model"
) )
func (s *SQLiteStorage) ListFeedStates() ([]FeedState, error) { func (s *SQLiteStorage) ListFeedStates() ([]model.FeedState, error) {
rows, err := s.db.Query(` rows, err := s.db.Query(`
select select
feed_id feed_id
@@ -20,9 +21,9 @@ func (s *SQLiteStorage) ListFeedStates() ([]FeedState, error) {
} }
defer rows.Close() defer rows.Close()
states := make([]FeedState, 0) states := make([]model.FeedState, 0)
for rows.Next() { for rows.Next() {
var state FeedState var state model.FeedState
err := rows.Scan( err := rows.Scan(
&state.FeedID, &state.FeedID,
&state.LastRefreshed, &state.LastRefreshed,
@@ -38,8 +39,8 @@ func (s *SQLiteStorage) ListFeedStates() ([]FeedState, error) {
return states, nil return states, nil
} }
func (s *SQLiteStorage) GetFeedState(feedID int64) (*FeedState, error) { func (s *SQLiteStorage) GetFeedState(feedID int64) (*model.FeedState, error) {
var state FeedState var state model.FeedState
err := s.db.QueryRow(` err := s.db.QueryRow(`
select select
feed_id feed_id
@@ -64,7 +65,7 @@ func (s *SQLiteStorage) GetFeedState(feedID int64) (*FeedState, error) {
return &state, nil return &state, nil
} }
func (s *SQLiteStorage) UpdateFeedState(feedID int64, params UpdateFeedStateParams) (bool, error) { func (s *SQLiteStorage) UpdateFeedState(feedID int64, params model.UpdateFeedStateParams) (bool, error) {
lastError := params.LastError lastError := params.LastError
if lastError != nil && *lastError == "" { if lastError != nil && *lastError == "" {
lastError = nil lastError = nil

View File

@@ -3,20 +3,22 @@ package sqlite
import ( import (
"testing" "testing"
"time" "time"
"github.com/nkanaev/yarr/src/storage/model"
) )
func TestUpdateFeedState_Full(t *testing.T) { func TestUpdateFeedState_Full(t *testing.T) {
s := testDB() s := testDB()
defer s.Close() defer s.Close()
f := s.CreateFeed(CreateFeedParams{Title: "Test", FeedLink: "http://example.com"}) f := s.CreateFeed(model.CreateFeedParams{Title: "Test", FeedLink: "http://example.com"})
now := time.Now().UTC().Truncate(time.Second) now := time.Now().UTC().Truncate(time.Second)
errMsg := "error" errMsg := "error"
lmod := "today" lmod := "today"
etag := "v1" etag := "v1"
ok, err := s.UpdateFeedState(f.Id, UpdateFeedStateParams{ ok, err := s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{
LastRefreshed: &now, LastRefreshed: &now,
LastError: &errMsg, LastError: &errMsg,
HTTPLastModified: &lmod, HTTPLastModified: &lmod,
@@ -54,12 +56,12 @@ func TestUpdateFeedState_Partial(t *testing.T) {
s := testDB() s := testDB()
defer s.Close() defer s.Close()
f := s.CreateFeed(CreateFeedParams{Title: "Test", FeedLink: "http://example.com"}) f := s.CreateFeed(model.CreateFeedParams{Title: "Test", FeedLink: "http://example.com"})
etag := "v1" etag := "v1"
s.UpdateFeedState(f.Id, UpdateFeedStateParams{HTTPEtag: &etag}) s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{HTTPEtag: &etag})
newErr := "new error" newErr := "new error"
_, err := s.UpdateFeedState(f.Id, UpdateFeedStateParams{ _, err := s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{
LastError: &newErr, LastError: &newErr,
}) })
if err != nil { if err != nil {
@@ -82,12 +84,12 @@ func TestUpdateFeedState_ClearError(t *testing.T) {
s := testDB() s := testDB()
defer s.Close() defer s.Close()
f := s.CreateFeed(CreateFeedParams{Title: "Test", FeedLink: "http://example.com"}) f := s.CreateFeed(model.CreateFeedParams{Title: "Test", FeedLink: "http://example.com"})
errMsg := "error" errMsg := "error"
s.UpdateFeedState(f.Id, UpdateFeedStateParams{LastError: &errMsg}) s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{LastError: &errMsg})
empty := "" empty := ""
_, err := s.UpdateFeedState(f.Id, UpdateFeedStateParams{ _, err := s.UpdateFeedState(f.Id, model.UpdateFeedStateParams{
LastError: &empty, LastError: &empty,
}) })
if err != nil { if err != nil {
@@ -107,12 +109,12 @@ func TestListFeedStates(t *testing.T) {
s := testDB() s := testDB()
defer s.Close() defer s.Close()
f1 := s.CreateFeed(CreateFeedParams{Title: "F1", FeedLink: "L1"}) f1 := s.CreateFeed(model.CreateFeedParams{Title: "F1", FeedLink: "L1"})
f2 := s.CreateFeed(CreateFeedParams{Title: "F2", FeedLink: "L2"}) f2 := s.CreateFeed(model.CreateFeedParams{Title: "F2", FeedLink: "L2"})
errMsg := "fail" errMsg := "fail"
s.UpdateFeedState(f1.Id, UpdateFeedStateParams{LastError: &errMsg}) s.UpdateFeedState(f1.Id, model.UpdateFeedStateParams{LastError: &errMsg})
s.UpdateFeedState(f2.Id, UpdateFeedStateParams{HTTPEtag: ptr("e")}) s.UpdateFeedState(f2.Id, model.UpdateFeedStateParams{HTTPEtag: ptr("e")})
states, err := s.ListFeedStates() states, err := s.ListFeedStates()
if err != nil { if err != nil {

View File

@@ -3,9 +3,11 @@ package sqlite
import ( import (
"database/sql" "database/sql"
"log" "log"
"github.com/nkanaev/yarr/src/storage/model"
) )
func (s *SQLiteStorage) CreateFolder(title string) *Folder { func (s *SQLiteStorage) CreateFolder(title string) *model.Folder {
expanded := true expanded := true
row := s.db.QueryRow(` row := s.db.QueryRow(`
insert into folders (title, is_expanded) values (:title, :is_expanded) insert into folders (title, is_expanded) values (:title, :is_expanded)
@@ -21,7 +23,7 @@ func (s *SQLiteStorage) CreateFolder(title string) *Folder {
log.Print(err) log.Print(err)
return nil return nil
} }
return &Folder{Id: id, Title: title, IsExpanded: expanded} return &model.Folder{Id: id, Title: title, IsExpanded: expanded}
} }
func (s *SQLiteStorage) DeleteFolder(folderId int64) bool { func (s *SQLiteStorage) DeleteFolder(folderId int64) bool {
@@ -32,7 +34,7 @@ func (s *SQLiteStorage) DeleteFolder(folderId int64) bool {
return err == nil return err == nil
} }
func (s *SQLiteStorage) UpdateFolder(folderId int64, params UpdateFolderParams) (bool, error) { func (s *SQLiteStorage) UpdateFolder(folderId int64, params model.UpdateFolderParams) (bool, error) {
_, err := s.db.Exec(` _, err := s.db.Exec(`
update folders set update folders set
title = coalesce(:title, title), title = coalesce(:title, title),
@@ -50,8 +52,8 @@ func (s *SQLiteStorage) UpdateFolder(folderId int64, params UpdateFolderParams)
return true, nil return true, nil
} }
func (s *SQLiteStorage) ListFolders() []Folder { func (s *SQLiteStorage) ListFolders() []model.Folder {
result := make([]Folder, 0) result := make([]model.Folder, 0)
rows, err := s.db.Query(` rows, err := s.db.Query(`
select id, title, is_expanded select id, title, is_expanded
from folders from folders
@@ -62,7 +64,7 @@ func (s *SQLiteStorage) ListFolders() []Folder {
return result return result
} }
for rows.Next() { for rows.Next() {
var f Folder var f model.Folder
err = rows.Scan(&f.Id, &f.Title, &f.IsExpanded) err = rows.Scan(&f.Id, &f.Title, &f.IsExpanded)
if err != nil { if err != nil {
log.Print(err) log.Print(err)

View File

@@ -2,6 +2,8 @@ package sqlite
import ( import (
"testing" "testing"
"github.com/nkanaev/yarr/src/storage/model"
) )
func TestUpdateFolder(t *testing.T) { func TestUpdateFolder(t *testing.T) {
@@ -13,7 +15,7 @@ func TestUpdateFolder(t *testing.T) {
t.Run("rename only", func(t *testing.T) { t.Run("rename only", func(t *testing.T) {
newTitle := "new title" newTitle := "new title"
ok, err := db.UpdateFolder(folder.Id, UpdateFolderParams{ ok, err := db.UpdateFolder(folder.Id, model.UpdateFolderParams{
Title: &newTitle, Title: &newTitle,
}) })
if !ok || err != nil { if !ok || err != nil {
@@ -31,7 +33,7 @@ func TestUpdateFolder(t *testing.T) {
t.Run("toggle expanded only", func(t *testing.T) { t.Run("toggle expanded only", func(t *testing.T) {
isExpanded := false isExpanded := false
ok, err := db.UpdateFolder(folder.Id, UpdateFolderParams{ ok, err := db.UpdateFolder(folder.Id, model.UpdateFolderParams{
IsExpanded: &isExpanded, IsExpanded: &isExpanded,
}) })
if !ok || err != nil { if !ok || err != nil {
@@ -50,7 +52,7 @@ func TestUpdateFolder(t *testing.T) {
t.Run("update both", func(t *testing.T) { t.Run("update both", func(t *testing.T) {
bothTitle := "both" bothTitle := "both"
isExpanded := true isExpanded := true
ok, err := db.UpdateFolder(folder.Id, UpdateFolderParams{ ok, err := db.UpdateFolder(folder.Id, model.UpdateFolderParams{
Title: &bothTitle, Title: &bothTitle,
IsExpanded: &isExpanded, IsExpanded: &isExpanded,
}) })
@@ -65,7 +67,7 @@ func TestUpdateFolder(t *testing.T) {
}) })
t.Run("update none", func(t *testing.T) { t.Run("update none", func(t *testing.T) {
ok, err := db.UpdateFolder(folder.Id, UpdateFolderParams{}) ok, err := db.UpdateFolder(folder.Id, model.UpdateFolderParams{})
if !ok || err != nil { if !ok || err != nil {
t.Fatalf("UpdateFolder failed: %v", err) t.Fatalf("UpdateFolder failed: %v", err)
} }

View File

@@ -14,7 +14,8 @@ import (
"github.com/nkanaev/yarr/src/storage/model" "github.com/nkanaev/yarr/src/storage/model"
) )
// TODO: serialize/deserialize type MediaLinks model.MediaLinks
func (m *MediaLinks) Scan(src any) error { func (m *MediaLinks) Scan(src any) error {
switch data := src.(type) { switch data := src.(type) {
case []byte: case []byte:
@@ -30,7 +31,7 @@ func (m MediaLinks) Value() (driver.Value, error) {
return json.Marshal(m) return json.Marshal(m)
} }
func (s *SQLiteStorage) CreateItems(items []Item) bool { func (s *SQLiteStorage) CreateItems(items []model.Item) bool {
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@@ -65,7 +66,7 @@ func (s *SQLiteStorage) CreateItems(items []Item) bool {
sql.Named("link", item.Link), sql.Named("link", item.Link),
sql.Named("date", item.Date), sql.Named("date", item.Date),
sql.Named("content", item.Content), sql.Named("content", item.Content),
sql.Named("media_links", item.MediaLinks), sql.Named("media_links", MediaLinks(item.MediaLinks)),
sql.Named("date_arrived", now), sql.Named("date_arrived", now),
sql.Named("last_arrived", now), sql.Named("last_arrived", now),
sql.Named("status", model.UNREAD), sql.Named("status", model.UNREAD),
@@ -86,7 +87,7 @@ func (s *SQLiteStorage) CreateItems(items []Item) bool {
return true return true
} }
func listQueryPredicate(filter ItemFilter, newestFirst bool) (string, []any) { func listQueryPredicate(filter model.ItemFilter, newestFirst bool) (string, []any) {
cond := make([]string, 0) cond := make([]string, 0)
args := make([]any, 0) args := make([]any, 0)
if filter.FolderID != nil { if filter.FolderID != nil {
@@ -169,13 +170,13 @@ func (s *SQLiteStorage) CountItems() int {
} }
func (s *SQLiteStorage) ListItems( func (s *SQLiteStorage) ListItems(
filter ItemFilter, filter model.ItemFilter,
limit int, limit int,
newestFirst bool, newestFirst bool,
withContent bool, withContent bool,
) []Item { ) []model.Item {
predicate, args := listQueryPredicate(filter, newestFirst) predicate, args := listQueryPredicate(filter, newestFirst)
result := make([]Item, 0) result := make([]model.Item, 0)
order := "date desc, id desc" order := "date desc, id desc"
if !newestFirst { if !newestFirst {
@@ -207,11 +208,11 @@ func (s *SQLiteStorage) ListItems(
return result return result
} }
for rows.Next() { for rows.Next() {
var x Item var x model.Item
err = rows.Scan( err = rows.Scan(
&x.Id, &x.GUID, &x.FeedId, &x.Id, &x.GUID, &x.FeedId,
&x.Title, &x.Link, &x.Date, &x.Title, &x.Link, &x.Date,
&x.Status, &x.MediaLinks, &x.Content, &x.Status, (*MediaLinks)(&x.MediaLinks), &x.Content,
) )
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@@ -222,8 +223,8 @@ func (s *SQLiteStorage) ListItems(
return result return result
} }
func (s *SQLiteStorage) GetItem(id int64) *Item { func (s *SQLiteStorage) GetItem(id int64) *model.Item {
i := &Item{} i := &model.Item{}
err := s.db.QueryRow(` err := s.db.QueryRow(`
select select
i.id, i.guid, i.feed_id, i.title, i.link, i.content, i.id, i.guid, i.feed_id, i.title, i.link, i.content,
@@ -232,7 +233,7 @@ func (s *SQLiteStorage) GetItem(id int64) *Item {
where i.id = :id where i.id = :id
`, sql.Named("id", id)).Scan( `, sql.Named("id", id)).Scan(
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content, &i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
&i.Date, &i.Status, &i.MediaLinks, &i.Date, &i.Status, (*MediaLinks)(&i.MediaLinks),
) )
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@@ -241,7 +242,7 @@ func (s *SQLiteStorage) GetItem(id int64) *Item {
return i return i
} }
func (s *SQLiteStorage) UpdateItemStatus(item_id int64, status ItemStatus) bool { func (s *SQLiteStorage) UpdateItemStatus(item_id int64, status model.ItemStatus) bool {
_, err := s.db.Exec(`update items set status = :status where id = :id`, _, err := s.db.Exec(`update items set status = :status where id = :id`,
sql.Named("status", status), sql.Named("status", status),
sql.Named("id", item_id), sql.Named("id", item_id),
@@ -249,8 +250,8 @@ func (s *SQLiteStorage) UpdateItemStatus(item_id int64, status ItemStatus) bool
return err == nil return err == nil
} }
func (s *SQLiteStorage) MarkItemsRead(filter MarkFilter) bool { func (s *SQLiteStorage) MarkItemsRead(filter model.MarkFilter) bool {
predicate, args := listQueryPredicate(ItemFilter{ predicate, args := listQueryPredicate(model.ItemFilter{
FolderID: filter.FolderID, FolderID: filter.FolderID,
FeedID: filter.FeedID, FeedID: filter.FeedID,
Before: filter.Before, Before: filter.Before,
@@ -258,7 +259,7 @@ func (s *SQLiteStorage) MarkItemsRead(filter MarkFilter) bool {
query := fmt.Sprintf(` query := fmt.Sprintf(`
update items as i set status = %d update items as i set status = %d
where %s and i.status != %d where %s and i.status != %d
`, READ, predicate, STARRED) `, model.READ, predicate, model.STARRED)
_, err := s.db.Exec(query, args...) _, err := s.db.Exec(query, args...)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@@ -266,14 +267,8 @@ func (s *SQLiteStorage) MarkItemsRead(filter MarkFilter) bool {
return err == nil return err == nil
} }
type FeedStat struct { func (s *SQLiteStorage) FeedStats() []model.FeedStat {
FeedId int64 `json:"feed_id"` result := make([]model.FeedStat, 0)
UnreadCount int64 `json:"unread"`
StarredCount int64 `json:"starred"`
}
func (s *SQLiteStorage) FeedStats() []FeedStat {
result := make([]FeedStat, 0)
rows, err := s.db.Query(fmt.Sprintf(` rows, err := s.db.Query(fmt.Sprintf(`
select select
feed_id, feed_id,
@@ -281,13 +276,13 @@ func (s *SQLiteStorage) FeedStats() []FeedStat {
sum(case status when %d then 1 else 0 end) sum(case status when %d then 1 else 0 end)
from items from items
group by feed_id group by feed_id
`, UNREAD, STARRED)) `, model.UNREAD, model.STARRED))
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return result return result
} }
for rows.Next() { for rows.Next() {
stat := FeedStat{} stat := model.FeedStat{}
rows.Scan(&stat.FeedId, &stat.UnreadCount, &stat.StarredCount) rows.Scan(&stat.FeedId, &stat.UnreadCount, &stat.StarredCount)
result = append(result, stat) result = append(result, stat)
} }
@@ -322,7 +317,7 @@ func (s *SQLiteStorage) DeleteOldItems() {
where rn > :keep_size where rn > :keep_size
and last_arrived < datetime(max_la, :keep_days_limit) and last_arrived < datetime(max_la, :keep_days_limit)
)`, )`,
sql.Named("starred_status", STARRED), sql.Named("starred_status", model.STARRED),
sql.Named("keep_size", itemsKeepSize), sql.Named("keep_size", itemsKeepSize),
sql.Named("keep_days_limit", fmt.Sprintf("-%d days", itemsKeepDays)), sql.Named("keep_days_limit", fmt.Sprintf("-%d days", itemsKeepDays)),
) )

View File

@@ -8,6 +8,8 @@ import (
"testing" "testing"
"testing/synctest" "testing/synctest"
"time" "time"
"github.com/nkanaev/yarr/src/storage/model"
) )
/* /*
@@ -30,22 +32,22 @@ import (
*/ */
type testItemScope struct { type testItemScope struct {
feed11, feed12 *Feed feed11, feed12 *model.Feed
feed21, feed01 *Feed feed21, feed01 *model.Feed
folder1, folder2 *Folder folder1, folder2 *model.Folder
} }
func testItemsSetup(db *SQLiteStorage) testItemScope { func testItemsSetup(db *SQLiteStorage) testItemScope {
folder1 := db.CreateFolder("folder1") folder1 := db.CreateFolder("folder1")
folder2 := db.CreateFolder("folder2") folder2 := db.CreateFolder("folder2")
feed11 := db.CreateFeed(CreateFeedParams{Title: "feed11", FeedLink: "http://test.com/feed11.xml", FolderID: &folder1.Id}) feed11 := db.CreateFeed(model.CreateFeedParams{Title: "feed11", FeedLink: "http://test.com/feed11.xml", FolderID: &folder1.Id})
feed12 := db.CreateFeed(CreateFeedParams{Title: "feed12", FeedLink: "http://test.com/feed12.xml", FolderID: &folder1.Id}) feed12 := db.CreateFeed(model.CreateFeedParams{Title: "feed12", FeedLink: "http://test.com/feed12.xml", FolderID: &folder1.Id})
feed21 := db.CreateFeed(CreateFeedParams{Title: "feed21", FeedLink: "http://test.com/feed21.xml", FolderID: &folder2.Id}) feed21 := db.CreateFeed(model.CreateFeedParams{Title: "feed21", FeedLink: "http://test.com/feed21.xml", FolderID: &folder2.Id})
feed01 := db.CreateFeed(CreateFeedParams{Title: "feed01", FeedLink: "http://test.com/feed01.xml"}) feed01 := db.CreateFeed(model.CreateFeedParams{Title: "feed01", FeedLink: "http://test.com/feed01.xml"})
now := time.Now() now := time.Now()
db.CreateItems([]Item{ db.CreateItems([]model.Item{
// feed11 // feed11
{GUID: "item111", FeedId: feed11.Id, Title: "title111", Date: now.Add(time.Hour * 24 * 1)}, {GUID: "item111", FeedId: feed11.Id, Title: "title111", Date: now.Add(time.Hour * 24 * 1)},
{ {
@@ -98,11 +100,11 @@ func testItemsSetup(db *SQLiteStorage) testItemScope {
}) })
db.db.Exec( db.db.Exec(
`update items set status = :status where guid in ("item112", "item122", "item211", "item012")`, `update items set status = :status where guid in ("item112", "item122", "item211", "item012")`,
sql.Named("status", READ), sql.Named("status", model.READ),
) )
db.db.Exec( db.db.Exec(
`update items set status = :status where guid in ("item113", "item212", "item013")`, `update items set status = :status where guid in ("item113", "item212", "item013")`,
sql.Named("status", STARRED), sql.Named("status", model.STARRED),
) )
return testItemScope{ return testItemScope{
@@ -115,8 +117,8 @@ func testItemsSetup(db *SQLiteStorage) testItemScope {
} }
} }
func getItem(db *SQLiteStorage, guid string) *Item { func getItem(db *SQLiteStorage, guid string) *model.Item {
i := &Item{} i := &model.Item{}
err := db.db.QueryRow(` err := db.db.QueryRow(`
select select
i.id, i.guid, i.feed_id, i.title, i.link, i.content, i.id, i.guid, i.feed_id, i.title, i.link, i.content,
@@ -125,7 +127,7 @@ func getItem(db *SQLiteStorage, guid string) *Item {
where i.guid = :guid where i.guid = :guid
`, sql.Named("guid", guid)).Scan( `, sql.Named("guid", guid)).Scan(
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content, &i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
&i.Date, &i.Status, &i.MediaLinks, &i.Date, &i.Status, (*MediaLinks)(&i.MediaLinks),
) )
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -133,7 +135,7 @@ func getItem(db *SQLiteStorage, guid string) *Item {
return i return i
} }
func getItemGuids(items []Item) []string { func getItemGuids(items []model.Item) []string {
guids := make([]string, 0) guids := make([]string, 0)
for _, item := range items { for _, item := range items {
guids = append(guids, item.GUID) guids = append(guids, item.GUID)
@@ -147,7 +149,7 @@ func TestListItems(t *testing.T) {
// filter by folder_id // filter by folder_id
have := getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder1.Id}, 10, false, false)) have := getItemGuids(db.ListItems(model.ItemFilter{FolderID: &scope.folder1.Id}, 10, false, false))
want := []string{"item111", "item112", "item113", "item121", "item122"} want := []string{"item111", "item112", "item113", "item121", "item122"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -155,7 +157,7 @@ func TestListItems(t *testing.T) {
t.Fail() t.Fail()
} }
have = getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder2.Id}, 10, false, false)) have = getItemGuids(db.ListItems(model.ItemFilter{FolderID: &scope.folder2.Id}, 10, false, false))
want = []string{"item211", "item212"} want = []string{"item211", "item212"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -165,7 +167,7 @@ func TestListItems(t *testing.T) {
// filter by feed_id // filter by feed_id
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed11.Id}, 10, false, false)) have = getItemGuids(db.ListItems(model.ItemFilter{FeedID: &scope.feed11.Id}, 10, false, false))
want = []string{"item111", "item112", "item113"} want = []string{"item111", "item112", "item113"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -173,7 +175,7 @@ func TestListItems(t *testing.T) {
t.Fail() t.Fail()
} }
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed01.Id}, 10, false, false)) have = getItemGuids(db.ListItems(model.ItemFilter{FeedID: &scope.feed01.Id}, 10, false, false))
want = []string{"item011", "item012", "item013"} want = []string{"item011", "item012", "item013"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -183,8 +185,8 @@ func TestListItems(t *testing.T) {
// filter by status // filter by status
var starred ItemStatus = STARRED var starred model.ItemStatus = model.STARRED
have = getItemGuids(db.ListItems(ItemFilter{Status: &starred}, 10, false, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Status: &starred}, 10, false, false))
want = []string{"item113", "item212", "item013"} want = []string{"item113", "item212", "item013"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -192,8 +194,8 @@ func TestListItems(t *testing.T) {
t.Fail() t.Fail()
} }
var unread ItemStatus = UNREAD var unread model.ItemStatus = model.UNREAD
have = getItemGuids(db.ListItems(ItemFilter{Status: &unread}, 10, false, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Status: &unread}, 10, false, false))
want = []string{"item111", "item121", "item011"} want = []string{"item111", "item121", "item011"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -203,7 +205,7 @@ func TestListItems(t *testing.T) {
// limit // limit
have = getItemGuids(db.ListItems(ItemFilter{}, 2, false, false)) have = getItemGuids(db.ListItems(model.ItemFilter{}, 2, false, false))
want = []string{"item111", "item112"} want = []string{"item111", "item112"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -213,7 +215,7 @@ func TestListItems(t *testing.T) {
// filter by search // filter by search
search1 := "title111" search1 := "title111"
have = getItemGuids(db.ListItems(ItemFilter{Search: &search1}, 4, true, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Search: &search1}, 4, true, false))
want = []string{"item111"} want = []string{"item111"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -222,7 +224,7 @@ func TestListItems(t *testing.T) {
} }
// sort by date // sort by date
have = getItemGuids(db.ListItems(ItemFilter{}, 4, true, false)) have = getItemGuids(db.ListItems(model.ItemFilter{}, 4, true, false))
want = []string{"item013", "item012", "item011", "item212"} want = []string{"item013", "item012", "item011", "item212"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -239,7 +241,7 @@ func TestListItemsPaginated(t *testing.T) {
item121 := getItem(db, "item121") item121 := getItem(db, "item121")
// all, newest first // all, newest first
have := getItemGuids(db.ListItems(ItemFilter{After: &item012.Id}, 3, true, false)) have := getItemGuids(db.ListItems(model.ItemFilter{After: &item012.Id}, 3, true, false))
want := []string{"item011", "item212", "item211"} want := []string{"item011", "item212", "item211"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
@@ -248,9 +250,9 @@ func TestListItemsPaginated(t *testing.T) {
} }
// unread, newest first // unread, newest first
unread := UNREAD unread := model.UNREAD
have = getItemGuids( have = getItemGuids(
db.ListItems(ItemFilter{After: &item012.Id, Status: &unread}, 3, true, false), db.ListItems(model.ItemFilter{After: &item012.Id, Status: &unread}, 3, true, false),
) )
want = []string{"item011", "item121", "item111"} want = []string{"item011", "item121", "item111"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
@@ -260,9 +262,9 @@ func TestListItemsPaginated(t *testing.T) {
} }
// starred, oldest first // starred, oldest first
starred := STARRED starred := model.STARRED
have = getItemGuids( have = getItemGuids(
db.ListItems(ItemFilter{After: &item121.Id, Status: &starred}, 3, false, false), db.ListItems(model.ItemFilter{After: &item121.Id, Status: &starred}, 3, false, false),
) )
want = []string{"item212", "item013"} want = []string{"item212", "item013"}
if !reflect.DeepEqual(have, want) { if !reflect.DeepEqual(have, want) {
@@ -274,12 +276,12 @@ func TestListItemsPaginated(t *testing.T) {
func TestMarkItemsRead(t *testing.T) { func TestMarkItemsRead(t *testing.T) {
// NOTE: starred items must not be marked as read // NOTE: starred items must not be marked as read
var read ItemStatus = READ var read model.ItemStatus = model.READ
db1 := testDB() db1 := testDB()
testItemsSetup(db1) testItemsSetup(db1)
db1.MarkItemsRead(MarkFilter{}) db1.MarkItemsRead(model.MarkFilter{})
have := getItemGuids(db1.ListItems(ItemFilter{Status: &read}, 10, false, false)) have := getItemGuids(db1.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
want := []string{ want := []string{
"item111", "item112", "item121", "item122", "item111", "item112", "item121", "item122",
"item211", "item011", "item012", "item211", "item011", "item012",
@@ -292,8 +294,8 @@ func TestMarkItemsRead(t *testing.T) {
db2 := testDB() db2 := testDB()
scope2 := testItemsSetup(db2) scope2 := testItemsSetup(db2)
db2.MarkItemsRead(MarkFilter{FolderID: &scope2.folder1.Id}) db2.MarkItemsRead(model.MarkFilter{FolderID: &scope2.folder1.Id})
have = getItemGuids(db2.ListItems(ItemFilter{Status: &read}, 10, false, false)) have = getItemGuids(db2.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
want = []string{ want = []string{
"item111", "item112", "item121", "item122", "item111", "item112", "item121", "item122",
"item211", "item012", "item211", "item012",
@@ -306,8 +308,8 @@ func TestMarkItemsRead(t *testing.T) {
db3 := testDB() db3 := testDB()
scope3 := testItemsSetup(db3) scope3 := testItemsSetup(db3)
db3.MarkItemsRead(MarkFilter{FeedID: &scope3.feed11.Id}) db3.MarkItemsRead(model.MarkFilter{FeedID: &scope3.feed11.Id})
have = getItemGuids(db3.ListItems(ItemFilter{Status: &read}, 10, false, false)) have = getItemGuids(db3.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
want = []string{ want = []string{
"item111", "item112", "item122", "item111", "item112", "item122",
"item211", "item012", "item211", "item012",
@@ -321,14 +323,14 @@ func TestMarkItemsRead(t *testing.T) {
func TestDeleteOldItems(t *testing.T) { func TestDeleteOldItems(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
starred := STARRED starred := model.STARRED
t.Run("keeps at least 50 items", func(t *testing.T) { t.Run("keeps at least 50 items", func(t *testing.T) {
db := testDB() db := testDB()
feed := db.CreateFeed(CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) feed := db.CreateFeed(model.CreateFeedParams{Title: "f", FeedLink: "http://f.xml"})
items := make([]Item, 100) items := make([]model.Item, 100)
for i := range 100 { for i := range 100 {
items[i] = Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Hour * 24)} items[i] = model.Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Hour * 24)}
} }
db.CreateItems(items) db.CreateItems(items)
@@ -346,10 +348,10 @@ func TestDeleteOldItems(t *testing.T) {
t.Run("keeps all less than 90 days old", func(t *testing.T) { t.Run("keeps all less than 90 days old", func(t *testing.T) {
db := testDB() db := testDB()
feed := db.CreateFeed(CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) feed := db.CreateFeed(model.CreateFeedParams{Title: "f", FeedLink: "http://f.xml"})
items := make([]Item, 100) items := make([]model.Item, 100)
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
items[i] = Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Second)} items[i] = model.Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Second)}
} }
db.CreateItems(items) db.CreateItems(items)
@@ -368,10 +370,10 @@ func TestDeleteOldItems(t *testing.T) {
t.Run("keeps starred", func(t *testing.T) { t.Run("keeps starred", func(t *testing.T) {
db := testDB() db := testDB()
feed := db.CreateFeed(CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) feed := db.CreateFeed(model.CreateFeedParams{Title: "f", FeedLink: "http://f.xml"})
items := make([]Item, 100) items := make([]model.Item, 100)
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
items[i] = Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Second)} items[i] = model.Item{GUID: strconv.Itoa(i), FeedId: feed.Id, Date: now.Add(time.Duration(i) * time.Second)}
} }
db.CreateItems(items) db.CreateItems(items)
@@ -397,9 +399,9 @@ func TestCreateItemsLastArrived(t *testing.T) {
synctest.Test(t, func(t *testing.T) { synctest.Test(t, func(t *testing.T) {
db := testDB() db := testDB()
defer db.db.Close() defer db.db.Close()
feed := db.CreateFeed(CreateFeedParams{Title: "test feed", FeedLink: "http://example.com/feed"}) feed := db.CreateFeed(model.CreateFeedParams{Title: "test feed", FeedLink: "http://example.com/feed"})
item := Item{ item := model.Item{
GUID: "item1", GUID: "item1",
FeedId: feed.Id, FeedId: feed.Id,
Title: "Title 1", Title: "Title 1",
@@ -407,7 +409,7 @@ func TestCreateItemsLastArrived(t *testing.T) {
} }
// 1. Initial creation // 1. Initial creation
db.CreateItems([]Item{item}) db.CreateItems([]model.Item{item})
var lastArrived1 time.Time var lastArrived1 time.Time
err := db.db.QueryRow("select last_arrived from items where guid = ?", item.GUID).Scan(&lastArrived1) err := db.db.QueryRow("select last_arrived from items where guid = ?", item.GUID).Scan(&lastArrived1)
@@ -418,7 +420,7 @@ func TestCreateItemsLastArrived(t *testing.T) {
time.Sleep(time.Second * 10) time.Sleep(time.Second * 10)
// 2. Update on conflict // 2. Update on conflict
db.CreateItems([]Item{item}) db.CreateItems([]model.Item{item})
var lastArrived2 time.Time var lastArrived2 time.Time
err = db.db.QueryRow("select last_arrived from items where guid = ?", item.GUID).Scan(&lastArrived2) err = db.db.QueryRow("select last_arrived from items where guid = ?", item.GUID).Scan(&lastArrived2)
@@ -435,9 +437,9 @@ func TestCreateItemsLastArrived(t *testing.T) {
func TestSearch(t *testing.T) { func TestSearch(t *testing.T) {
db := testDB() db := testDB()
defer db.Close() defer db.Close()
feed := db.CreateFeed(CreateFeedParams{Title: "f", FeedLink: "http://f.xml"}) feed := db.CreateFeed(model.CreateFeedParams{Title: "f", FeedLink: "http://f.xml"})
db.CreateItems([]Item{ db.CreateItems([]model.Item{
{ {
GUID: "i1", GUID: "i1",
FeedId: feed.Id, FeedId: feed.Id,
@@ -460,40 +462,40 @@ func TestSearch(t *testing.T) {
// 1. Basic search // 1. Basic search
s1 := "emergency" s1 := "emergency"
have := getItemGuids(db.ListItems(ItemFilter{Search: &s1}, 10, true, false)) have := getItemGuids(db.ListItems(model.ItemFilter{Search: &s1}, 10, true, false))
if !reflect.DeepEqual(have, []string{"i1"}) { if !reflect.DeepEqual(have, []string{"i1"}) {
t.Errorf("basic search failed: expected [i1], got %v", have) t.Errorf("basic search failed: expected [i1], got %v", have)
} }
// 2. HTML stripping: Should find text, but NOT the tags // 2. HTML stripping: Should find text, but NOT the tags
s2 := "test" s2 := "test"
have = getItemGuids(db.ListItems(ItemFilter{Search: &s2}, 10, true, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s2}, 10, true, false))
if !reflect.DeepEqual(have, []string{"i1"}) { if !reflect.DeepEqual(have, []string{"i1"}) {
t.Errorf("html text search failed: expected [i1], got %v", have) t.Errorf("html text search failed: expected [i1], got %v", have)
} }
s3 := "secret-class" s3 := "secret-class"
have = getItemGuids(db.ListItems(ItemFilter{Search: &s3}, 10, true, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s3}, 10, true, false))
if len(have) > 0 { if len(have) > 0 {
t.Errorf("html tag search should have failed but found: %v", have) t.Errorf("html tag search should have failed but found: %v", have)
} }
// 3. Multi-word (AND) // 3. Multi-word (AND)
s4 := "broadcast system" s4 := "broadcast system"
have = getItemGuids(db.ListItems(ItemFilter{Search: &s4}, 10, true, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s4}, 10, true, false))
if !reflect.DeepEqual(have, []string{"i1"}) { if !reflect.DeepEqual(have, []string{"i1"}) {
t.Errorf("multi-word search failed: expected [i1], got %v", have) t.Errorf("multi-word search failed: expected [i1], got %v", have)
} }
// 4. Unicode // 4. Unicode
s5 := "Привет" s5 := "Привет"
have = getItemGuids(db.ListItems(ItemFilter{Search: &s5}, 10, true, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s5}, 10, true, false))
if !reflect.DeepEqual(have, []string{"i2"}) { if !reflect.DeepEqual(have, []string{"i2"}) {
t.Errorf("unicode search failed: expected [i2], got %v", have) t.Errorf("unicode search failed: expected [i2], got %v", have)
} }
s6 := "世界" s6 := "世界"
have = getItemGuids(db.ListItems(ItemFilter{Search: &s6}, 10, true, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s6}, 10, true, false))
if !reflect.DeepEqual(have, []string{"i2"}) { if !reflect.DeepEqual(have, []string{"i2"}) {
t.Errorf("unicode search (CJK) failed: expected [i2], got %v", have) t.Errorf("unicode search (CJK) failed: expected [i2], got %v", have)
} }
@@ -501,14 +503,14 @@ func TestSearch(t *testing.T) {
// 5. Trigger: Update // 5. Trigger: Update
db.db.Exec("update items set title = 'Updated Title' where guid = 'i1'") db.db.Exec("update items set title = 'Updated Title' where guid = 'i1'")
s7 := "Updated" s7 := "Updated"
have = getItemGuids(db.ListItems(ItemFilter{Search: &s7}, 10, true, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s7}, 10, true, false))
if !reflect.DeepEqual(have, []string{"i1"}) { if !reflect.DeepEqual(have, []string{"i1"}) {
t.Errorf("update trigger failed: expected [i1], got %v", have) t.Errorf("update trigger failed: expected [i1], got %v", have)
} }
// 6. Trigger: Delete // 6. Trigger: Delete
db.db.Exec("delete from items where guid = 'i1'") db.db.Exec("delete from items where guid = 'i1'")
have = getItemGuids(db.ListItems(ItemFilter{Search: &s7}, 10, true, false)) have = getItemGuids(db.ListItems(model.ItemFilter{Search: &s7}, 10, true, false))
if len(have) > 0 { if len(have) > 0 {
t.Errorf("delete trigger failed: found deleted item: %v", have) t.Errorf("delete trigger failed: found deleted item: %v", have)
} }

View File

@@ -4,10 +4,12 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"log" "log"
"github.com/nkanaev/yarr/src/storage/model"
) )
func settingsDefaults() Settings { func settingsDefaults() model.Settings {
return Settings{ return model.Settings{
Filter: "", Filter: "",
Feed: "", Feed: "",
FeedListWidth: 300, FeedListWidth: 300,
@@ -21,7 +23,7 @@ func settingsDefaults() Settings {
} }
} }
func (s *SQLiteStorage) GetSettings() Settings { func (s *SQLiteStorage) GetSettings() model.Settings {
result := settingsDefaults() result := settingsDefaults()
rows, err := s.db.Query(`select key, val from settings;`) rows, err := s.db.Query(`select key, val from settings;`)
if err != nil { if err != nil {
@@ -61,7 +63,7 @@ func (s *SQLiteStorage) GetSettings() Settings {
return result return result
} }
func (s *SQLiteStorage) UpdateSettings(params UpdateSettingsParams) bool { func (s *SQLiteStorage) UpdateSettings(params model.UpdateSettingsParams) bool {
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
log.Print(err) log.Print(err)

View File

@@ -4,6 +4,8 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/nkanaev/yarr/src/storage/model"
) )
func TestSettingsDefaults(t *testing.T) { func TestSettingsDefaults(t *testing.T) {
@@ -22,7 +24,7 @@ func TestUpdateSettings(t *testing.T) {
s := testDB() s := testDB()
defer s.Close() defer s.Close()
params := UpdateSettingsParams{ params := model.UpdateSettingsParams{
ThemeName: ptr("night"), ThemeName: ptr("night"),
FeedListWidth: ptr(400), FeedListWidth: ptr(400),
RefreshRate: ptr(int64(15)), RefreshRate: ptr(int64(15)),
@@ -49,7 +51,7 @@ func TestGetSettings(t *testing.T) {
s := testDB() s := testDB()
defer s.Close() defer s.Close()
s.UpdateSettings(UpdateSettingsParams{Language: ptr("fr")}) s.UpdateSettings(model.UpdateSettingsParams{Language: ptr("fr")})
settings := s.GetSettings() settings := s.GetSettings()
if settings.Language != "fr" { if settings.Language != "fr" {
@@ -64,8 +66,8 @@ func TestSettingsExhaustive(t *testing.T) {
s := testDB() s := testDB()
defer s.Close() defer s.Close()
settingsType := reflect.TypeOf(Settings{}) settingsType := reflect.TypeOf(model.Settings{})
paramsType := reflect.TypeOf(UpdateSettingsParams{}) paramsType := reflect.TypeOf(model.UpdateSettingsParams{})
settings := s.GetSettings() settings := s.GetSettings()
m := settings.Map() m := settings.Map()
@@ -125,7 +127,7 @@ func TestSettingsExhaustive(t *testing.T) {
} }
} }
if ok := s.UpdateSettings(paramsValue.Interface().(UpdateSettingsParams)); !ok { if ok := s.UpdateSettings(paramsValue.Interface().(model.UpdateSettingsParams)); !ok {
t.Errorf("UpdateSettings failed for %q", jsonKey) t.Errorf("UpdateSettings failed for %q", jsonKey)
} }