Symptom: repeated full-suite runs poisoned the dev database with leaked rows (duplicate Prefs Scope libraries, colliding device names, saved-filter name conflicts on rerun) because tests created rows but never deleted them, and stale-binary runs left rows the new cleanup could not match. - test_helpers: library creation now registers t.Cleanup DELETE (the leak root cause — createTestLibraryWithFolder never cleaned up). Suite-wide cleanup passes for libraries/collections/devices/ saved-filters run both at setup (pre-pass against legacy residue) and in t.Cleanup (no per-run residue). Preserve the admin's real libraries; delete only convention-named test rows. - setup seeds canonical system collections every run keyed on query_type (idempotent — heals renames/stale tombstones), moved out of the preserved-admin branch so it runs for every suite start. - dashboard_integration: scope-testing library renamed into the test-row convention; ordering assertions on the restored "Want to Read" collection preserved. - filters_test: filter names moved into the convention (rerun-safe — name-uniqueness 409 no longer trips second runs); five .(string) type assertions panic-proofed with require.True guards (a panic here used to kill the whole test process mid-run, which is why a stack of "never ran" failures surfaced at once later). - device_test: device names moved into the convention (ILIKE %test% hit them; duplicate-device-name conflicts gone).
532 lines
19 KiB
Go
532 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))
|
|
}
|