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()) }) }