package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type SeriesIntegrationTestSuite struct { suite.Suite setup *TestServerSetup token string libraryID string } func (s *SeriesIntegrationTestSuite) SetupSuite() { s.setup = setupTestServer(s.T()) s.token = s.setup.Token s.libraryID = createTestLibraryWithFolder(s.T(), s.setup.Server, s.token, "Test Series Library", false) } func (s *SeriesIntegrationTestSuite) TearDownSuite() { s.setup.Close() } func (s *SeriesIntegrationTestSuite) TestGetSeries_RequiresLibraryID() { req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series", nil) req.Header.Set("Authorization", "Bearer "+s.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) var body map[string]string err = json.NewDecoder(resp.Body).Decode(&body) require.NoError(s.T(), err) assert.Equal(s.T(), "library_id required", body["error"]) } func (s *SeriesIntegrationTestSuite) TestGetSeries_InvalidLibraryID() { req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series?library_id=not-a-uuid", nil) req.Header.Set("Authorization", "Bearer "+s.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) var body map[string]string err = json.NewDecoder(resp.Body).Decode(&body) require.NoError(s.T(), err) assert.Equal(s.T(), "invalid library_id", body["error"]) } func (s *SeriesIntegrationTestSuite) TestGetSeries_EmptyLibrary() { url := fmt.Sprintf("%s/api/series?library_id=%s", s.setup.Server.URL, s.libraryID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+s.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 body map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&body) require.NoError(s.T(), err) series, ok := body["series"].([]interface{}) require.True(s.T(), ok, "series should be an array") assert.Empty(s.T(), series, "empty library should have no series") total, ok := body["total"].(float64) require.True(s.T(), ok, "total should be a number") assert.Equal(s.T(), float64(0), total) } func (s *SeriesIntegrationTestSuite) TestGetSeries_Unauthorized() { req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series?library_id="+s.libraryID, 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 *SeriesIntegrationTestSuite) TestGetSeries_PaginationParams() { url := fmt.Sprintf("%s/api/series?library_id=%s&limit=5&offset=0", s.setup.Server.URL, s.libraryID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+s.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 body map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&body) require.NoError(s.T(), err) limit, ok := body["limit"].(float64) require.True(s.T(), ok) assert.Equal(s.T(), float64(5), limit) offset, ok := body["offset"].(float64) require.True(s.T(), ok) assert.Equal(s.T(), float64(0), offset) } func (s *SeriesIntegrationTestSuite) TestGetSeries_LimitClampedTo100() { url := fmt.Sprintf("%s/api/series?library_id=%s&limit=999", s.setup.Server.URL, s.libraryID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+s.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 body map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&body) require.NoError(s.T(), err) limit, ok := body["limit"].(float64) require.True(s.T(), ok) assert.Equal(s.T(), float64(100), limit, "limit should be clamped to 100") } func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_RequiresLibraryID() { req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series/books?name=Test+Series", nil) req.Header.Set("Authorization", "Bearer "+s.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) var body map[string]string err = json.NewDecoder(resp.Body).Decode(&body) require.NoError(s.T(), err) assert.Equal(s.T(), "library_id required", body["error"]) } func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_RequiresName() { url := fmt.Sprintf("%s/api/series/books?library_id=%s", s.setup.Server.URL, s.libraryID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+s.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) var body map[string]string err = json.NewDecoder(resp.Body).Decode(&body) require.NoError(s.T(), err) assert.Equal(s.T(), "name required", body["error"]) } func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_InvalidLibraryID() { req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series/books?library_id=bad-uuid&name=Test", nil) req.Header.Set("Authorization", "Bearer "+s.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 *SeriesIntegrationTestSuite) TestGetSeriesBooks_NonexistentSeries() { url := fmt.Sprintf("%s/api/series/books?library_id=%s&name=Nonexistent+Series", s.setup.Server.URL, s.libraryID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+s.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 body map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&body) require.NoError(s.T(), err) books, ok := body["books"].([]interface{}) require.True(s.T(), ok, "books should be an array") assert.Empty(s.T(), books, "nonexistent series should return empty books array") assert.Equal(s.T(), "Nonexistent Series", body["name"]) total, ok := body["total"].(float64) require.True(s.T(), ok) assert.Equal(s.T(), float64(0), total) } func (s *SeriesIntegrationTestSuite) TestGetSeriesBooks_Unauthorized() { req, _ := http.NewRequest("GET", s.setup.Server.URL+"/api/series/books?library_id="+s.libraryID+"&name=Test", 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 *SeriesIntegrationTestSuite) TestGetSeries_SpecialCharactersInName() { seriesName := "Series: Book & Other (Vol. 1)" url := fmt.Sprintf("%s/api/series/books?library_id=%s&name=%s", s.setup.Server.URL, s.libraryID, url.QueryEscape(seriesName)) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+s.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) } func (s *SeriesIntegrationTestSuite) TestGetSeries_ResponseStructure() { url := fmt.Sprintf("%s/api/series?library_id=%s", s.setup.Server.URL, s.libraryID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+s.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 body map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&body) require.NoError(s.T(), err) assert.Contains(s.T(), body, "series", "response should contain 'series' key") assert.Contains(s.T(), body, "total", "response should contain 'total' key") assert.Contains(s.T(), body, "limit", "response should contain 'limit' key") assert.Contains(s.T(), body, "offset", "response should contain 'offset' key") _, ok := body["series"].([]interface{}) assert.True(s.T(), ok, "'series' should be an array") } func (s *SeriesIntegrationTestSuite) TestRestoreSystemCollection_ContinueSeries() { reqBody := map[string]interface{}{ "collection_name": "Continue Series", } 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 "+s.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, "message") } func (s *SeriesIntegrationTestSuite) TestGetSections_IncludesContinueSeries() { url := fmt.Sprintf("%s/api/dashboard/sections?library_id=%s", s.setup.Server.URL, s.libraryID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+s.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") sectionIDs := make(map[string]bool) for _, sec := range sections { section := sec.(map[string]interface{}) sectionIDs[section["id"].(string)] = true } assert.Contains(s.T(), sectionIDs, "continue-series", "dashboard should include continue-series section") assert.Contains(s.T(), sectionIDs, "continue-reading") assert.Contains(s.T(), sectionIDs, "recently-added") assert.Contains(s.T(), sectionIDs, "recently-read") assert.Contains(s.T(), sectionIDs, "not-started") } func TestSeriesIntegrationTestSuite(t *testing.T) { suite.Run(t, new(SeriesIntegrationTestSuite)) }