Files
tver/internal/web/store_test.go

362 lines
9.3 KiB
Go

package web
import (
"context"
"sort"
"time"
"gitea.kazu634.com/kazu634/tver/internal/store"
"github.com/jackc/pgx/v5"
)
type fakeJobStore struct {
jobs map[string]store.Job
subscriptions map[string]store.Subscription
nextID int64
lastEpisode store.Episode
enqueueCalls, statusCalls int
statusIDs []string
listStatuses []string
listLimits []int
registerCalls int
registerEpisodes []store.Episode
registerDownload bool
registerErr error
subscribedCalls int
subscribedIDs []string
beforeDeleteSucceeded func()
err error
}
func (f *fakeJobStore) ensure() {
if f.jobs == nil {
f.jobs = make(map[string]store.Job)
}
}
func (f *fakeJobStore) Enqueue(_ context.Context, ep store.Episode, source string) (store.Job, bool, error) {
f.enqueueCalls++
f.lastEpisode = ep
if f.err != nil {
return store.Job{}, false, f.err
}
f.ensure()
if job, ok := f.jobs[ep.ID]; ok {
return job, false, nil
}
f.nextID++
job := store.Job{ID: f.nextID, EpisodeID: ep.ID, SeriesTitle: ep.SeriesTitle,
Title: ep.Title, BroadcasterName: ep.BroadcasterName, Source: source,
Status: "queued", CreatedAt: time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC)}
f.jobs[ep.ID] = job
return job, true, nil
}
func (f *fakeJobStore) EpisodeStatuses(_ context.Context, ids []string) (map[string]string, error) {
f.statusCalls++
f.statusIDs = append([]string(nil), ids...)
if f.err != nil {
return nil, f.err
}
result := make(map[string]string)
for _, id := range ids {
if job, ok := f.jobs[id]; ok {
result[id] = job.Status
}
}
return result, nil
}
func (f *fakeJobStore) ListJobs(_ context.Context, status string, limit int) ([]store.Job, error) {
f.listStatuses = append(f.listStatuses, status)
f.listLimits = append(f.listLimits, limit)
if f.err != nil {
return nil, f.err
}
var result []store.Job
for _, job := range f.jobs {
if job.Status == status {
result = append(result, job)
}
}
sort.Slice(result, func(i, j int) bool { return result[i].ID > result[j].ID })
if len(result) > limit {
result = result[:limit]
}
return result, nil
}
func (f *fakeJobStore) GetJob(_ context.Context, id int64) (store.Job, error) {
if f.err != nil {
return store.Job{}, f.err
}
for _, job := range f.jobs {
if job.ID == id {
return job, nil
}
}
return store.Job{}, pgx.ErrNoRows
}
func (f *fakeJobStore) Retry(_ context.Context, id int64) error {
for ep, job := range f.jobs {
if job.ID == id && (job.Status == "failed" || job.Status == "canceled") {
job.Status = "queued"
f.jobs[ep] = job
return nil
}
}
return store.ErrInvalidTransition
}
func (f *fakeJobStore) RetryEpisode(_ context.Context, id string) error {
job, ok := f.jobs[id]
if !ok || (job.Status != "failed" && job.Status != "canceled") {
return store.ErrInvalidTransition
}
job.Status = "queued"
f.jobs[id] = job
return nil
}
func (f *fakeJobStore) Cancel(_ context.Context, id int64) error {
if f.err != nil {
return f.err
}
for ep, job := range f.jobs {
if job.ID == id && (job.Status == "queued" || job.Status == "running") {
job.Status = "canceled"
f.jobs[ep] = job
return nil
}
}
return store.ErrInvalidTransition
}
func (f *fakeJobStore) CancelQueued(ctx context.Context, id int64) error {
return f.Cancel(ctx, id)
}
var _ JobStore = (*fakeJobStore)(nil)
func (f *fakeJobStore) SubscribedSeries(_ context.Context, ids []string) (map[string]bool, error) {
f.subscribedCalls++
f.subscribedIDs = append([]string(nil), ids...)
if f.err != nil {
return nil, f.err
}
result := make(map[string]bool)
for _, id := range ids {
_, result[id] = f.subscriptions[id]
}
return result, nil
}
func (f *fakeJobStore) RegisterSubscription(_ context.Context, seriesID, title string, episodes []store.Episode, download bool) (bool, error) {
f.registerCalls++
f.registerEpisodes = append([]store.Episode(nil), episodes...)
f.registerDownload = download
if f.registerErr != nil {
return false, f.registerErr
}
if f.err != nil {
return false, f.err
}
if f.subscriptions == nil {
f.subscriptions = make(map[string]store.Subscription)
}
if _, exists := f.subscriptions[seriesID]; exists {
return false, nil
}
var count int64
if download {
count = int64(len(episodes))
}
checked := time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC)
f.subscriptions[seriesID] = store.Subscription{SeriesID: seriesID, SeriesTitle: title,
Enabled: true, CreatedAt: checked, LastCheckedAt: &checked, JobCount: count}
return true, nil
}
func (f *fakeJobStore) ListSubscriptions(_ context.Context) ([]store.Subscription, error) {
if f.err != nil {
return nil, f.err
}
var result []store.Subscription
for _, sub := range f.subscriptions {
result = append(result, sub)
}
sort.Slice(result, func(i, j int) bool { return result[i].SeriesID < result[j].SeriesID })
return result, nil
}
func (f *fakeJobStore) SetSubscriptionEnabled(_ context.Context, seriesID string, enabled bool) error {
if f.err != nil {
return f.err
}
sub, exists := f.subscriptions[seriesID]
if !exists {
return store.ErrInvalidTransition
}
sub.Enabled = enabled
f.subscriptions[seriesID] = sub
return nil
}
func (f *fakeJobStore) DeleteSubscription(_ context.Context, seriesID string) (bool, error) {
if f.err != nil {
return false, f.err
}
if _, exists := f.subscriptions[seriesID]; !exists {
return false, nil
}
delete(f.subscriptions, seriesID)
return true, nil
}
func (f *fakeJobStore) ListSucceededJobs(_ context.Context, limit int) ([]store.Job, error) {
if f.err != nil {
return nil, f.err
}
var result []store.Job
for _, job := range f.jobs {
if job.Status == "succeeded" {
result = append(result, job)
}
}
sort.Slice(result, func(i, j int) bool { return result[i].ID > result[j].ID })
if len(result) > limit {
result = result[:limit]
}
return result, nil
}
func (f *fakeJobStore) ListLibrarySeries(_ context.Context) ([]store.LibrarySeries, error) {
if f.err != nil {
return nil, f.err
}
bySeries := make(map[string]*store.LibrarySeries)
latestIDs := make(map[string]int64)
for _, job := range f.jobs {
if job.Status != "succeeded" {
continue
}
item := bySeries[job.SeriesID]
if item == nil {
item = &store.LibrarySeries{SeriesID: job.SeriesID}
bySeries[job.SeriesID] = item
}
item.EpisodeCount++
if job.FileSizeBytes != nil {
item.TotalSizeBytes += *job.FileSizeBytes
}
current := store.Job{ID: latestIDs[job.SeriesID], FinishedAt: item.LatestFinishedAt}
if item.SeriesTitle == "" || jobFinishedLater(job, current) {
item.SeriesTitle = job.SeriesTitle
item.LatestTitle = job.Title
item.BroadcasterName = job.BroadcasterName
item.ThumbnailPath = job.ThumbnailPath
if job.FinishedAt != nil {
finished := *job.FinishedAt
item.LatestFinishedAt = &finished
}
latestIDs[job.SeriesID] = job.ID
}
_, item.Subscribed = f.subscriptions[job.SeriesID]
}
result := make([]store.LibrarySeries, 0, len(bySeries))
for _, item := range bySeries {
result = append(result, *item)
}
sort.Slice(result, func(i, j int) bool {
left, right := result[i].LatestFinishedAt, result[j].LatestFinishedAt
if left == nil || right == nil {
if left != right {
return left != nil
}
} else if !left.Equal(*right) {
return left.After(*right)
}
return result[i].SeriesID < result[j].SeriesID
})
return result, nil
}
func (f *fakeJobStore) ListSucceededJobsBySeries(_ context.Context, seriesID string) ([]store.Job, error) {
if f.err != nil {
return nil, f.err
}
var result []store.Job
for _, job := range f.jobs {
if job.Status == "succeeded" && job.SeriesID == seriesID {
result = append(result, job)
}
}
sortJobsLatest(result)
return result, nil
}
func (f *fakeJobStore) ListSucceededJobsByIDs(_ context.Context, ids []int64) ([]store.Job, error) {
if f.err != nil {
return nil, f.err
}
wanted := make(map[int64]struct{}, len(ids))
for _, id := range ids {
wanted[id] = struct{}{}
}
var result []store.Job
for _, job := range f.jobs {
if _, ok := wanted[job.ID]; ok && job.Status == "succeeded" {
result = append(result, job)
}
}
sortJobsLatest(result)
return result, nil
}
func (f *fakeJobStore) ListSucceededJobsBySeriesIDs(_ context.Context, seriesIDs []string) ([]store.Job, error) {
if f.err != nil {
return nil, f.err
}
wanted := make(map[string]struct{}, len(seriesIDs))
for _, id := range seriesIDs {
wanted[id] = struct{}{}
}
var result []store.Job
for _, job := range f.jobs {
if _, ok := wanted[job.SeriesID]; ok && job.Status == "succeeded" {
result = append(result, job)
}
}
sortJobsLatest(result)
return result, nil
}
func (f *fakeJobStore) GetSucceededJob(_ context.Context, id int64) (store.Job, error) {
if f.err != nil {
return store.Job{}, f.err
}
for _, job := range f.jobs {
if job.ID == id && job.Status == "succeeded" {
return job, nil
}
}
return store.Job{}, pgx.ErrNoRows
}
func (f *fakeJobStore) DeleteSucceededJob(_ context.Context, id int64) error {
if f.beforeDeleteSucceeded != nil {
f.beforeDeleteSucceeded()
}
if f.err != nil {
return f.err
}
for episodeID, job := range f.jobs {
if job.ID == id && job.Status == "succeeded" {
delete(f.jobs, episodeID)
return nil
}
}
return store.ErrInvalidTransition
}