Files
bookhoard/internal/handlers/progress_test.go
T
john-okeefe 1cda4e5191 feat(handlers): integrate ProgressService into media, koreader, kobo, and queue
All four progress write paths now delegate to ProgressService.SaveProgress:

- MediaHandler: UpdateMediaReadingProgress uses ProgressService for web
  saves with richer request body (reading_mode, zoom_level, scroll). GET
  now uses GetUniversalProgress query that JOINs media_items for
  format_group, total_characters, chapter_count.

- KOReaderHandler: updateProgressForBook delegates to ProgressService.
  Fixed device ID bug (was using userID, now uses deviceID). Removed
  duplicate UpdateDeviceLastSync with zero UUID. Added pgtype helper
  functions (textPtrToPgText, intPtrToPgInt4, int64PtrToPgInt8).

- KoboHandler: all four progress write points (Markup ReadingSync, Markup
  last-read-place, AnalyticsGettests, SyncFromServer) delegate to
  ProgressService. Fixed empty epubcfi string now correctly set to
  Valid: false. SyncFromServer preserves last_sync_source=bookhoard
  and Broadcast: false.

- QueueProcessor: syncProgress delegates to ProgressService.

- main.go: creates ProgressService after ConnectionManager, injects via
  SetProgressService() on all handlers and queue processor.

Handler tests cover pgtype conversion helpers (textPtrToPgText, etc.)
and device icon mapping.
2026-04-25 21:16:29 -04:00

153 lines
3.5 KiB
Go

package handlers
import (
"testing"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
)
func TestGetDeviceIcon_Kobo(t *testing.T) {
result := getDeviceIcon("kobo")
assert.Equal(t, "📚", result)
}
func TestGetDeviceIcon_KOReader(t *testing.T) {
result := getDeviceIcon("koreader")
assert.Equal(t, "📖", result)
}
func TestGetDeviceIcon_Kindle(t *testing.T) {
result := getDeviceIcon("kindle")
assert.Equal(t, "📱", result)
}
func TestGetDeviceIcon_Unknown(t *testing.T) {
result := getDeviceIcon("unknown")
assert.Equal(t, "📚", result)
result = getDeviceIcon("")
assert.Equal(t, "📚", result)
}
func TestTextPtrToPgText(t *testing.T) {
t.Run("nil returns invalid", func(t *testing.T) {
result := textPtrToPgText(nil)
assert.False(t, result.Valid)
})
t.Run("non-nil returns valid", func(t *testing.T) {
s := "epubcfi(/6/4/2:10)"
result := textPtrToPgText(&s)
assert.True(t, result.Valid)
assert.Equal(t, s, result.String)
})
t.Run("empty string returns valid", func(t *testing.T) {
s := ""
result := textPtrToPgText(&s)
assert.True(t, result.Valid)
assert.Equal(t, "", result.String)
})
}
func TestIntPtrToPgInt4(t *testing.T) {
t.Run("nil returns invalid", func(t *testing.T) {
result := intPtrToPgInt4(nil)
assert.False(t, result.Valid)
})
t.Run("non-nil returns valid", func(t *testing.T) {
v := 5
result := intPtrToPgInt4(&v)
assert.True(t, result.Valid)
assert.Equal(t, int32(5), result.Int32)
})
}
func TestInt64PtrToPgInt8(t *testing.T) {
t.Run("nil returns invalid", func(t *testing.T) {
result := int64PtrToPgInt8(nil)
assert.False(t, result.Valid)
})
t.Run("non-nil returns valid", func(t *testing.T) {
v := int64(10000)
result := int64PtrToPgInt8(&v)
assert.True(t, result.Valid)
assert.Equal(t, int64(10000), result.Int64)
})
}
func TestFloat64PtrHelpers(t *testing.T) {
t.Run("pgtype float64 valid", func(t *testing.T) {
v := pgtype.Float8{Float64: 0.5, Valid: true}
result := float64PtrVal(v)
assert.NotNil(t, result)
assert.InDelta(t, 0.5, *result, 0.001)
})
t.Run("pgtype float64 invalid", func(t *testing.T) {
v := pgtype.Float8{Valid: false}
result := float64PtrVal(v)
assert.Nil(t, result)
})
t.Run("pgtype text valid", func(t *testing.T) {
v := pgtype.Text{String: "hello", Valid: true}
result := textPtrVal(v)
assert.NotNil(t, result)
assert.Equal(t, "hello", *result)
})
t.Run("pgtype text invalid", func(t *testing.T) {
v := pgtype.Text{Valid: false}
result := textPtrVal(v)
assert.Nil(t, result)
})
t.Run("pgtype int4 valid", func(t *testing.T) {
v := pgtype.Int4{Int32: 42, Valid: true}
result := int32PtrVal(v)
assert.NotNil(t, result)
assert.Equal(t, 42, *result)
})
t.Run("pgtype int4 invalid", func(t *testing.T) {
v := pgtype.Int4{Valid: false}
result := int32PtrVal(v)
assert.Nil(t, result)
})
t.Run("pgtype int8 valid", func(t *testing.T) {
v := pgtype.Int8{Int64: 10000, Valid: true}
result := int64PtrVal(v)
assert.NotNil(t, result)
assert.Equal(t, int64(10000), *result)
})
t.Run("pgtype int8 invalid", func(t *testing.T) {
v := pgtype.Int8{Valid: false}
result := int64PtrVal(v)
assert.Nil(t, result)
})
}
func float64PtrVal(v pgtype.Float8) *float64 {
if v.Valid {
return &v.Float64
}
return nil
}
func textPtrVal(v pgtype.Text) *string {
if v.Valid {
return &v.String
}
return nil
}
func int32PtrVal(v pgtype.Int4) *int {
if v.Valid {
val := int(v.Int32)
return &val
}
return nil
}
func int64PtrVal(v pgtype.Int8) *int64 {
if v.Valid {
return &v.Int64
}
return nil
}