website-feeds

Make RSS feeds of your favorite "vote on posts" websites

git clone https://code.pdelong.com/website-feeds.git

  1package store
  2
  3import (
  4	"context"
  5	"fmt"
  6	"slices"
  7	"time"
  8
  9	"website-feeds/model"
 10)
 11
 12type Fake struct {
 13	settings model.Settings
 14	sites    []model.Site
 15	posts    map[int]map[string]storedPost
 16	requests []model.Request
 17
 18	InjectCreateSiteError           *error
 19	InjectNumFetchPostsForSiteError *error
 20	InjectSettingsError             *error
 21	InjectUpdateSettingsError       *error
 22	InjectSiteByNameError           *error
 23	InjectTopPostsForSiteError      *error
 24	InjectWriteRequestError         *error
 25	InjectWriteSitePostsError       *error
 26}
 27
 28type storedPost struct {
 29	model.Post
 30	lastFetched time.Time
 31}
 32
 33func NewFake() Fake {
 34	return Fake{
 35		settings: model.Settings{
 36			// These are the current default settings
 37			DefaultNumPosts: 21,
 38			MaxNumPosts:     70,
 39			SiteFetchWait:   3 * time.Hour,
 40		},
 41	}
 42}
 43
 44var _ Store = (*Fake)(nil)
 45
 46func siteByName(name string) func(model.Site) bool {
 47	return func(s model.Site) bool {
 48		return s.Name == name
 49	}
 50}
 51
 52func siteByID(id int) func(model.Site) bool {
 53	return func(s model.Site) bool {
 54		return s.ID == id
 55	}
 56}
 57
 58// Close implements Store.
 59func (f *Fake) Close() {}
 60
 61// CreateSite implements Store.
 62func (f *Fake) CreateSite(
 63	ctx context.Context,
 64	name string,
 65	displayName string,
 66) (*model.Site, error) {
 67	if err := f.InjectCreateSiteError; err != nil {
 68		f.InjectCreateSiteError = nil
 69		return nil, *err
 70	}
 71
 72	if slices.ContainsFunc(f.sites, siteByName(name)) {
 73		return nil, fmt.Errorf("site already exists")
 74	}
 75
 76	s := model.Site{
 77		ID:          len(f.sites),
 78		Name:        name,
 79		DisplayName: displayName,
 80		LastFetched: nil,
 81	}
 82	f.sites = append(f.sites, s)
 83
 84	return &s, nil
 85}
 86
 87// SiteByName implements Store.
 88func (f *Fake) SiteByName(
 89	ctx context.Context,
 90	name string,
 91) (*model.Site, error) {
 92	if err := f.InjectSiteByNameError; err != nil {
 93		f.InjectSiteByNameError = nil
 94		return nil, *err
 95	}
 96
 97	if i := slices.IndexFunc(f.sites, siteByName(name)); i < 0 {
 98		return nil, fmt.Errorf("site not found")
 99	} else {
100		return f.sites[i].Clone(), nil
101	}
102}
103
104// WriteSitePosts implements Store.
105func (f *Fake) WriteSitePosts(
106	ctx context.Context,
107	siteID int,
108	posts []model.Post,
109	lastFetched model.SiteLastFetched,
110) error {
111	if err := f.InjectWriteSitePostsError; err != nil {
112		f.InjectWriteSitePostsError = nil
113		return *err
114	}
115
116	if i := slices.IndexFunc(f.sites, siteByID(siteID)); i >= 0 {
117		f.sites[i].LastFetched = lastFetched.Clone()
118		if _, ok := f.posts[siteID]; !ok {
119			f.posts[siteID] = map[string]storedPost{}
120		}
121
122		for _, post := range posts {
123			if dbPost, ok := f.posts[siteID][post.SiteUniqueID]; ok {
124				if !lastFetched.Time.Before(dbPost.lastFetched) {
125					dbPost.Score = post.Score
126					dbPost.Title = post.Title
127					dbPost.lastFetched = lastFetched.Time
128					f.posts[siteID][post.SiteUniqueID] = dbPost
129				}
130			} else {
131				f.posts[siteID][post.SiteUniqueID] = storedPost{
132					Post:        post,
133					lastFetched: lastFetched.Time,
134				}
135			}
136		}
137	} else {
138		return fmt.Errorf("site not found")
139	}
140
141	return nil
142}
143
144// TopPostsForSite implements Store.
145func (f *Fake) TopPostsForSite(
146	ctx context.Context,
147	siteID int,
148	now time.Time,
149	numPosts int,
150) ([]model.Post, error) {
151	if err := f.InjectTopPostsForSiteError; err != nil {
152		f.InjectTopPostsForSiteError = nil
153		return nil, *err
154	}
155
156	returnPosts := []model.Post{}
157	if posts, ok := f.posts[siteID]; ok {
158		for _, post := range posts {
159			since := now.Sub(post.Created)
160			if since > 8*24*time.Hour || since < 24*time.Hour {
161				continue
162			}
163
164			returnPosts = append(returnPosts, post.Post)
165		}
166	} else {
167		return nil, fmt.Errorf("site not found")
168	}
169
170	return returnPosts, nil
171}
172
173// Settings implements Store.
174func (f *Fake) Settings(ctx context.Context) (*model.Settings, error) {
175	if err := f.InjectSettingsError; err != nil {
176		f.InjectSettingsError = nil
177		return nil, *err
178	}
179
180	return f.settings.Clone(), nil
181}
182
183// UpdateSettings implements Store.
184func (f *Fake) UpdateSettings(
185	ctx context.Context,
186	settings *model.Settings,
187) error {
188	if err := f.InjectUpdateSettingsError; err != nil {
189		f.InjectUpdateSettingsError = nil
190		return *err
191	}
192
193	f.settings = *settings.Clone()
194
195	return nil
196}
197
198// WriteRequest implements Store.
199func (f *Fake) WriteRequest(ctx context.Context, r *model.Request) error {
200	if err := f.InjectWriteRequestError; err != nil {
201		f.InjectWriteRequestError = nil
202		return *err
203	}
204
205	f.requests = append(f.requests, *r)
206
207	return nil
208}
209
210// NumFetchPostsForSite implements Store.
211func (f *Fake) NumFetchPostsForSite(
212	ctx context.Context,
213	siteID int,
214	now time.Time,
215) (int, error) {
216	if err := f.InjectNumFetchPostsForSiteError; err != nil {
217		f.InjectNumFetchPostsForSiteError = nil
218		return -1, *err
219	}
220
221	numPosts := -1
222	for _, request := range f.requests {
223		if now.Sub(request.Timestamp) > 24*time.Hour {
224			continue
225		}
226
227		if request.SiteId != siteID {
228			continue
229		}
230
231		numPosts = max(request.NumPosts, numPosts)
232	}
233
234	if len(f.requests) == 0 {
235		return -1, fmt.Errorf("no matching requests in store")
236	}
237
238	return numPosts, nil
239}