1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
package test
import (
"io"
"net/http"
"path/filepath"
"rss_parrot/dal"
"rss_parrot/logic"
"rss_parrot/shared"
"sync"
"testing"
"time"
)
// Hand-written stand-ins for the collaborators a test does not exercise.
// Anything the tests do exercise is real: the repo is a SQLite database with
// every migration applied, the logger is the real one writing nowhere.
func newTestLogger() shared.ILogger {
return shared.NewLogger(io.Discard, "Error")
}
// newTestRepo opens a fresh database in a temporary directory, with every
// migration applied and the built-in birb account created from cfg.
func newTestRepo(t *testing.T, cfg *shared.Config) dal.IRepo {
t.Helper()
cfg.DbFile = filepath.Join(t.TempDir(), "test.db")
repo := dal.NewRepo(cfg, newTestLogger())
repo.InitUpdateDb()
return repo
}
type nopMetrics struct{}
func (nopMetrics) CurrentConnections(int) {}
func (nopMetrics) StartWebRequestIn(string) logic.IRequestObserver { return nopObserver{} }
func (nopMetrics) StartApubRequestIn(string) logic.IRequestObserver { return nopObserver{} }
func (nopMetrics) StartApubRequestOut(string) logic.IRequestObserver { return nopObserver{} }
func (nopMetrics) FeedRequested(string) {}
func (nopMetrics) FeedUpdated() {}
func (nopMetrics) NewPostSaved() {}
func (nopMetrics) PostsDeleted(int) {}
func (nopMetrics) TotalPosts(int) {}
func (nopMetrics) FeedTootSent() {}
func (nopMetrics) ServiceStarted() {}
func (nopMetrics) TotalFollowers(int) {}
func (nopMetrics) TootQueueLength(int) {}
func (nopMetrics) CheckableFeedCount(int) {}
func (nopMetrics) DbFileSize(int64) {}
type nopObserver struct{}
func (nopObserver) Finish() {}
type nopUserAgent struct{}
func (nopUserAgent) AddUserAgent(*http.Request) {}
// fakeTexts renders a snippet as its id followed by its values, so a test can
// see which snippet was used without depending on its wording.
type fakeTexts struct {
mu sync.Mutex
used []string
}
func (ft *fakeTexts) Get(id string) string {
ft.record(id)
return id
}
func (ft *fakeTexts) WithVals(id string, vals map[string]string) string {
ft.record(id)
return fakeTextWithVals(id, vals)
}
func (ft *fakeTexts) record(id string) {
ft.mu.Lock()
ft.used = append(ft.used, id)
ft.mu.Unlock()
}
func (ft *fakeTexts) usedIds() []string {
ft.mu.Lock()
defer ft.mu.Unlock()
return append([]string(nil), ft.used...)
}
// sentMessage is one call to IMessenger.SendMessageAsync.
type sentMessage struct {
byUser, toInbox, msg string
mentions []*logic.MsgMention
to, cc []string
inReplyTo string
}
// recordingMessenger delivers every message to a channel instead of sending it.
type recordingMessenger struct {
sent chan sentMessage
}
func newRecordingMessenger() *recordingMessenger {
return &recordingMessenger{sent: make(chan sentMessage, 16)}
}
func (rm *recordingMessenger) SendMessageAsync(byUser string, toInbox, msg string,
mentions []*logic.MsgMention, to, cc []string, inReplyTo string) {
rm.sent <- sentMessage{byUser, toInbox, msg, mentions, to, cc, inReplyTo}
}
func (rm *recordingMessenger) EnqueueBroadcast(string, string, time.Time, string) error {
return nil
}
// next waits for the next message, failing the test if none comes.
func (rm *recordingMessenger) next(t *testing.T) sentMessage {
t.Helper()
select {
case m := <-rm.sent:
return m
case <-time.After(time.Second):
t.Fatal("expected a message to be sent; none was")
return sentMessage{}
}
}
// none checks that no message is sent within a short grace period.
func (rm *recordingMessenger) none(t *testing.T) {
t.Helper()
select {
case m := <-rm.sent:
t.Errorf("expected no message, but one was sent: %+v", m)
case <-time.After(100 * time.Millisecond):
}
}
// fakeFeedFollower answers GetAccountForFeed with a fixed result and records
// what was asked for.
type fakeFeedFollower struct {
mu sync.Mutex
requested []string
acct *dal.Account
status logic.FeedStatus
err error
}
func (ff *fakeFeedFollower) GetAccountForFeed(urlStr string) (*dal.Account, logic.FeedStatus, error) {
ff.mu.Lock()
ff.requested = append(ff.requested, urlStr)
ff.mu.Unlock()
return ff.acct, ff.status, ff.err
}
func (ff *fakeFeedFollower) PurgeOldPosts(*dal.Account, int, int) error { return nil }
func (ff *fakeFeedFollower) requests() []string {
ff.mu.Lock()
defer ff.mu.Unlock()
return append([]string(nil), ff.requested...)
}
|