2023-12-26 22:18:09 +00:00
|
|
|
package deadlinks
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"sort"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"code.betamike.com/mediocregopher/mediocre-go-lib/miter"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
)
|
|
|
|
|
|
|
|
type sqliteStoreHarness struct {
|
|
|
|
ctx context.Context
|
|
|
|
now time.Time
|
|
|
|
store *SQLiteStore
|
|
|
|
}
|
|
|
|
|
|
|
|
func newSQLiteStoreHarness() *sqliteStoreHarness {
|
|
|
|
var (
|
|
|
|
ctx = context.Background()
|
|
|
|
now = time.Now().Truncate(time.Second).UTC()
|
2023-12-28 14:40:07 +00:00
|
|
|
store = NewSQLiteStore(nil)
|
2023-12-26 22:18:09 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return &sqliteStoreHarness{
|
|
|
|
ctx, now, store,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *sqliteStoreHarness) assertGetByStatus(
|
|
|
|
t *testing.T, want []Resource, status ResourceStatus,
|
|
|
|
) {
|
|
|
|
norm := func(rr []Resource) {
|
|
|
|
for i, r := range rr {
|
|
|
|
sort.Slice(r.IncomingLinkURLs, func(i, j int) bool {
|
|
|
|
return r.IncomingLinkURLs[i] < r.IncomingLinkURLs[j]
|
|
|
|
})
|
|
|
|
sort.Slice(r.OutgoingLinkURLs, func(i, j int) bool {
|
|
|
|
return r.OutgoingLinkURLs[i] < r.OutgoingLinkURLs[j]
|
|
|
|
})
|
|
|
|
rr[i] = r
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Slice(rr, func(i, j int) bool {
|
|
|
|
return rr[i].URL < rr[j].URL
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
got, err := miter.ToSlice(h.ctx, h.store.GetByStatus(status))
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
norm(want)
|
|
|
|
norm(got)
|
|
|
|
assert.Equal(t, want, got)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestSQLiteStore(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
t.Run("SetPinned", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
var (
|
|
|
|
h = newSQLiteStoreHarness()
|
|
|
|
urlA = URL("https://a.com")
|
|
|
|
urlB = URL("https://b.com")
|
|
|
|
a = Resource{URL: urlA, Pinned: true}
|
|
|
|
b = Resource{URL: urlB, Pinned: true}
|
|
|
|
)
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.SetPinned(h.ctx, []URL{urlA}))
|
|
|
|
h.assertGetByStatus(t, nil, ResourceStatusOK)
|
|
|
|
h.assertGetByStatus(t, []Resource{a}, ResourceStatusUnknown)
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.SetPinned(h.ctx, []URL{urlA, urlB}))
|
|
|
|
h.assertGetByStatus(t, nil, ResourceStatusOK)
|
|
|
|
h.assertGetByStatus(t, []Resource{a, b}, ResourceStatusUnknown)
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.SetPinned(h.ctx, []URL{urlB}))
|
|
|
|
h.assertGetByStatus(t, nil, ResourceStatusOK)
|
2023-12-28 14:40:07 +00:00
|
|
|
// GetByStatus should not return resources which are not pinned and have
|
|
|
|
// no incoming links
|
|
|
|
h.assertGetByStatus(t, []Resource{b}, ResourceStatusUnknown)
|
2023-12-26 22:18:09 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Update", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
var (
|
|
|
|
h = newSQLiteStoreHarness()
|
|
|
|
|
|
|
|
urlA = URL("https://a.com")
|
|
|
|
urlB = URL("https://b.com")
|
|
|
|
urlC = URL("https://c.com")
|
|
|
|
|
|
|
|
a = Resource{URL: urlA, Pinned: true}
|
|
|
|
b = Resource{URL: urlB, Pinned: true}
|
|
|
|
c = Resource{URL: urlC}
|
|
|
|
)
|
|
|
|
|
|
|
|
// updating a non-existing URL should fail and make no changes
|
|
|
|
assert.Error(t, h.store.Update(h.ctx, h.now, urlA, ResourceStatusOK, "errstr", nil))
|
|
|
|
h.assertGetByStatus(t, nil, ResourceStatusUnknown)
|
|
|
|
h.assertGetByStatus(t, nil, ResourceStatusOK)
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.SetPinned(h.ctx, []URL{urlA, urlB}))
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.Update(h.ctx, h.now, urlA, ResourceStatusOK, "", nil))
|
|
|
|
a.LastChecked = h.now
|
|
|
|
a.Status = ResourceStatusOK
|
|
|
|
h.assertGetByStatus(t, []Resource{b}, ResourceStatusUnknown)
|
|
|
|
h.assertGetByStatus(t, []Resource{a}, ResourceStatusOK)
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.Update(h.ctx, h.now, urlB, ResourceStatusError, "error!", nil))
|
|
|
|
b.LastChecked = h.now
|
|
|
|
b.Status = ResourceStatusError
|
|
|
|
b.ErrorString = "error!"
|
|
|
|
h.assertGetByStatus(t, nil, ResourceStatusUnknown)
|
|
|
|
h.assertGetByStatus(t, nil, ResourceStatusUnknown)
|
|
|
|
h.assertGetByStatus(t, []Resource{a}, ResourceStatusOK)
|
|
|
|
h.assertGetByStatus(t, []Resource{b}, ResourceStatusError)
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.Update(
|
|
|
|
h.ctx, h.now, urlA, ResourceStatusOK, "", []URL{urlC},
|
|
|
|
))
|
|
|
|
a.OutgoingLinkURLs = []URL{urlC}
|
|
|
|
c.IncomingLinkURLs = []URL{urlA}
|
|
|
|
h.assertGetByStatus(t, []Resource{c}, ResourceStatusUnknown)
|
|
|
|
h.assertGetByStatus(t, []Resource{a}, ResourceStatusOK)
|
|
|
|
h.assertGetByStatus(t, []Resource{b}, ResourceStatusError)
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.Update(
|
|
|
|
h.ctx, h.now, urlB, ResourceStatusOK, "", []URL{urlC},
|
|
|
|
))
|
|
|
|
b.Status = ResourceStatusOK
|
|
|
|
b.ErrorString = ""
|
|
|
|
b.OutgoingLinkURLs = []URL{urlC}
|
|
|
|
c.IncomingLinkURLs = []URL{urlA, urlB}
|
|
|
|
h.assertGetByStatus(t, []Resource{c}, ResourceStatusUnknown)
|
|
|
|
h.assertGetByStatus(t, []Resource{a, b}, ResourceStatusOK)
|
|
|
|
h.assertGetByStatus(t, nil, ResourceStatusError)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("GetURLsByLastChecked", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
var (
|
|
|
|
h = newSQLiteStoreHarness()
|
|
|
|
nowA = h.now
|
|
|
|
nowB = h.now.Add(1 * time.Minute)
|
|
|
|
|
|
|
|
urlA = URL("https://a.com")
|
|
|
|
urlB = URL("https://b.com")
|
|
|
|
urlC = URL("https://c.com")
|
|
|
|
)
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.SetPinned(h.ctx, []URL{urlA, urlB, urlC}))
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.Update(
|
|
|
|
h.ctx, nowA, urlA, ResourceStatusOK, "", nil,
|
|
|
|
))
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.Update(
|
|
|
|
h.ctx, nowB, urlB, ResourceStatusOK, "", nil,
|
|
|
|
))
|
|
|
|
|
|
|
|
assertGetURLsByLastChecked := func(want []URL, olderThan time.Time) {
|
|
|
|
got, err := miter.ToSlice(h.ctx, h.store.GetURLsByLastChecked(olderThan))
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.ElementsMatch(t, want, got)
|
|
|
|
}
|
|
|
|
|
|
|
|
assertGetURLsByLastChecked([]URL{urlA, urlB, urlC}, nowB.Add(1*time.Second))
|
|
|
|
assertGetURLsByLastChecked([]URL{urlA, urlC}, nowB)
|
|
|
|
assertGetURLsByLastChecked([]URL{urlA, urlC}, nowA.Add(1*time.Second))
|
|
|
|
assertGetURLsByLastChecked([]URL{urlC}, nowA)
|
2023-12-28 14:40:07 +00:00
|
|
|
|
|
|
|
// A Resource which is not pinned and has no incoming links should not
|
|
|
|
// be returned
|
|
|
|
assert.NoError(t, h.store.SetPinned(h.ctx, []URL{urlA, urlB}))
|
|
|
|
assertGetURLsByLastChecked([]URL{urlA, urlB}, nowB.Add(1*time.Second))
|
|
|
|
assertGetURLsByLastChecked([]URL{}, nowA)
|
2023-12-26 22:18:09 +00:00
|
|
|
})
|
|
|
|
|
2023-12-28 14:40:07 +00:00
|
|
|
t.Run("GC", func(t *testing.T) {
|
2023-12-26 22:18:09 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
var (
|
|
|
|
h = newSQLiteStoreHarness()
|
|
|
|
|
|
|
|
urlA = URL("https://a.com")
|
|
|
|
urlB = URL("https://b.com")
|
|
|
|
urlC = URL("https://c.com")
|
|
|
|
urlD = URL("https://d.com")
|
|
|
|
|
|
|
|
a = Resource{URL: urlA, Pinned: true, LastChecked: h.now, OutgoingLinkURLs: []URL{urlB}}
|
|
|
|
b = Resource{URL: urlB, IncomingLinkURLs: []URL{urlA}}
|
|
|
|
)
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.SetPinned(h.ctx, []URL{urlA, urlB, urlC, urlD}))
|
|
|
|
assert.NoError(t, h.store.SetPinned(h.ctx, []URL{urlA}))
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.Update(
|
|
|
|
h.ctx, h.now, urlA, ResourceStatusUnknown, "", []URL{urlB},
|
|
|
|
))
|
|
|
|
|
|
|
|
assert.NoError(t, h.store.Update(
|
|
|
|
h.ctx, h.now, urlC, ResourceStatusUnknown, "", []URL{urlD},
|
|
|
|
))
|
|
|
|
|
2023-12-28 14:40:07 +00:00
|
|
|
assert.NoError(t, h.store.GC(h.ctx))
|
|
|
|
h.assertGetByStatus(t, []Resource{a, b}, ResourceStatusUnknown)
|
|
|
|
|
|
|
|
// Calling again shouldn't do anything
|
|
|
|
assert.NoError(t, h.store.GC(h.ctx))
|
2023-12-26 22:18:09 +00:00
|
|
|
h.assertGetByStatus(t, []Resource{a, b}, ResourceStatusUnknown)
|
|
|
|
})
|
|
|
|
}
|