524 lines
12 KiB
Go
524 lines
12 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
|
|
"coda/internal/models"
|
|
)
|
|
|
|
func openTestDB(t *testing.T) *Store {
|
|
t.Helper()
|
|
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
st, err := New(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("New(%q): %v", dbPath, err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
return st
|
|
}
|
|
|
|
func TestNewIdempotent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
|
|
st1, err := New(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("first New: %v", err)
|
|
}
|
|
st1.Close()
|
|
|
|
st2, err := New(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("second New: %v", err)
|
|
}
|
|
st2.Close()
|
|
}
|
|
|
|
func TestNewRelativePath(t *testing.T) {
|
|
t.Chdir(t.TempDir())
|
|
|
|
st, err := New(filepath.Join("nested", "test.db"))
|
|
if err != nil {
|
|
t.Fatalf("New relative path: %v", err)
|
|
}
|
|
st.Close()
|
|
|
|
if _, err := os.Stat(filepath.Join("nested", "test.db")); err != nil {
|
|
t.Fatalf("stat relative db: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestJobCreateAndGet(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
job := &models.Job{
|
|
Artist: "Test Artist",
|
|
Album: "Test Album",
|
|
Path: "/music/Test Artist/Test Album",
|
|
Status: models.StatusReceived,
|
|
Source: models.SourceManual,
|
|
}
|
|
|
|
if err := st.Jobs.Create(ctx, job); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if job.ID == "" {
|
|
t.Fatal("expected ID to be set by Create")
|
|
}
|
|
if job.CreatedAt.IsZero() {
|
|
t.Fatal("expected CreatedAt to be set by Create")
|
|
}
|
|
|
|
got, err := st.Jobs.Get(ctx, job.ID)
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if got.Artist != job.Artist {
|
|
t.Errorf("Artist = %q, want %q", got.Artist, job.Artist)
|
|
}
|
|
if got.Status != models.StatusReceived {
|
|
t.Errorf("Status = %q, want %q", got.Status, models.StatusReceived)
|
|
}
|
|
}
|
|
|
|
func TestJobGetNotFound(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
_, err := st.Jobs.Get(ctx, "nonexistent")
|
|
if err != ErrNotFound {
|
|
t.Errorf("expected ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestJobUpdateStatus(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
job := &models.Job{Artist: "A", Album: "B", Path: "/p", Status: models.StatusReceived, Source: models.SourceManual}
|
|
if err := st.Jobs.Create(ctx, job); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
if err := st.Jobs.UpdateStatus(ctx, job.ID, models.StatusProcessing); err != nil {
|
|
t.Fatalf("UpdateStatus: %v", err)
|
|
}
|
|
|
|
got, _ := st.Jobs.Get(ctx, job.ID)
|
|
if got.Status != models.StatusProcessing {
|
|
t.Errorf("Status = %q, want %q", got.Status, models.StatusProcessing)
|
|
}
|
|
}
|
|
|
|
func TestJobUpdateStatusNotFound(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
err := st.Jobs.UpdateStatus(ctx, "nonexistent", models.StatusProcessing)
|
|
if err != ErrNotFound {
|
|
t.Errorf("expected ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestJobUpdateProgress(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
job := &models.Job{Artist: "A", Album: "B", Path: "/p", Status: models.StatusProcessing, Source: models.SourceManual}
|
|
if err := st.Jobs.Create(ctx, job); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
if err := st.Jobs.UpdateProgress(ctx, job.ID, 0, 5); err != nil {
|
|
t.Fatalf("UpdateProgress (set total): %v", err)
|
|
}
|
|
|
|
got, _ := st.Jobs.Get(ctx, job.ID)
|
|
if got.FilesTotal != 5 {
|
|
t.Errorf("FilesTotal = %d, want 5", got.FilesTotal)
|
|
}
|
|
if got.FilesProcessed != 0 {
|
|
t.Errorf("FilesProcessed = %d, want 0", got.FilesProcessed)
|
|
}
|
|
|
|
if err := st.Jobs.UpdateProgress(ctx, job.ID, 3, 5); err != nil {
|
|
t.Fatalf("UpdateProgress (mid): %v", err)
|
|
}
|
|
|
|
got, _ = st.Jobs.Get(ctx, job.ID)
|
|
if got.FilesProcessed != 3 {
|
|
t.Errorf("FilesProcessed = %d, want 3", got.FilesProcessed)
|
|
}
|
|
if got.FilesTotal != 5 {
|
|
t.Errorf("FilesTotal = %d, want 5", got.FilesTotal)
|
|
}
|
|
}
|
|
|
|
func TestJobUpdateProgressNotFound(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
err := st.Jobs.UpdateProgress(ctx, "nonexistent", 1, 5)
|
|
if err != ErrNotFound {
|
|
t.Errorf("expected ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestJobFinish(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
job := &models.Job{Artist: "A", Album: "B", Path: "/p", Status: models.StatusProcessing, Source: models.SourceManual}
|
|
if err := st.Jobs.Create(ctx, job); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
err := st.Jobs.Finish(ctx, job.ID, models.StatusDone, 5000, "")
|
|
if err != nil {
|
|
t.Fatalf("Finish: %v", err)
|
|
}
|
|
|
|
got, _ := st.Jobs.Get(ctx, job.ID)
|
|
if got.Status != models.StatusDone {
|
|
t.Errorf("Status = %q, want %q", got.Status, models.StatusDone)
|
|
}
|
|
if got.DurationMs != 5000 {
|
|
t.Errorf("DurationMs = %d, want 5000", got.DurationMs)
|
|
}
|
|
if got.FinishedAt == nil {
|
|
t.Fatal("expected FinishedAt to be set")
|
|
}
|
|
}
|
|
|
|
func TestJobFinishError(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
job := &models.Job{Artist: "A", Album: "B", Path: "/p", Status: models.StatusProcessing, Source: models.SourceManual}
|
|
if err := st.Jobs.Create(ctx, job); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
err := st.Jobs.Finish(ctx, job.ID, models.StatusError, 0, "something went wrong")
|
|
if err != nil {
|
|
t.Fatalf("Finish: %v", err)
|
|
}
|
|
|
|
got, _ := st.Jobs.Get(ctx, job.ID)
|
|
if got.Status != models.StatusError {
|
|
t.Errorf("Status = %q, want %q", got.Status, models.StatusError)
|
|
}
|
|
if got.ErrorMsg != "something went wrong" {
|
|
t.Errorf("ErrorMsg = %q, want %q", got.ErrorMsg, "something went wrong")
|
|
}
|
|
}
|
|
|
|
func TestJobListAndCount(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
for _, s := range []models.JobStatus{models.StatusReceived, models.StatusDone, models.StatusError} {
|
|
job := &models.Job{
|
|
Artist: "ListTest",
|
|
Album: "Album",
|
|
Path: "/p",
|
|
Status: s,
|
|
Source: models.SourceManual,
|
|
}
|
|
if err := st.Jobs.Create(ctx, job); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
}
|
|
|
|
jobs, err := st.Jobs.List(ctx, ListJobsParams{Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(jobs) < 3 {
|
|
t.Errorf("List len = %d, want >= 3", len(jobs))
|
|
}
|
|
|
|
total, err := st.Jobs.Count(ctx, nil)
|
|
if err != nil {
|
|
t.Fatalf("Count(all): %v", err)
|
|
}
|
|
if total < 3 {
|
|
t.Errorf("Count(all) = %d, want >= 3", total)
|
|
}
|
|
|
|
doneCount, err := st.Jobs.Count(ctx, &[]models.JobStatus{models.StatusDone}[0])
|
|
if err != nil {
|
|
t.Fatalf("Count(done): %v", err)
|
|
}
|
|
if doneCount != 1 {
|
|
t.Errorf("Count(done) = %d, want 1", doneCount)
|
|
}
|
|
}
|
|
|
|
func TestJobListPagination(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 5; i++ {
|
|
job := &models.Job{Artist: "Page", Album: "Test", Path: "/p", Status: models.StatusReceived, Source: models.SourceManual}
|
|
if err := st.Jobs.Create(ctx, job); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
}
|
|
|
|
jobs, err := st.Jobs.List(ctx, ListJobsParams{Limit: 2, Offset: 0})
|
|
if err != nil {
|
|
t.Fatalf("List page 1: %v", err)
|
|
}
|
|
if len(jobs) != 2 {
|
|
t.Errorf("List page 1 len = %d, want 2", len(jobs))
|
|
}
|
|
|
|
jobs2, err := st.Jobs.List(ctx, ListJobsParams{Limit: 2, Offset: 2})
|
|
if err != nil {
|
|
t.Fatalf("List page 2: %v", err)
|
|
}
|
|
if len(jobs2) < 2 {
|
|
t.Errorf("List page 2 len = %d, want >= 2", len(jobs2))
|
|
}
|
|
}
|
|
|
|
func TestLogInsertAndList(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
entry := &models.LogEntry{
|
|
JobID: "job-123",
|
|
Level: "INFO",
|
|
Message: "test message",
|
|
}
|
|
if err := st.Logs.Insert(ctx, entry); err != nil {
|
|
t.Fatalf("Insert: %v", err)
|
|
}
|
|
if entry.ID == 0 {
|
|
t.Fatal("expected ID to be set by Insert")
|
|
}
|
|
|
|
logs, err := st.Logs.List(ctx, ListLogsParams{Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(logs) < 1 {
|
|
t.Fatal("expected at least 1 log entry")
|
|
}
|
|
if logs[0].JobID != "job-123" {
|
|
t.Errorf("JobID = %q, want %q", logs[0].JobID, "job-123")
|
|
}
|
|
}
|
|
|
|
func TestLogListFilters(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
for _, level := range []string{"INFO", "WARN", "ERROR"} {
|
|
entry := &models.LogEntry{JobID: "filter-test", Level: level, Message: "msg"}
|
|
if err := st.Logs.Insert(ctx, entry); err != nil {
|
|
t.Fatalf("Insert: %v", err)
|
|
}
|
|
}
|
|
|
|
warnLogs, err := st.Logs.List(ctx, ListLogsParams{Level: "WARN", Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("List WARN: %v", err)
|
|
}
|
|
if len(warnLogs) != 1 || warnLogs[0].Level != "WARN" {
|
|
t.Errorf("expected 1 WARN log, got %d", len(warnLogs))
|
|
}
|
|
|
|
byJob, err := st.Logs.List(ctx, ListLogsParams{JobID: "filter-test", Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("List by job: %v", err)
|
|
}
|
|
if len(byJob) != 3 {
|
|
t.Errorf("expected 3 logs for filter-test, got %d", len(byJob))
|
|
}
|
|
}
|
|
|
|
func TestLogCount(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 3; i++ {
|
|
entry := &models.LogEntry{Level: "INFO", Message: "msg"}
|
|
if err := st.Logs.Insert(ctx, entry); err != nil {
|
|
t.Fatalf("Insert: %v", err)
|
|
}
|
|
}
|
|
|
|
count, err := st.Logs.Count(ctx, ListLogsParams{})
|
|
if err != nil {
|
|
t.Fatalf("Count: %v", err)
|
|
}
|
|
if count < 3 {
|
|
t.Errorf("Count = %d, want >= 3", count)
|
|
}
|
|
|
|
infoCount, err := st.Logs.Count(ctx, ListLogsParams{Level: "INFO"})
|
|
if err != nil {
|
|
t.Fatalf("Count INFO: %v", err)
|
|
}
|
|
if infoCount < 3 {
|
|
t.Errorf("Count INFO = %d, want >= 3", infoCount)
|
|
}
|
|
}
|
|
|
|
func TestConcurrentWritesDoNotReturnBusy(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 90)
|
|
|
|
for i := 0; i < 30; i++ {
|
|
wg.Add(3)
|
|
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
job := &models.Job{
|
|
Artist: fmt.Sprintf("Artist %d", i),
|
|
Album: fmt.Sprintf("Album %d", i),
|
|
Path: fmt.Sprintf("/music/%d", i),
|
|
Status: models.StatusReceived,
|
|
Source: models.SourceManual,
|
|
}
|
|
if err := st.Jobs.Create(ctx, job); err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
if err := st.Jobs.UpdateStatus(ctx, job.ID, models.StatusProcessing); err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
if err := st.Jobs.Finish(ctx, job.ID, models.StatusDone, int64(i), ""); err != nil {
|
|
errs <- err
|
|
}
|
|
}(i)
|
|
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
errs <- st.Logs.Insert(ctx, &models.LogEntry{
|
|
JobID: fmt.Sprintf("job-%d", i),
|
|
Level: "INFO",
|
|
Message: fmt.Sprintf("message %d", i),
|
|
})
|
|
}(i)
|
|
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
errs <- st.Config.Set(ctx, fmt.Sprintf("key-%d", i), fmt.Sprintf("value-%d", i))
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("concurrent write: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConfigSetGetGetAllDelete(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
err := st.Config.Set(ctx, "mykey", "myvalue")
|
|
if err != nil {
|
|
t.Fatalf("Set: %v", err)
|
|
}
|
|
|
|
val, found, err := st.Config.Get(ctx, "mykey")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if !found {
|
|
t.Fatal("expected key to be found")
|
|
}
|
|
if val != "myvalue" {
|
|
t.Errorf("value = %q, want %q", val, "myvalue")
|
|
}
|
|
|
|
_, found, err = st.Config.Get(ctx, "nonexistent")
|
|
if err != nil {
|
|
t.Fatalf("Get nonexistent: %v", err)
|
|
}
|
|
if found {
|
|
t.Fatal("expected key not found")
|
|
}
|
|
|
|
all, err := st.Config.GetAll(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetAll: %v", err)
|
|
}
|
|
if len(all) < 1 {
|
|
t.Error("expected at least 1 config key")
|
|
}
|
|
|
|
err = st.Config.Delete(ctx, "mykey")
|
|
if err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
|
|
_, found, err = st.Config.Get(ctx, "mykey")
|
|
if err != nil {
|
|
t.Fatalf("Get after delete: %v", err)
|
|
}
|
|
if found {
|
|
t.Fatal("expected key to be deleted")
|
|
}
|
|
}
|
|
|
|
func TestConfigUpsert(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
st.Config.Set(ctx, "dupe", "first")
|
|
st.Config.Set(ctx, "dupe", "second")
|
|
|
|
val, found, _ := st.Config.Get(ctx, "dupe")
|
|
if !found || val != "second" {
|
|
t.Errorf("value = %q, want %q", val, "second")
|
|
}
|
|
|
|
all, _ := st.Config.GetAll(ctx)
|
|
count := 0
|
|
for k := range all {
|
|
if k == "dupe" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 entry for dupe, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestConfigDeleteNotFound(t *testing.T) {
|
|
st := openTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
err := st.Config.Delete(ctx, "nonexistent")
|
|
if err != ErrNotFound {
|
|
t.Errorf("expected ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
if os.Getenv("CI") != "" {
|
|
// TODO: CI-specific setup
|
|
}
|
|
}
|