coda/internal/store/store_test.go

480 lines
11 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 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
}
}