Closes the remaining integration-suite failures (handoff open bug 3),
line by line:
- Fixture family (the upsert equivalence): tags_filter_test
("Science Fiction" found 1 of 3; "Fantasy" 404'd on zero results),
media_item_isbn_test (normalization read back the first case's ISBN
everywhere; edge cases saw a nil ISBN; pagination collapsed 5
creates onto 1 row), search_unified_test (its 3 books were actually
1 row + two no-op creates) — all POSTed a constant file_path into
one library while CreateMediaItem upserts ON CONFLICT
(library_id, file_path). Every fixture now uses unique per-item
paths, the suite's universal convention elsewhere. TestTagsFilter
confirmed same-family: counts came right with no second bug.
- New TestMediaItemCreateDuplicatePath pins the honest-create
contract from da13898: 201 for the first create; 200 + duplicate
flag + the EXISTING row (never a 201 lie), second payload not
overwritten.
- library_browse_test: the admin subtest browsed the container's
/tmp and required it non-empty — env-coupled (empty /tmp → "was
[]"). Now browses a self-created t.TempDir with two subdirs and a
decoy file, asserting ONLY directories are listed.
- series_integration_test: three stale assertions re-anchored to the
shipped contracts, each with a ground-truth comment: /dashboard/
sections key identity on the collection NAME (BuildSections ID =
CollectionName; the web/app reorder + hidden prefs persist those
strings) — the kebab query_type is the storage key and is not
exposed; /api/series/books is name-ONLY (the app client and the
web SSR page both send name only; library_id is ignored, never
validated); /api/series is optional-scope BY DESIGN (GetDistinct-
Series: narg IS NULL OR library_id = narg aggregates across
libraries when omitted). The former 400-on-missing-library_id
expectations asserted validation that never shipped.
- gofmt rider on three untouched-in-this-arc test files
(dashboard_integration, search, test_helpers).
Full suite with -parallel=1: 217 groups pass, zero failures.
531 lines
19 KiB
Go
531 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
type DashboardIntegrationTestSuite struct {
|
|
suite.Suite
|
|
setup *TestServerSetup
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) SetupSuite() {
|
|
s.setup = setupTestServer(s.T())
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TearDownSuite() {
|
|
s.setup.Close()
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestGetSections_EndToEndFlow() {
|
|
token := s.setup.Token
|
|
|
|
// Create test library
|
|
libraryID := createTestLibraryWithFolder(s.T(), s.setup.Server, token, "Test Library", false)
|
|
|
|
// Execute API call
|
|
url := fmt.Sprintf("%s/api/dashboard/sections?library_id=%s", s.setup.Server.URL, libraryID)
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(s.T(), err)
|
|
|
|
sections, ok := response["sections"].([]interface{})
|
|
require.True(s.T(), ok, "sections should be an array")
|
|
require.Len(s.T(), sections, 5, "Should have 5 system collections")
|
|
|
|
// Verify response structure
|
|
sectionMap := make(map[string]map[string]interface{})
|
|
for _, sec := range sections {
|
|
section := sec.(map[string]interface{})
|
|
sectionMap[section["id"].(string)] = section
|
|
|
|
// Verify field types
|
|
assert.IsType(s.T(), false, section["is_system"], "is_system should be boolean")
|
|
assert.IsType(s.T(), "", section["title"], "title should be string")
|
|
assert.IsType(s.T(), "", section["description"], "description should be string")
|
|
assert.IsType(s.T(), "", section["icon"], "icon should be string")
|
|
assert.IsType(s.T(), float64(0), section["priority"], "priority should be number")
|
|
}
|
|
|
|
// Verify all system collections exist (names are the section ids;
|
|
// the canonical seed names are Title-case).
|
|
assert.Contains(s.T(), sectionMap, "Continue Reading")
|
|
assert.Contains(s.T(), sectionMap, "Recently Added")
|
|
assert.Contains(s.T(), sectionMap, "Recently Read")
|
|
assert.Contains(s.T(), sectionMap, "Not Started")
|
|
assert.Contains(s.T(), sectionMap, "Continue Series")
|
|
|
|
// Verify continue-reading is a system collection
|
|
continueReading := sectionMap["Continue Reading"]
|
|
assert.True(s.T(), continueReading["is_system"].(bool), "continue-reading should be system collection")
|
|
assert.Equal(s.T(), "📖", continueReading["icon"], "icon should match")
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestGetSections_MissingLibraryID() {
|
|
token := s.setup.Token
|
|
|
|
// No library_id = the All Libraries view: the endpoint aggregates
|
|
// sections across every library (200), it does not 400.
|
|
req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/dashboard/sections", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(s.T(), err)
|
|
sections, ok := response["sections"].([]interface{})
|
|
require.True(s.T(), ok, "sections should be an array")
|
|
assert.NotEmpty(s.T(), sections, "All Libraries view should aggregate sections")
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestGetSections_InvalidLibraryID() {
|
|
token := s.setup.Token
|
|
|
|
req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/dashboard/sections?library_id=invalid-uuid", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestGetSections_Unauthorized() {
|
|
req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/dashboard/sections?library_id="+uuid.New().String(), nil)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode)
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestUpdatePreferences_Success() {
|
|
token := s.setup.Token
|
|
libraryID := createTestLibraryWithFolder(s.T(), s.setup.Server, token, "Test Library", false)
|
|
|
|
reqBody := map[string]interface{}{
|
|
"library_id": libraryID,
|
|
"hidden_collections": []string{"not-started"},
|
|
"collection_order": []string{"recently-added", "continue-reading", "recently-read"},
|
|
"items_per_section": 20,
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
req, _ := http.NewRequest("PUT", s.setup.Server.URL+"/api/dashboard/preferences", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(s.T(), err)
|
|
|
|
assert.Contains(s.T(), response, "hidden_collections")
|
|
assert.Contains(s.T(), response, "collection_order")
|
|
assert.Contains(s.T(), response, "items_per_section")
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestUpdatePreferences_Unauthorized() {
|
|
reqBody := map[string]interface{}{
|
|
"library_id": uuid.New().String(),
|
|
"hidden_collections": []string{},
|
|
"collection_order": []string{},
|
|
"items_per_section": 20,
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
req, _ := http.NewRequest("PUT", s.setup.Server.URL+"/api/dashboard/preferences", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode)
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestRestoreSystemCollection_InvalidName() {
|
|
token := s.setup.Token
|
|
|
|
reqBody := map[string]interface{}{
|
|
"collection_name": "invalid-collection-name",
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/dashboard/restore-system-collection", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestRestoreSystemCollection_Unauthorized() {
|
|
reqBody := map[string]interface{}{
|
|
"collection_name": "continue-reading",
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/dashboard/restore-system-collection", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode)
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestRestoreSystemCollection_ValidNames() {
|
|
token := s.setup.Token
|
|
|
|
validCollections := []string{"Continue Reading", "Recently Added", "Recently Read", "Not Started", "Continue Series"}
|
|
|
|
for _, collName := range validCollections {
|
|
s.T().Run(collName, func(t *testing.T) {
|
|
reqBody := map[string]interface{}{
|
|
"collection_name": collName,
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/dashboard/restore-system-collection", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(s.T(), err)
|
|
|
|
assert.Contains(t, response, "message")
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestUpdatePreferences_GlobalScope() {
|
|
token := s.setup.Token
|
|
|
|
// Absent library_id (JSON null after omit) = the global All Libraries
|
|
// scope: stored in user_dashboard_preferences_global, one row per user.
|
|
reqBody := map[string]interface{}{
|
|
"hidden_collections": []string{"not-started"},
|
|
"collection_order": []string{"continue-reading", "recently-added"},
|
|
"items_per_section": 15,
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
req, _ := http.NewRequest("PUT", s.setup.Server.URL+"/api/dashboard/preferences", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
|
|
// Second global save with different values: must UPDATE the same row
|
|
// (the old ON CONFLICT (user_id, library_id) never fired for NULLs and
|
|
// would have minted a duplicate per save).
|
|
reqBody["items_per_section"] = 25
|
|
body, _ = json.Marshal(reqBody)
|
|
req, _ = http.NewRequest("PUT", s.setup.Server.URL+"/api/dashboard/preferences", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err = client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
|
|
// Reading prefs without library_id returns the global row (not some
|
|
// arbitrary library's).
|
|
req, _ = http.NewRequest("GET", s.setup.Server.URL+"/api/dashboard/preferences", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err = client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
|
|
var prefs map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&prefs)
|
|
require.NoError(s.T(), err)
|
|
assert.Equal(s.T(), float64(25), prefs["items_per_section"], "global row should carry the second save")
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestUpdatePreferences_EmptyLibraryIDRejected() {
|
|
token := s.setup.Token
|
|
|
|
// Empty string is a LOST value (a client bug), never a scope — must
|
|
// stay a loud 400 rather than silently writing the global scope.
|
|
reqBody := map[string]interface{}{
|
|
"library_id": "",
|
|
"hidden_collections": []string{},
|
|
"collection_order": []string{},
|
|
"items_per_section": 20,
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
req, _ := http.NewRequest("PUT", s.setup.Server.URL+"/api/dashboard/preferences", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
|
|
assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
|
|
}
|
|
|
|
func (s *DashboardIntegrationTestSuite) TestGetPreferences_GlobalAndPerLibraryIndependent() {
|
|
token := s.setup.Token
|
|
libraryID := createTestLibraryWithFolder(s.T(), s.setup.Server, token, "Prefs Scope Test Library", false)
|
|
|
|
// Global scope: 15.
|
|
globalBody, _ := json.Marshal(map[string]interface{}{
|
|
"hidden_collections": []string{},
|
|
"collection_order": []string{},
|
|
"items_per_section": 15,
|
|
})
|
|
req, _ := http.NewRequest("PUT", s.setup.Server.URL+"/api/dashboard/preferences", bytes.NewBuffer(globalBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
resp.Body.Close()
|
|
require.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
|
|
// Per-library scope: 30. Must not bleed into global or vice versa.
|
|
libBody, _ := json.Marshal(map[string]interface{}{
|
|
"library_id": libraryID,
|
|
"hidden_collections": []string{},
|
|
"collection_order": []string{},
|
|
"items_per_section": 30,
|
|
})
|
|
req, _ = http.NewRequest("PUT", s.setup.Server.URL+"/api/dashboard/preferences", bytes.NewBuffer(libBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err = client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
resp.Body.Close()
|
|
require.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
|
|
// Per-library read.
|
|
req, _ = http.NewRequest("GET", s.setup.Server.URL+"/api/dashboard/preferences?library_id="+libraryID, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err = client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
require.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
var libPrefs map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&libPrefs)
|
|
require.NoError(s.T(), err)
|
|
assert.Equal(s.T(), float64(30), libPrefs["items_per_section"], "per-library scope should read its own row")
|
|
|
|
// Global read.
|
|
req, _ = http.NewRequest("GET", s.setup.Server.URL+"/api/dashboard/preferences", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err = client.Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
require.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
var globalPrefs map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&globalPrefs)
|
|
require.NoError(s.T(), err)
|
|
assert.Equal(s.T(), float64(15), globalPrefs["items_per_section"], "global scope should stay independent")
|
|
}
|
|
|
|
// TestDeleteSystemCollection_TombstonesAndRestores pins the system-
|
|
// collection permanence contract: DELETE hides the section everywhere
|
|
// (tombstone) and restore brings back the EXACT SAME row (id stable),
|
|
// never a fresh one.
|
|
func newHTTPClient() *http.Client { return &http.Client{} }
|
|
func (s *DashboardIntegrationTestSuite) TestDeleteSystemCollection_TombstonesAndRestores() {
|
|
token := s.setup.Token
|
|
libraryID := createTestLibraryWithFolder(s.T(), s.setup.Server, token, "Test Library", false)
|
|
|
|
collectionID := func(sectionID string) string {
|
|
url := fmt.Sprintf("%s/api/dashboard/sections?library_id=%s", s.setup.Server.URL, libraryID)
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := newHTTPClient().Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body)
|
|
require.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
var response map[string]interface{}
|
|
require.NoError(s.T(), json.NewDecoder(resp.Body).Decode(&response))
|
|
for _, sec := range response["sections"].([]interface{}) {
|
|
section := sec.(map[string]interface{})
|
|
if section["id"] == sectionID {
|
|
return section["collection_id"].(string)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
hasSection := func(sectionID string) bool {
|
|
url := fmt.Sprintf("%s/api/dashboard/sections?library_id=%s", s.setup.Server.URL, libraryID)
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := newHTTPClient().Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body)
|
|
var response map[string]interface{}
|
|
require.NoError(s.T(), json.NewDecoder(resp.Body).Decode(&response))
|
|
for _, sec := range response["sections"].([]interface{}) {
|
|
if sec.(map[string]interface{})["id"] == sectionID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
idBefore := collectionID("Continue Reading")
|
|
require.NotEmpty(s.T(), idBefore, "Continue Reading should exist")
|
|
|
|
// DELETE a system collection = tombstone (204, behaves like a delete).
|
|
delReq, _ := http.NewRequest("DELETE", s.setup.Server.URL+"/api/collections/"+idBefore, nil)
|
|
delReq.Header.Set("Authorization", "Bearer "+token)
|
|
delResp, err := newHTTPClient().Do(delReq)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(delResp.Body)
|
|
assert.Equal(s.T(), http.StatusNoContent, delResp.StatusCode)
|
|
|
|
// Invisible on the dashboard.
|
|
assert.False(s.T(), hasSection("Continue Reading"), "tombstoned section must vanish")
|
|
|
|
// Restore brings back the EXACT SAME row.
|
|
restoreBody, _ := json.Marshal(map[string]string{"collection_name": "Continue Reading"})
|
|
req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/dashboard/restore-system-collection", bytes.NewBuffer(restoreBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
restoreResp, err := newHTTPClient().Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(restoreResp.Body)
|
|
require.Equal(s.T(), http.StatusOK, restoreResp.StatusCode)
|
|
|
|
idAfter := collectionID("Continue Reading")
|
|
assert.Equal(s.T(), idBefore, idAfter, "restore must resurrect the SAME row id")
|
|
assert.True(s.T(), hasSection("Continue Reading"))
|
|
}
|
|
|
|
// TestDeleteUserCollection_HardDelete pins the user-collection contract:
|
|
// user collections hard-delete exactly as before (no tombstone, no restore).
|
|
func (s *DashboardIntegrationTestSuite) TestDeleteUserCollection_HardDelete() {
|
|
token := s.setup.Token
|
|
|
|
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test User Shelf", "type": "ebooks"})
|
|
req, _ := http.NewRequest("POST", s.setup.Server.URL+"/api/collections", bytes.NewBuffer(createBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
createResp, err := newHTTPClient().Do(req)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(createResp.Body)
|
|
require.Equal(s.T(), http.StatusCreated, createResp.StatusCode)
|
|
|
|
var created map[string]interface{}
|
|
require.NoError(s.T(), json.NewDecoder(createResp.Body).Decode(&created))
|
|
collectionID := created["id"].(string)
|
|
|
|
delReq, _ := http.NewRequest("DELETE", s.setup.Server.URL+"/api/collections/"+collectionID, nil)
|
|
delReq.Header.Set("Authorization", "Bearer "+token)
|
|
delResp, err := newHTTPClient().Do(delReq)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(delResp.Body)
|
|
assert.Equal(s.T(), http.StatusNoContent, delResp.StatusCode)
|
|
|
|
listReq, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/collections", nil)
|
|
listReq.Header.Set("Authorization", "Bearer "+token)
|
|
listResp, err := newHTTPClient().Do(listReq)
|
|
require.NoError(s.T(), err)
|
|
defer func(Body io.ReadCloser) { _ = Body.Close() }(listResp.Body)
|
|
var list struct {
|
|
Collections []map[string]interface{} `json:"collections"`
|
|
}
|
|
require.NoError(s.T(), json.NewDecoder(listResp.Body).Decode(&list))
|
|
for _, c := range list.Collections {
|
|
assert.NotEqual(s.T(), collectionID, c["id"], "hard-deleted user collection must be gone")
|
|
}
|
|
}
|
|
|
|
func TestDashboardIntegrationTestSuite(t *testing.T) {
|
|
suite.Run(t, new(DashboardIntegrationTestSuite))
|
|
}
|