mirror of
https://github.com/nkanaev/yarr.git
synced 2026-06-09 18:03:19 +00:00
feedstate: implement + test
This commit is contained in:
@@ -1,31 +1,109 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type FeedState struct {
|
type FeedState struct {
|
||||||
LastRefreshed time.Time
|
FeedID int64
|
||||||
LastError string
|
LastRefreshed time.Time
|
||||||
|
LastError *string
|
||||||
HTTPLastModified string
|
HTTPLastModified string
|
||||||
HTTPEtag string
|
HTTPEtag string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) ListFeedStates() ([]FeedState, error) {
|
func (s *Storage) ListFeedStates() ([]FeedState, error) {
|
||||||
// TODO: implement
|
rows, err := s.db.Query(`
|
||||||
|
select feed_id, last_refreshed, last_modified, etag, last_error
|
||||||
|
from feed_states
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
states := make([]FeedState, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var state FeedState
|
||||||
|
err := rows.Scan(
|
||||||
|
&state.FeedID,
|
||||||
|
&state.LastRefreshed,
|
||||||
|
&state.HTTPLastModified,
|
||||||
|
&state.HTTPEtag,
|
||||||
|
&state.LastError,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
states = append(states, state)
|
||||||
|
}
|
||||||
|
return states, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) GetFeedState() (FeedState, error) {
|
func (s *Storage) GetFeedState(feedID int64) (*FeedState, error) {
|
||||||
// TODO: implement
|
var state FeedState
|
||||||
|
err := s.db.QueryRow(`
|
||||||
|
select feed_id, last_refreshed, last_modified, etag, last_error
|
||||||
|
from feed_states where feed_id = :id
|
||||||
|
`, sql.Named("id", feedID)).Scan(
|
||||||
|
&state.FeedID,
|
||||||
|
&state.LastRefreshed,
|
||||||
|
&state.HTTPLastModified,
|
||||||
|
&state.HTTPEtag,
|
||||||
|
&state.LastError,
|
||||||
|
)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateFeedStateParams struct {
|
type UpdateFeedStateParams struct {
|
||||||
LastRefreshed *time.Time
|
LastRefreshed *time.Time
|
||||||
LastError *string
|
LastError *string
|
||||||
|
|
||||||
HTTPLastModified *string
|
HTTPLastModified *string
|
||||||
HTTPEtag *string
|
HTTPEtag *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) UpdateFeedState(params UpdateFeedStateParams) (bool, error) {
|
func (s *Storage) UpdateFeedState(feedID int64, params UpdateFeedStateParams) (bool, error) {
|
||||||
// TODO: implement
|
lastError := params.LastError
|
||||||
|
if lastError != nil && *lastError == "" {
|
||||||
|
lastError = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.db.Exec(`
|
||||||
|
insert into feed_states (
|
||||||
|
feed_id
|
||||||
|
, last_refreshed
|
||||||
|
, last_modified
|
||||||
|
, etag
|
||||||
|
, last_error
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
:id
|
||||||
|
, coalesce(:refreshed, 0)
|
||||||
|
, coalesce(:last_modified, '')
|
||||||
|
, coalesce(:etag, '')
|
||||||
|
, coalesce(:last_error, '')
|
||||||
|
)
|
||||||
|
on conflict (feed_id) do update set
|
||||||
|
last_refreshed = coalesce(:refreshed, last_refreshed),
|
||||||
|
last_modified = coalesce(:last_modified, last_modified),
|
||||||
|
etag = coalesce(:etag, etag),
|
||||||
|
last_error = coalesce(:last_error, last_error)
|
||||||
|
`,
|
||||||
|
sql.Named("id", feedID),
|
||||||
|
sql.Named("refreshed", params.LastRefreshed),
|
||||||
|
sql.Named("last_modified", params.HTTPLastModified),
|
||||||
|
sql.Named("etag", params.HTTPEtag),
|
||||||
|
sql.Named("last_error", params.LastError),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateFeedState_Full(t *testing.T) {
|
||||||
|
s := testDB()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
f := s.CreateFeed(CreateFeedParams{Title: "Test", FeedLink: "http://example.com"})
|
||||||
|
|
||||||
|
now := time.Now().UTC().Truncate(time.Second)
|
||||||
|
errMsg := "error"
|
||||||
|
lmod := "today"
|
||||||
|
etag := "v1"
|
||||||
|
|
||||||
|
ok, err := s.UpdateFeedState(f.Id, UpdateFeedStateParams{
|
||||||
|
LastRefreshed: &now,
|
||||||
|
LastError: &errMsg,
|
||||||
|
HTTPLastModified: &lmod,
|
||||||
|
HTTPEtag: &etag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Error("expected true")
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := s.GetFeedState(f.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
t.Fatal("expected state, got nil")
|
||||||
|
}
|
||||||
|
if !state.LastRefreshed.Equal(now) {
|
||||||
|
t.Errorf("expected %v, got %v", now, state.LastRefreshed)
|
||||||
|
}
|
||||||
|
if state.LastError == nil || *state.LastError != errMsg {
|
||||||
|
t.Errorf("expected %s, got %v", errMsg, state.LastError)
|
||||||
|
}
|
||||||
|
if state.HTTPLastModified != lmod {
|
||||||
|
t.Errorf("expected %s, got %s", lmod, state.HTTPLastModified)
|
||||||
|
}
|
||||||
|
if state.HTTPEtag != etag {
|
||||||
|
t.Errorf("expected %s, got %s", etag, state.HTTPEtag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateFeedState_Partial(t *testing.T) {
|
||||||
|
s := testDB()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
f := s.CreateFeed(CreateFeedParams{Title: "Test", FeedLink: "http://example.com"})
|
||||||
|
etag := "v1"
|
||||||
|
s.UpdateFeedState(f.Id, UpdateFeedStateParams{HTTPEtag: &etag})
|
||||||
|
|
||||||
|
newErr := "new error"
|
||||||
|
_, err := s.UpdateFeedState(f.Id, UpdateFeedStateParams{
|
||||||
|
LastError: &newErr,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := s.GetFeedState(f.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if state.LastError == nil || *state.LastError != newErr {
|
||||||
|
t.Errorf("expected %s, got %v", newErr, state.LastError)
|
||||||
|
}
|
||||||
|
if state.HTTPEtag != etag {
|
||||||
|
t.Errorf("etag should be unchanged, got %s", state.HTTPEtag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateFeedState_ClearError(t *testing.T) {
|
||||||
|
s := testDB()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
f := s.CreateFeed(CreateFeedParams{Title: "Test", FeedLink: "http://example.com"})
|
||||||
|
errMsg := "error"
|
||||||
|
s.UpdateFeedState(f.Id, UpdateFeedStateParams{LastError: &errMsg})
|
||||||
|
|
||||||
|
empty := ""
|
||||||
|
_, err := s.UpdateFeedState(f.Id, UpdateFeedStateParams{
|
||||||
|
LastError: &empty,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := s.GetFeedState(f.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if state.LastError == nil || *state.LastError != "" {
|
||||||
|
t.Errorf("expected empty string error, got %v", state.LastError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListFeedStates(t *testing.T) {
|
||||||
|
s := testDB()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
f1 := s.CreateFeed(CreateFeedParams{Title: "F1", FeedLink: "L1"})
|
||||||
|
f2 := s.CreateFeed(CreateFeedParams{Title: "F2", FeedLink: "L2"})
|
||||||
|
|
||||||
|
errMsg := "fail"
|
||||||
|
etag := "e"
|
||||||
|
s.UpdateFeedState(f1.Id, UpdateFeedStateParams{LastError: &errMsg})
|
||||||
|
s.UpdateFeedState(f2.Id, UpdateFeedStateParams{HTTPEtag: &etag})
|
||||||
|
|
||||||
|
states, err := s.ListFeedStates()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(states) != 2 {
|
||||||
|
t.Errorf("expected 2 states, got %d", len(states))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ var migrations = []func(*sql.Tx) error{
|
|||||||
m10_add_item_medialinks,
|
m10_add_item_medialinks,
|
||||||
m11_add_item_last_arrived,
|
m11_add_item_last_arrived,
|
||||||
m12_remove_feed_sizes,
|
m12_remove_feed_sizes,
|
||||||
|
m13_consolidate_feed_states,
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxVersion = int64(len(migrations))
|
var maxVersion = int64(len(migrations))
|
||||||
@@ -345,3 +346,32 @@ func m12_remove_feed_sizes(tx *sql.Tx) error {
|
|||||||
_, err := tx.Exec(`drop table if exists feed_sizes`)
|
_, err := tx.Exec(`drop table if exists feed_sizes`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func m13_consolidate_feed_states(tx *sql.Tx) error {
|
||||||
|
sql := `
|
||||||
|
create table feed_states (
|
||||||
|
feed_id references feeds(id) on delete cascade unique,
|
||||||
|
last_refreshed datetime not null default 0,
|
||||||
|
last_modified string not null default '',
|
||||||
|
etag string not null default '',
|
||||||
|
last_error string not null default ''
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into feed_states (feed_id, last_refreshed, last_modified, etag, last_error)
|
||||||
|
select
|
||||||
|
f.id,
|
||||||
|
coalesce(h.last_refreshed, 0),
|
||||||
|
coalesce(h.last_modified, ''),
|
||||||
|
coalesce(h.etag, ''),
|
||||||
|
coalesce(e.error, '')
|
||||||
|
from feeds f
|
||||||
|
left join http_states h on f.id = h.feed_id
|
||||||
|
left join feed_errors e on f.id = e.feed_id
|
||||||
|
where h.feed_id is not null or e.feed_id is not null;
|
||||||
|
|
||||||
|
drop table http_states;
|
||||||
|
drop table feed_errors;
|
||||||
|
`
|
||||||
|
_, err := tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,3 +38,7 @@ func New(path string) (*Storage, error) {
|
|||||||
}
|
}
|
||||||
return &Storage{db: db}, nil
|
return &Storage{db: db}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Close() error {
|
||||||
|
return s.db.Close()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user