Release / build-and-push (push) Successful in 3m2s
TestAudioResumeRewind drives the whole surface against the shared dev DB with the fresh per-test regular user (the live admin is never touched; the fixture book lives in a test-named library that cascades away): default 0, profile PUT "5" rides the chapters payload, non-allow-listed values 400 without side effects, explicit "0" resets everywhere, and an omitted JSON key leaves the column untouched. Changelog entry in docs/contract_pins.md (additive chapters key + the profile field's string-shaped PUT, keep_screen_awake precedent) and the get_profile.md example gains the preference keys. Debug note for the next agent: the first runs here 401'd with 'Ghost session rejected' — NOT a harness or auth bug. The tests never execute schema.sql; the shared dev DB gets its schema from the app container's boot. The rebuilt tests image ran the new GetUser (selecting audio_resume_rewind_seconds) against a DB whose schema came from the old app image — every GetUser errored, and the ghost-session guard turned that into 401s. Applying the schema.sql ALTER to the dev DB (what the next app rebuild does anyway) turned the suite green. Until make rebuild-app runs, the running app container serves the old binary.
124 lines
4.6 KiB
Go
124 lines
4.6 KiB
Go
package main
|
|
|
|
// Resume rewind (per-user server setting): seconds before the saved
|
|
// position the audiobook/podcast player starts (0 = exact resume).
|
|
// Contract: the profile endpoint owns reads/writes with a strict
|
|
// 0/5/10 allow-list (the request shape is the keep_screen_awake string
|
|
// precedent — the form select must be able to send an explicit Off),
|
|
// and the chapters payload carries the value so every client (web +
|
|
// Android, incl. the Auto service) gets it with its existing fetch.
|
|
// All writes ride the fresh per-test regular user — the shared admin
|
|
// account is never touched (AGENTS doctrine). Fixture book lives in a
|
|
// test-named library that cascades away with the library delete.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAudioResumeRewind(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
t.Cleanup(func() { _ = setup.Close() })
|
|
|
|
admin, _ := moveTestAdminID(t, setup)
|
|
libID, folder := audioTestLibrary(t, setup, fmt.Sprintf("Test Resume Rewind %d", time.Now().UnixNano()))
|
|
t.Cleanup(func() { deleteLibraries(t, setup, libID) })
|
|
|
|
// 664 Hz tone: not the shared helper's 440 or the speed test's 552,
|
|
// so the scanner's content dedup never skips this book.
|
|
writeTestTrackTone(t, filepath.Join(folder, "Rewind Book", "01 one.mp3"), 664)
|
|
scanTestFolder(t, setup, admin, folder)
|
|
|
|
var bookPath string
|
|
for _, item := range listLibraryAudioItems(t, setup, libID) {
|
|
if strings.Contains(filepath.ToSlash(item.FilePath), "Rewind Book") && bookPath == "" {
|
|
bookPath = item.FilePath
|
|
}
|
|
}
|
|
require.NotEmpty(t, bookPath, "Rewind Book item must exist")
|
|
book, found := findAudioItemByPath(t, setup, libID, bookPath)
|
|
require.True(t, found)
|
|
bookID := uuid.UUID(book.ID.Bytes)
|
|
|
|
client := &http.Client{}
|
|
token := setup.RegularToken
|
|
authed := func(method, url, body string) (*http.Response, func()) {
|
|
var req *http.Request
|
|
if body == "" {
|
|
req, _ = http.NewRequest(method, url, nil)
|
|
} else {
|
|
req, _ = http.NewRequest(method, url, strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
return resp, func() { _ = resp.Body.Close() }
|
|
}
|
|
getPayload := func(itemID uuid.UUID) (rewind int) {
|
|
resp, closer := authed("GET", setup.Server.URL+"/api/media-items/"+itemID.String()+"/chapters", "")
|
|
defer closer()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
var data struct {
|
|
ResumeRewindSeconds int `json:"resume_rewind_seconds"`
|
|
}
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&data))
|
|
return data.ResumeRewindSeconds
|
|
}
|
|
getProfileRewind := func() int {
|
|
resp, closer := authed("GET", setup.Server.URL+"/api/auth/profile", "")
|
|
defer closer()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
var profile struct {
|
|
AudioResumeRewind int `json:"audio_resume_rewind_seconds"`
|
|
}
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&profile))
|
|
return profile.AudioResumeRewind
|
|
}
|
|
|
|
t.Run("defaults to off (exact resume)", func(t *testing.T) {
|
|
require.Equal(t, 0, getProfileRewind())
|
|
require.Equal(t, 0, getPayload(bookID))
|
|
})
|
|
|
|
t.Run("profile PUT selects 5 and rides the chapters payload", func(t *testing.T) {
|
|
resp, closer := authed("PUT", setup.Server.URL+"/api/auth/profile", `{"audio_resume_rewind_seconds":"5"}`)
|
|
defer closer()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
require.Equal(t, 5, getProfileRewind())
|
|
require.Equal(t, 5, getPayload(bookID))
|
|
})
|
|
|
|
t.Run("non-allow-listed values are rejected and change nothing", func(t *testing.T) {
|
|
for _, body := range []string{`{"audio_resume_rewind_seconds":"7"}`, `{"audio_resume_rewind_seconds":"-5"}`, `{"audio_resume_rewind_seconds":"abc"}`} {
|
|
resp, closer := authed("PUT", setup.Server.URL+"/api/auth/profile", body)
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
closer()
|
|
}
|
|
require.Equal(t, 5, getProfileRewind())
|
|
})
|
|
|
|
t.Run("explicit off resets to 0 everywhere", func(t *testing.T) {
|
|
resp, closer := authed("PUT", setup.Server.URL+"/api/auth/profile", `{"audio_resume_rewind_seconds":"0"}`)
|
|
defer closer()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
require.Equal(t, 0, getProfileRewind())
|
|
require.Equal(t, 0, getPayload(bookID))
|
|
})
|
|
|
|
t.Run("JSON clients that omit the key leave it untouched", func(t *testing.T) {
|
|
resp, closer := authed("PUT", setup.Server.URL+"/api/auth/profile", `{"timezone":"UTC"}`)
|
|
defer closer()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
require.Equal(t, 0, getProfileRewind())
|
|
})
|
|
}
|