872 lines
24 KiB
Go
872 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
baseURL = "http://localhost:8765"
|
|
)
|
|
|
|
type TestContext struct {
|
|
AdminToken string
|
|
UserToken string
|
|
AdminID string
|
|
UserID string
|
|
LibraryID string
|
|
EbookID string
|
|
MediaItemID string
|
|
NoteID string
|
|
HighlightID string
|
|
}
|
|
|
|
type AuthResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
Token string `json:"token"`
|
|
TokenType string `json:"token_type"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
User struct {
|
|
ID string `json:"id"`
|
|
Email string `json:"email"`
|
|
Username string `json:"username"`
|
|
Role string `json:"role"`
|
|
} `json:"user"`
|
|
}
|
|
|
|
type PaginatedResponse struct {
|
|
Data interface{} `json:"data"`
|
|
Total int `json:"total"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
}
|
|
|
|
func makeRequest(t *testing.T, method, endpoint string, body interface{}, token string) *http.Response {
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
jsonBody, err := json.Marshal(body)
|
|
require.NoError(t, err)
|
|
reqBody = bytes.NewBuffer(jsonBody)
|
|
}
|
|
|
|
req, err := http.NewRequest(method, baseURL+endpoint, reqBody)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
|
|
return resp
|
|
}
|
|
|
|
func extractToken(authResp AuthResponse) string {
|
|
if authResp.AccessToken != "" {
|
|
return authResp.AccessToken
|
|
}
|
|
if authResp.Token != "" {
|
|
return authResp.Token
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func setupTestSuite(t *testing.T) *TestContext {
|
|
ctx := &TestContext{}
|
|
|
|
t.Run("Setup_GetAdminCredentials", func(t *testing.T) {
|
|
// Try to login as existing admin first
|
|
loginReq := map[string]interface{}{
|
|
"login": "test@example.com",
|
|
"password": "Password123!",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/auth/login", loginReq, "")
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
var authResp AuthResponse
|
|
body, _ := io.ReadAll(resp.Body)
|
|
err := json.Unmarshal(body, &authResp)
|
|
require.NoError(t, err)
|
|
|
|
ctx.AdminToken = extractToken(authResp)
|
|
ctx.AdminID = authResp.User.ID
|
|
t.Logf("Logged in as existing admin: %s (%s)", authResp.User.Email, authResp.User.Role)
|
|
return
|
|
}
|
|
|
|
// If no admin exists, create one
|
|
adminReq := map[string]interface{}{
|
|
"email": "integrationadmin@test.com",
|
|
"username": "integrationadmin",
|
|
"password": "AdminPass123!",
|
|
}
|
|
|
|
resp = makeRequest(t, "POST", "/api/auth/register", adminReq, "")
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
|
|
var authResp AuthResponse
|
|
body, _ := io.ReadAll(resp.Body)
|
|
err := json.Unmarshal(body, &authResp)
|
|
require.NoError(t, err)
|
|
|
|
ctx.AdminToken = extractToken(authResp)
|
|
ctx.AdminID = authResp.User.ID
|
|
t.Logf("Created new admin: %s (%s)", authResp.User.Email, authResp.User.Role)
|
|
} else {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("Failed to create admin: %s", string(body))
|
|
}
|
|
|
|
require.NotEmpty(t, ctx.AdminToken, "Admin token is empty")
|
|
})
|
|
|
|
t.Run("Setup_GetUserCredentials", func(t *testing.T) {
|
|
// Try to login as existing regular user
|
|
loginReq := map[string]interface{}{
|
|
"login": "admin@test.com",
|
|
"password": "Admin123!",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/auth/login", loginReq, "")
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
var authResp AuthResponse
|
|
body, _ := io.ReadAll(resp.Body)
|
|
err := json.Unmarshal(body, &authResp)
|
|
require.NoError(t, err)
|
|
|
|
ctx.UserToken = extractToken(authResp)
|
|
ctx.UserID = authResp.User.ID
|
|
t.Logf("Logged in as existing user: %s (%s)", authResp.User.Email, authResp.User.Role)
|
|
return
|
|
}
|
|
|
|
// Create a regular user
|
|
userReq := map[string]interface{}{
|
|
"email": "integrationuser@test.com",
|
|
"username": "integrationuser",
|
|
"password": "UserPass123!",
|
|
}
|
|
|
|
resp = makeRequest(t, "POST", "/api/auth/register", userReq, "")
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
|
|
var authResp AuthResponse
|
|
body, _ := io.ReadAll(resp.Body)
|
|
err := json.Unmarshal(body, &authResp)
|
|
require.NoError(t, err)
|
|
|
|
ctx.UserToken = extractToken(authResp)
|
|
ctx.UserID = authResp.User.ID
|
|
t.Logf("Created new user: %s (%s)", authResp.User.Email, authResp.User.Role)
|
|
}
|
|
|
|
require.NotEmpty(t, ctx.UserToken, "User token is empty")
|
|
})
|
|
|
|
t.Run("Setup_GetLibrary", func(t *testing.T) {
|
|
// First try to list existing libraries
|
|
resp := makeRequest(t, "GET", "/api/libraries", nil, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
// Try to unmarshal as array first
|
|
var librariesArray []map[string]interface{}
|
|
errArray := json.Unmarshal(body, &librariesArray)
|
|
|
|
if errArray == nil && len(librariesArray) > 0 {
|
|
ctx.LibraryID = librariesArray[0]["id"].(string)
|
|
t.Logf("Using existing library: %v", librariesArray[0]["name"])
|
|
return
|
|
}
|
|
|
|
// Try to unmarshal as object with data field
|
|
var result map[string]interface{}
|
|
errObj := json.Unmarshal(body, &result)
|
|
if errObj == nil {
|
|
if libraries, ok := result["data"].([]interface{}); ok && len(libraries) > 0 {
|
|
if firstLib, ok := libraries[0].(map[string]interface{}); ok {
|
|
ctx.LibraryID = firstLib["id"].(string)
|
|
t.Logf("Using existing library: %v", firstLib["name"])
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a library
|
|
libReq := map[string]interface{}{
|
|
"name": "Integration Test Library",
|
|
"description": "Library for integration tests",
|
|
"type": "ebooks",
|
|
}
|
|
|
|
resp = makeRequest(t, "POST", "/api/libraries", libReq, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK {
|
|
var lib map[string]interface{}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
err := json.Unmarshal(body, &lib)
|
|
require.NoError(t, err)
|
|
|
|
ctx.LibraryID = lib["id"].(string)
|
|
t.Logf("Created new library: %v", lib["name"])
|
|
}
|
|
|
|
require.NotEmpty(t, ctx.LibraryID, "Library ID is empty")
|
|
})
|
|
|
|
t.Run("Setup_WaitForRateLimit", func(t *testing.T) {
|
|
// Wait a bit to avoid rate limiting
|
|
time.Sleep(2 * time.Second)
|
|
})
|
|
|
|
return ctx
|
|
}
|
|
|
|
func TestIntegrationAPI(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration tests in short mode")
|
|
}
|
|
|
|
ctx := setupTestSuite(t)
|
|
|
|
t.Run("Authentication", func(t *testing.T) {
|
|
testAuthentication(t, ctx)
|
|
})
|
|
|
|
t.Run("UserProfile", func(t *testing.T) {
|
|
testUserProfile(t, ctx)
|
|
})
|
|
|
|
t.Run("Libraries", func(t *testing.T) {
|
|
testLibraries(t, ctx)
|
|
})
|
|
|
|
t.Run("Ebooks", func(t *testing.T) {
|
|
testEbooks(t, ctx)
|
|
})
|
|
|
|
t.Run("MediaItems", func(t *testing.T) {
|
|
testMediaItems(t, ctx)
|
|
})
|
|
|
|
t.Run("Admin", func(t *testing.T) {
|
|
testAdmin(t, ctx)
|
|
})
|
|
}
|
|
|
|
func testAuthentication(t *testing.T, ctx *TestContext) {
|
|
t.Run("Register_DuplicateEmail", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"email": "test@example.com",
|
|
"username": "newuser123",
|
|
"password": "Password123!",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/auth/register", req, "")
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusConflict, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Register_DuplicateUsername", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"email": "newemail@example.com",
|
|
"username": "testuser",
|
|
"password": "Password123!",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/auth/register", req, "")
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusConflict, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Register_WeakPassword", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"email": "weak@example.com",
|
|
"username": "weakuser",
|
|
"password": "weak",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/auth/register", req, "")
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Login_InvalidCredentials", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"login": "test@example.com",
|
|
"password": "wrongpassword",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/auth/login", req, "")
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("ProtectedEndpoint_NoAuth", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/auth/profile", nil, "")
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("ProtectedEndpoint_ValidAuth", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/auth/profile", nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
func testUserProfile(t *testing.T, ctx *TestContext) {
|
|
t.Run("GetProfile", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/auth/profile", nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var profile map[string]interface{}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
err := json.Unmarshal(body, &profile)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, ctx.UserID, profile["id"])
|
|
})
|
|
|
|
t.Run("UpdateProfile", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"first_name": "Integration",
|
|
"last_name": "Test User",
|
|
}
|
|
|
|
resp := makeRequest(t, "PUT", "/api/auth/profile", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("UpdateEmail", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"email": "integrationnew@example.com",
|
|
}
|
|
|
|
resp := makeRequest(t, "PUT", "/api/auth/email", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
// Should succeed
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict)
|
|
|
|
// Change back
|
|
req = map[string]interface{}{
|
|
"email": "integrationuser@test.com",
|
|
}
|
|
resp = makeRequest(t, "PUT", "/api/auth/email", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
})
|
|
|
|
t.Run("UpdateUsername", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"username": "integrationuser2",
|
|
}
|
|
|
|
resp := makeRequest(t, "PUT", "/api/auth/username", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict)
|
|
})
|
|
|
|
t.Run("UpdatePassword", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"current_password": "UserPass123!",
|
|
"new_password": "NewPass123!",
|
|
"confirm_password": "NewPass123!",
|
|
}
|
|
|
|
resp := makeRequest(t, "PUT", "/api/auth/password", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
// Change back
|
|
req = map[string]interface{}{
|
|
"current_password": "NewPass123!",
|
|
"new_password": "UserPass123!",
|
|
"confirm_password": "UserPass123!",
|
|
}
|
|
resp = makeRequest(t, "PUT", "/api/auth/password", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
})
|
|
|
|
t.Run("UpdateTheme", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"theme": "tokyo-night",
|
|
}
|
|
|
|
resp := makeRequest(t, "PUT", "/api/auth/theme", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("DeleteOwnAccount", func(t *testing.T) {
|
|
// Create a temporary user
|
|
tempReq := map[string]interface{}{
|
|
"email": "tempdelete@example.com",
|
|
"username": "tempdelete",
|
|
"password": "TempPass123!",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/auth/register", tempReq, "")
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
|
|
var authResp AuthResponse
|
|
body, _ := io.ReadAll(resp.Body)
|
|
json.Unmarshal(body, &authResp)
|
|
|
|
tempToken := extractToken(authResp)
|
|
|
|
resp = makeRequest(t, "DELETE", "/api/auth/account", nil, tempToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func testLibraries(t *testing.T, ctx *TestContext) {
|
|
t.Run("GetLibraryTypes", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/libraries/types", nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("CreateLibrary_UserForbidden", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"name": "User Library",
|
|
"description": "Should fail",
|
|
"type": "ebooks",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/libraries", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("ListLibraries_Admin", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/libraries", nil, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("ListLibraries_UserForbidden", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/libraries", nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetLibrary_Admin", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", fmt.Sprintf("/api/libraries/%s", ctx.LibraryID), nil, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("UpdateLibrary_Admin", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"name": "Updated Library Name",
|
|
"description": "Updated during integration test",
|
|
"type": "ebooks",
|
|
}
|
|
|
|
resp := makeRequest(t, "PUT", fmt.Sprintf("/api/libraries/%s", ctx.LibraryID), req, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("SetLibraryVisibility", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"library_ids": []string{ctx.LibraryID},
|
|
"is_visible": true,
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/libraries/visibility", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetUserVisibleLibraries", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/libraries/visible", nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetLibraryStats", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", fmt.Sprintf("/api/libraries/%s/stats", ctx.LibraryID), nil, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("AddLibraryFolder", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"folder_path": "/tmp/test_integration_folder",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", fmt.Sprintf("/api/libraries/%s/folders", ctx.LibraryID), req, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
// May fail if path doesn't exist, but endpoint should be accessible
|
|
assert.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusConflict)
|
|
})
|
|
|
|
t.Run("GetLibraryFolders", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", fmt.Sprintf("/api/libraries/%s/folders", ctx.LibraryID), nil, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
func testEbooks(t *testing.T, ctx *TestContext) {
|
|
t.Run("ListEbooks", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/ebooks", nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("CreateEbook_Admin", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"title": "Integration Test Ebook",
|
|
"author": "Test Author",
|
|
"isbn": "9999999999",
|
|
"description": "Created during integration test",
|
|
"file_path": "/tmp/test_integration.epub",
|
|
"file_size": 1024,
|
|
"mime_type": "application/epub+zip",
|
|
"publisher": "Test Publisher",
|
|
"date_published": "2024-01-01",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/ebooks", req, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusCreated {
|
|
var ebook map[string]interface{}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
err := json.Unmarshal(body, &ebook)
|
|
require.NoError(t, err)
|
|
|
|
ctx.EbookID = ebook["id"].(string)
|
|
}
|
|
})
|
|
|
|
t.Run("CreateEbook_UserForbidden", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"title": "User Ebook",
|
|
"file_path": "/tmp/user.epub",
|
|
"file_size": 1024,
|
|
"mime_type": "application/epub+zip",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/ebooks", req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
})
|
|
|
|
if ctx.EbookID != "" {
|
|
t.Run("GetEbook", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", fmt.Sprintf("/api/ebooks/%s", ctx.EbookID), nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("UpdateEbook_Admin", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"title": "Updated Ebook Title",
|
|
"description": "Updated during test",
|
|
}
|
|
|
|
resp := makeRequest(t, "PUT", fmt.Sprintf("/api/ebooks/%s", ctx.EbookID), req, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("CreateEbookRating", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"rating": 5,
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", fmt.Sprintf("/api/ebooks/%s/rating", ctx.EbookID), req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK)
|
|
})
|
|
|
|
t.Run("GetEbookRating", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", fmt.Sprintf("/api/ebooks/%s/rating", ctx.EbookID), nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound)
|
|
})
|
|
|
|
t.Run("UpdateReadingProgress", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"progress_percentage": 50,
|
|
"page_number": 125,
|
|
"total_pages": 250,
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", fmt.Sprintf("/api/ebooks/%s/progress", ctx.EbookID), req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("GetReadingProgress", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", fmt.Sprintf("/api/ebooks/%s/progress", ctx.EbookID), nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testMediaItems(t *testing.T, ctx *TestContext) {
|
|
t.Run("ListMediaItems", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/media-items", nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
// Try to unmarshal as array first
|
|
var itemsArray []map[string]interface{}
|
|
errArray := json.Unmarshal(body, &itemsArray)
|
|
|
|
if errArray == nil && len(itemsArray) > 0 {
|
|
ctx.MediaItemID = itemsArray[0]["id"].(string)
|
|
return
|
|
}
|
|
|
|
// Try to unmarshal as object with data field
|
|
var result map[string]interface{}
|
|
errObj := json.Unmarshal(body, &result)
|
|
if errObj == nil {
|
|
if items, ok := result["data"].([]interface{}); ok && len(items) > 0 {
|
|
if firstItem, ok := items[0].(map[string]interface{}); ok {
|
|
ctx.MediaItemID = firstItem["id"].(string)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if ctx.MediaItemID != "" {
|
|
t.Run("GetMediaItem", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", fmt.Sprintf("/api/media-items/%s", ctx.MediaItemID), nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("CreateMediaNote", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"content": "Integration test note",
|
|
"position": "page:42",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", fmt.Sprintf("/api/media-items/%s/notes", ctx.MediaItemID), req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusCreated {
|
|
var note map[string]interface{}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
err := json.Unmarshal(body, ¬e)
|
|
require.NoError(t, err)
|
|
|
|
ctx.NoteID = note["id"].(string)
|
|
}
|
|
})
|
|
|
|
t.Run("GetMediaNotes", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", fmt.Sprintf("/api/media-items/%s/notes", ctx.MediaItemID), nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
if ctx.NoteID != "" {
|
|
t.Run("UpdateMediaNote", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"content": "Updated integration test note",
|
|
}
|
|
|
|
resp := makeRequest(t, "PUT", fmt.Sprintf("/api/media-items/%s/notes/%s", ctx.MediaItemID, ctx.NoteID), req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("DeleteMediaNote", func(t *testing.T) {
|
|
resp := makeRequest(t, "DELETE", fmt.Sprintf("/api/media-items/%s/notes/%s", ctx.MediaItemID, ctx.NoteID), nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
t.Run("CreateMediaHighlight", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"content": "Integration test highlight",
|
|
"position": "page:42",
|
|
"color": "yellow",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", fmt.Sprintf("/api/media-items/%s/highlights", ctx.MediaItemID), req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusCreated {
|
|
var highlight map[string]interface{}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
err := json.Unmarshal(body, &highlight)
|
|
require.NoError(t, err)
|
|
|
|
ctx.HighlightID = highlight["id"].(string)
|
|
}
|
|
})
|
|
|
|
t.Run("GetMediaHighlights", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", fmt.Sprintf("/api/media-items/%s/highlights", ctx.MediaItemID), nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
if ctx.HighlightID != "" {
|
|
t.Run("UpdateMediaHighlight", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"content": "Updated integration test highlight",
|
|
"color": "blue",
|
|
}
|
|
|
|
resp := makeRequest(t, "PUT", fmt.Sprintf("/api/media-items/%s/highlights/%s", ctx.MediaItemID, ctx.HighlightID), req, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("DeleteMediaHighlight", func(t *testing.T) {
|
|
resp := makeRequest(t, "DELETE", fmt.Sprintf("/api/media-items/%s/highlights/%s", ctx.MediaItemID, ctx.HighlightID), nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func testAdmin(t *testing.T, ctx *TestContext) {
|
|
t.Run("ListUsers_Admin", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/auth/users", nil, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
// Try to unmarshal as array first
|
|
var usersArray []map[string]interface{}
|
|
errArray := json.Unmarshal(body, &usersArray)
|
|
|
|
if errArray == nil {
|
|
assert.GreaterOrEqual(t, len(usersArray), 1)
|
|
return
|
|
}
|
|
|
|
// Try to unmarshal as object with data field
|
|
var result map[string]interface{}
|
|
errObj := json.Unmarshal(body, &result)
|
|
if errObj == nil {
|
|
if users, ok := result["data"].([]interface{}); ok {
|
|
assert.GreaterOrEqual(t, len(users), 1)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("ListUsers_UserForbidden", func(t *testing.T) {
|
|
resp := makeRequest(t, "GET", "/api/auth/users", nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("DeleteUserAccount_Admin", func(t *testing.T) {
|
|
// Create a user to delete
|
|
createReq := map[string]interface{}{
|
|
"email": "deleteme@example.com",
|
|
"username": "deleteme",
|
|
"password": "DeleteMe123!",
|
|
}
|
|
|
|
resp := makeRequest(t, "POST", "/api/auth/register", createReq, "")
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
|
|
var authResp AuthResponse
|
|
body, _ := io.ReadAll(resp.Body)
|
|
json.Unmarshal(body, &authResp)
|
|
|
|
deleteURL := fmt.Sprintf("/api/auth/account?user_id=%s", authResp.User.ID)
|
|
resp = makeRequest(t, "DELETE", deleteURL, nil, ctx.AdminToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("DeleteUserAccount_UserForbidden", func(t *testing.T) {
|
|
deleteURL := fmt.Sprintf("/api/auth/account?user_id=%s", ctx.AdminID)
|
|
resp := makeRequest(t, "DELETE", deleteURL, nil, ctx.UserToken)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
})
|
|
}
|