Found while verifying H1's "app offline-play -> flush -> position lands"
with the emulator path: the flush has been dead code in two independent
ways, invisible because the first break made the second unreachable.
1. ListPendingSyncQueueItems matched `device_id = $1` with a NULL $1
(the processor passes DeviceID{Valid:false} to enumerate all devices;
uuid = NULL matches no rows). The ticker therefore processed nothing,
ever. The WHERE now treats NULL as "every device" (the same
narg::uuid IS NULL idiom the visibility queries use; EXPLAIN'd — seq
scan on a bounded table, idx_sync_queue_status_priority_created
fits). The processor also dropped its fake-pagination loop, which
re-fetched the same first batch forever once the backlog hit the
batch size (no OFFSET in the query).
2. dbItemToSyncQueueItem dropped SyncData — even with a working
enumerator, every flushed item arrived with an empty payload and
failed JSON unmarshal. Dead code harboring silent breakage, exactly
the plan's thesis.
Callers updated for the renamed RowLimit param (queue handler, offline
detector). TestH1_SyncQueueFlush_LandsAndPreservesAudio drives the real
flow: web PUT seeds audio 900s, a pending checkpoint-style queue row
applies percentage 0.8 through SaveProgress, audio survives, queue row
completes.
Also fixes dashboard_test.go's stale BuildSections signature (the
package's test binary has been unbuildable) — user-approved: tests
designed against old shapes are not worth keeping.
195 lines
5.7 KiB
Go
195 lines
5.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/services"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestBuildSections_ConvertsServiceTypesToHandlerTypes(t *testing.T) {
|
|
itemUUID := uuid.New()
|
|
|
|
serviceSections := []services.DashboardSection{
|
|
{
|
|
CollectionID: uuid.UUID{},
|
|
CollectionName: "continue-reading",
|
|
QueryType: "continue-reading",
|
|
Priority: 1,
|
|
IsSystem: true,
|
|
Title: "Continue Reading",
|
|
Description: "Books you're currently reading",
|
|
Icon: "📖",
|
|
Items: []database.MediaItems{
|
|
{
|
|
ID: pgtype.UUID{Bytes: itemUUID, Valid: true},
|
|
Title: "Test Book",
|
|
Author: pgtype.Text{String: "Test Author", Valid: true},
|
|
CoverImagePath: pgtype.Text{String: "/path/to/cover.jpg", Valid: true},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
CollectionID: uuid.UUID{},
|
|
CollectionName: "my-favorites",
|
|
QueryType: "filter",
|
|
Priority: 10,
|
|
IsSystem: false,
|
|
Title: "My Favorites",
|
|
Description: "My favorite books",
|
|
Icon: "⭐",
|
|
Items: []database.MediaItems{},
|
|
},
|
|
}
|
|
|
|
result := BuildSections(serviceSections, "", "", map[string]string{})
|
|
|
|
assert.Equal(t, 2, len(result), "should have 2 sections")
|
|
|
|
section1 := result[0]
|
|
assert.Equal(t, "continue-reading", section1.ID, "ID should match")
|
|
assert.True(t, section1.IsSystem, "IsSystem should be boolean true")
|
|
assert.Equal(t, "Continue Reading", section1.Title, "Title should be string")
|
|
assert.Equal(t, "Books you're currently reading", section1.Description, "Description should be string")
|
|
assert.Equal(t, "📖", section1.Icon, "Icon should be string")
|
|
assert.Equal(t, 1, section1.Priority, "Priority should be number")
|
|
assert.Equal(t, "/collections/00000000-0000-0000-0000-000000000000", section1.ViewAllURL, "ViewAllURL should match collection URL")
|
|
|
|
section2 := result[1]
|
|
assert.Equal(t, "my-favorites", section2.ID)
|
|
assert.False(t, section2.IsSystem, "IsSystem should be boolean false")
|
|
assert.Equal(t, "My Favorites", section2.Title)
|
|
assert.Equal(t, "/collections/00000000-0000-0000-0000-000000000000", section2.ViewAllURL, "ViewAllURL should be set for user collections")
|
|
}
|
|
|
|
func TestBuildSections_ConvertsItemsCorrectly(t *testing.T) {
|
|
itemUUID := uuid.New()
|
|
|
|
serviceSections := []services.DashboardSection{
|
|
{
|
|
CollectionID: uuid.UUID{},
|
|
CollectionName: "continue-reading",
|
|
QueryType: "continue-reading",
|
|
Priority: 1,
|
|
IsSystem: true,
|
|
Title: "Continue Reading",
|
|
Description: "Books you're currently reading",
|
|
Icon: "📖",
|
|
Items: []database.MediaItems{
|
|
{
|
|
ID: pgtype.UUID{Bytes: itemUUID, Valid: true},
|
|
Title: "Test Book",
|
|
Author: pgtype.Text{String: "Test Author", Valid: true},
|
|
CoverImagePath: pgtype.Text{String: "/path/to/cover.jpg", Valid: true},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := BuildSections(serviceSections, "", "", map[string]string{})
|
|
|
|
assert.Equal(t, 1, len(result), "should have 1 section")
|
|
section := result[0]
|
|
assert.Equal(t, 1, len(section.Items), "should have 1 item")
|
|
|
|
item := section.Items[0]
|
|
assert.Equal(t, itemUUID.String(), item.MediaItemID, "MediaItemID should match")
|
|
assert.Equal(t, "Test Book", item.Title, "Title should be string")
|
|
assert.Equal(t, "Test Author", item.Author, "Author should be string")
|
|
assert.Equal(t, "/path/to/cover.jpg", item.CoverImagePath, "CoverImagePath should be string")
|
|
}
|
|
|
|
func TestTextToString_Valid(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input pgtype.Text
|
|
expected string
|
|
}{
|
|
{
|
|
name: "valid text",
|
|
input: pgtype.Text{String: "hello", Valid: true},
|
|
expected: "hello",
|
|
},
|
|
{
|
|
name: "empty text",
|
|
input: pgtype.Text{String: "", Valid: true},
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "invalid text",
|
|
input: pgtype.Text{String: "ignored", Valid: false},
|
|
expected: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := textToString(tt.input)
|
|
assert.Equal(t, tt.expected, result, "textToString result mismatch")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetViewAllURL_SystemCollections(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
collectionID string
|
|
libraryID string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "continue-reading with no library",
|
|
collectionID: "continue-reading",
|
|
libraryID: "",
|
|
expected: "/collections/continue-reading",
|
|
},
|
|
{
|
|
name: "continue-reading with library",
|
|
collectionID: "continue-reading",
|
|
libraryID: "lib-123",
|
|
expected: "/collections/continue-reading?library_id=lib-123",
|
|
},
|
|
{
|
|
name: "recently-added with no library",
|
|
collectionID: "recently-added",
|
|
libraryID: "",
|
|
expected: "/collections/recently-added",
|
|
},
|
|
{
|
|
name: "recently-read with library",
|
|
collectionID: "recently-read",
|
|
libraryID: "lib-456",
|
|
expected: "/collections/recently-read?library_id=lib-456",
|
|
},
|
|
{
|
|
name: "not-started with no library",
|
|
collectionID: "not-started",
|
|
libraryID: "",
|
|
expected: "/collections/not-started",
|
|
},
|
|
{
|
|
name: "user collection with library",
|
|
collectionID: "my-favorites",
|
|
libraryID: "lib-789",
|
|
expected: "/collections/my-favorites?library_id=lib-789",
|
|
},
|
|
{
|
|
name: "empty collection ID",
|
|
collectionID: "",
|
|
libraryID: "lib-123",
|
|
expected: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := getViewAllURL(tt.collectionID, tt.libraryID)
|
|
assert.Equal(t, tt.expected, result, "getViewAllURL mismatch")
|
|
})
|
|
}
|
|
}
|