Fixed thumbnails ratio. Preparing user preference management
This commit is contained in:
@@ -174,6 +174,8 @@ class UserRepository:
|
|||||||
update_data["role"] = user_update.role
|
update_data["role"] = user_update.role
|
||||||
if user_update.is_active is not None:
|
if user_update.is_active is not None:
|
||||||
update_data["is_active"] = user_update.is_active
|
update_data["is_active"] = user_update.is_active
|
||||||
|
if user_update.preferences is not None:
|
||||||
|
update_data["preferences"] = user_update.preferences
|
||||||
|
|
||||||
# Remove None values from update data
|
# Remove None values from update data
|
||||||
clean_update_data = {k: v for k, v in update_data.items() if v is not None}
|
clean_update_data = {k: v for k, v in update_data.items() if v is not None}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ class UserUpdate(BaseModel):
|
|||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
role: Optional[UserRole] = None
|
role: Optional[UserRole] = None
|
||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
|
preferences: Optional[dict] = None
|
||||||
|
|
||||||
@field_validator('username')
|
@field_validator('username')
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -130,6 +131,7 @@ class UserInDB(BaseModel):
|
|||||||
hashed_password: str
|
hashed_password: str
|
||||||
role: UserRole
|
role: UserRole
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
|
preferences: dict = Field(default_factory=dict)
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|
||||||
|
|||||||
@@ -184,3 +184,18 @@ class UserService:
|
|||||||
bool: True if user exists, False otherwise
|
bool: True if user exists, False otherwise
|
||||||
"""
|
"""
|
||||||
return self.user_repository.user_exists(username)
|
return self.user_repository.user_exists(username)
|
||||||
|
|
||||||
|
def get_preference(self, user_id: str, preference):
|
||||||
|
user = self.get_user_by_id(user_id)
|
||||||
|
if user is None:
|
||||||
|
return None
|
||||||
|
return user.preferences.get(preference, None)
|
||||||
|
|
||||||
|
def set_preference(self, user_id: str, preference, value):
|
||||||
|
user = self.get_user_by_id(user_id)
|
||||||
|
if user is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
user.preferences[preference] = value
|
||||||
|
self.user_repository.update_user(user_id, UserUpdate(preferences=user.preferences))
|
||||||
|
return self.get_user_by_id(user_id)
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
/* Main Content Area */
|
/* Main Content Area */
|
||||||
.mainContent {
|
.mainContent {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0; /* Important for flex to work properly with scrolling */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main Content Inner Container */
|
/* Main Content Inner Container */
|
||||||
@@ -26,5 +28,9 @@
|
|||||||
max-width: 80rem; /* container max-width */
|
max-width: 80rem; /* container max-width */
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding: 2rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0; /* Important for flex to work properly with scrolling */
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* Supports different view modes: small, large, and detail
|
* Supports different view modes: small, large, and detail
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {memo} from 'react';
|
import React, {memo, useState, useEffect} from 'react';
|
||||||
import {API_BASE_URL} from "../../utils/api.js";
|
import {API_BASE_URL} from "../../utils/api.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,11 +40,67 @@ const formatDate = (dateString) => {
|
|||||||
*/
|
*/
|
||||||
const buildFullUrl = (relativePath) => {
|
const buildFullUrl = (relativePath) => {
|
||||||
if (!relativePath) return '';
|
if (!relativePath) return '';
|
||||||
// Use the base URL from your API configuration
|
|
||||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || API_BASE_URL;
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || API_BASE_URL;
|
||||||
return `${baseUrl}${relativePath}`;
|
return `${baseUrl}${relativePath}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to load protected images with bearer token
|
||||||
|
* @param {string} url - Image URL
|
||||||
|
* @returns {Object} { imageSrc, loading, error }
|
||||||
|
*/
|
||||||
|
const useProtectedImage = (url) => {
|
||||||
|
const [imageSrc, setImageSrc] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!url) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let objectUrl;
|
||||||
|
|
||||||
|
const fetchImage = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const fullUrl = buildFullUrl(url);
|
||||||
|
|
||||||
|
const response = await fetch(fullUrl, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load image');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
objectUrl = URL.createObjectURL(blob);
|
||||||
|
setImageSrc(objectUrl);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading thumbnail:', err);
|
||||||
|
setError(true);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchImage();
|
||||||
|
|
||||||
|
// Cleanup: revoke object URL on unmount
|
||||||
|
return () => {
|
||||||
|
if (objectUrl) {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return { imageSrc, loading, error };
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DocumentCard component
|
* DocumentCard component
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
@@ -57,6 +113,9 @@ const buildFullUrl = (relativePath) => {
|
|||||||
const DocumentCard = memo(({document, viewMode, onEdit, onDelete}) => {
|
const DocumentCard = memo(({document, viewMode, onEdit, onDelete}) => {
|
||||||
const {name, originalFileType, thumbnailUrl, pageCount, fileSize, createdAt, tags, categories} = document;
|
const {name, originalFileType, thumbnailUrl, pageCount, fileSize, createdAt, tags, categories} = document;
|
||||||
|
|
||||||
|
// Load protected image
|
||||||
|
const { imageSrc, loading, error } = useProtectedImage(thumbnailUrl);
|
||||||
|
|
||||||
// Determine card classes based on view mode
|
// Determine card classes based on view mode
|
||||||
const getCardClasses = () => {
|
const getCardClasses = () => {
|
||||||
const baseClasses = 'card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow group relative';
|
const baseClasses = 'card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow group relative';
|
||||||
@@ -74,51 +133,67 @@ const DocumentCard = memo(({document, viewMode, onEdit, onDelete}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Render thumbnail with hover actions
|
// Render thumbnail with hover actions
|
||||||
const renderThumbnail = () => (
|
const renderThumbnail = () => {
|
||||||
<figure className="relative overflow-hidden">
|
const heightClass = viewMode === 'small' ? 'h-48' : viewMode === 'large' ? 'h-64' : 'h-64';
|
||||||
<img
|
|
||||||
src={buildFullUrl(thumbnailUrl)}
|
|
||||||
alt={`${name}`}
|
|
||||||
className={`w-[200px] object-cover ${
|
|
||||||
viewMode === 'small' ? 'h-32' : viewMode === 'large' ? 'h-48' : 'h-64'
|
|
||||||
}`}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Hover overlay with actions */}
|
return (
|
||||||
<div className="absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
<figure className="relative overflow-hidden">
|
||||||
<button
|
{loading ? (
|
||||||
className="btn btn-sm btn-circle btn-primary"
|
<div className={`w-[200px] ${heightClass} bg-gray-200 animate-pulse flex items-center justify-center`}>
|
||||||
onClick={onEdit}
|
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
aria-label="Edit document"
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
title="Edit"
|
</svg>
|
||||||
>
|
</div>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
) : error ? (
|
||||||
stroke="currentColor">
|
<div className={`w-[200px] ${heightClass} bg-gray-300 flex flex-col items-center justify-center`}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
<svg className="w-8 h-8 text-gray-500 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
<span className="text-gray-500 text-xs">Failed to load</span>
|
||||||
<button
|
</div>
|
||||||
className="btn btn-sm btn-circle btn-error"
|
) : (
|
||||||
onClick={onDelete}
|
<img
|
||||||
aria-label="Delete document"
|
src={imageSrc}
|
||||||
title="Delete"
|
alt={`${name}`}
|
||||||
>
|
className={`object-cover ${heightClass}`}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
/>
|
||||||
stroke="currentColor">
|
)}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* File type badge */}
|
{/* Hover overlay with actions */}
|
||||||
<div className="absolute bottom-2 left-2">
|
<div className="absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<span className="badge badge-accent badge-sm">{originalFileType}</span>
|
<button
|
||||||
</div>
|
className="btn btn-sm btn-circle btn-primary"
|
||||||
</figure>
|
onClick={onEdit}
|
||||||
);
|
aria-label="Edit document"
|
||||||
|
title="Edit"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-circle btn-error"
|
||||||
|
onClick={onDelete}
|
||||||
|
aria-label="Delete document"
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File type badge */}
|
||||||
|
<div className="absolute bottom-2 left-2">
|
||||||
|
<span className="badge badge-accent badge-sm">{originalFileType}</span>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Render card body based on view mode
|
// Render card body based on view mode
|
||||||
const renderCardBody = () => {
|
const renderCardBody = () => {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const DocumentGallery = () => {
|
|||||||
// Loading state
|
// Loading state
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-[400px]">
|
<div className="flex justify-center items-center min-h-[400px] ">
|
||||||
<span className="loading loading-spinner loading-lg"></span>
|
<span className="loading loading-spinner loading-lg"></span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -122,11 +122,10 @@ const DocumentGallery = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="h-full flex flex-col">
|
||||||
{/* Header with view mode switcher */}
|
{/* Header with view mode switcher - Always visible */}
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6 flex-shrink-0">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold">Documents</h2>
|
|
||||||
<p className="text-gray-500">{documents.length} document{documents.length !== 1 ? 's' : ''}</p>
|
<p className="text-gray-500">{documents.length} document{documents.length !== 1 ? 's' : ''}</p>
|
||||||
</div>
|
</div>
|
||||||
<ViewModeSwitcher
|
<ViewModeSwitcher
|
||||||
@@ -135,26 +134,28 @@ const DocumentGallery = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Document grid/list */}
|
{/* Document grid/list - Scrollable */}
|
||||||
<div className={getGridClasses()}>
|
<div className="flex-1 overflow-y-auto">
|
||||||
{documents.map(document => (
|
<div className={getGridClasses()}>
|
||||||
viewMode === 'detail' ? (
|
{documents.map(document => (
|
||||||
<DocumentDetailView
|
viewMode === 'detail' ? (
|
||||||
key={document.id}
|
<DocumentDetailView
|
||||||
document={document}
|
key={document.id}
|
||||||
onEdit={() => handleEditClick(document)}
|
document={document}
|
||||||
onDelete={() => handleDeleteClick(document)}
|
onEdit={() => handleEditClick(document)}
|
||||||
/>
|
onDelete={() => handleDeleteClick(document)}
|
||||||
) : (
|
/>
|
||||||
<DocumentCard
|
) : (
|
||||||
key={document.id}
|
<DocumentCard
|
||||||
document={document}
|
key={document.id}
|
||||||
viewMode={viewMode}
|
document={document}
|
||||||
onEdit={() => handleEditClick(document)}
|
viewMode={viewMode}
|
||||||
onDelete={() => handleDeleteClick(document)}
|
onEdit={() => handleEditClick(document)}
|
||||||
/>
|
onDelete={() => handleDeleteClick(document)}
|
||||||
)
|
/>
|
||||||
))}
|
)
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {FaList} from "react-icons/fa6";
|
||||||
|
import {FaTh, FaThLarge} from "react-icons/fa";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {'small' | 'large' | 'detail'} ViewMode
|
* @typedef {'small' | 'large' | 'detail'} ViewMode
|
||||||
@@ -18,27 +20,30 @@ import React from 'react';
|
|||||||
*/
|
*/
|
||||||
const ViewModeSwitcher = ({ currentMode, onModeChange }) => {
|
const ViewModeSwitcher = ({ currentMode, onModeChange }) => {
|
||||||
const modes = [
|
const modes = [
|
||||||
{ id: 'small', label: 'Small', icon: '⊞' },
|
{ id: 'small', label: 'Small', icon: FaTh },
|
||||||
{ id: 'large', label: 'Large', icon: '⊡' },
|
{ id: 'large', label: 'Large', icon: FaThLarge },
|
||||||
{ id: 'detail', label: 'Detail', icon: '☰' }
|
{ id: 'detail', label: 'Detail', icon: FaList }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{modes.map(mode => (
|
{modes.map(mode => {
|
||||||
<button
|
const IconComponent = mode.icon;
|
||||||
key={mode.id}
|
return (
|
||||||
onClick={() => onModeChange(mode.id)}
|
<button
|
||||||
className={`btn btn-sm ${
|
key={mode.id}
|
||||||
currentMode === mode.id ? 'btn-primary' : 'btn-ghost'
|
onClick={() => onModeChange(mode.id)}
|
||||||
}`}
|
className={`btn btn-sm ${
|
||||||
aria-label={`Switch to ${mode.label} view`}
|
currentMode === mode.id ? 'btn-primary' : 'btn-ghost'
|
||||||
title={`${mode.label} view`}
|
}`}
|
||||||
>
|
aria-label={`Switch to ${mode.label} view`}
|
||||||
<span className="text-lg">{mode.icon}</span>
|
title={`${mode.label} view`}
|
||||||
<span className="hidden sm:inline ml-1">{mode.label}</span>
|
>
|
||||||
</button>
|
<IconComponent />
|
||||||
))}
|
<span className="hidden sm:inline ml-1">{mode.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import DocumentGallery from '../components/documents/DocumentGallery';
|
|||||||
*/
|
*/
|
||||||
const DocumentsPage = () => {
|
const DocumentsPage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="h-full flex flex-col">
|
||||||
<DocumentGallery />
|
<DocumentGallery />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
739
tests/services/test_user_service.py
Normal file
739
tests/services/test_user_service.py
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for UserService using in-memory MongoDB.
|
||||||
|
|
||||||
|
Tests the business logic operations with real MongoDB operations
|
||||||
|
using mongomock for better integration testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from bson import ObjectId
|
||||||
|
from mongomock.mongo_client import MongoClient
|
||||||
|
|
||||||
|
from app.models.auth import UserRole
|
||||||
|
from app.models.user import UserCreate, UserUpdate, UserCreateNoValidation
|
||||||
|
from app.services.user_service import UserService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def in_memory_database():
|
||||||
|
"""Create an in-memory database for testing."""
|
||||||
|
client = MongoClient()
|
||||||
|
return client.test_database
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user_service(in_memory_database):
|
||||||
|
"""Create UserService with in-memory repositories."""
|
||||||
|
service = UserService(in_memory_database).initialize()
|
||||||
|
return service
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_user_data():
|
||||||
|
"""Sample user data for testing."""
|
||||||
|
return {
|
||||||
|
"username": "testuser",
|
||||||
|
"email": "testuser@example.com",
|
||||||
|
"password": "SecureP@ssw0rd123"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_user_data_2():
|
||||||
|
"""Second sample user data for testing."""
|
||||||
|
return {
|
||||||
|
"username": "anotheruser",
|
||||||
|
"email": "anotheruser@example.com",
|
||||||
|
"password": "AnotherP@ssw0rd456"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateUser:
|
||||||
|
"""Tests for create_user method."""
|
||||||
|
|
||||||
|
def test_i_can_create_user_with_valid_data(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test creating user with valid data."""
|
||||||
|
# Execute
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
result = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Verify user creation
|
||||||
|
assert result is not None
|
||||||
|
assert result.username == sample_user_data["username"]
|
||||||
|
assert result.email == sample_user_data["email"]
|
||||||
|
assert result.hashed_password is not None
|
||||||
|
assert result.hashed_password != sample_user_data["password"]
|
||||||
|
assert result.role == UserRole.USER
|
||||||
|
assert result.is_active is True
|
||||||
|
assert result.preferences == {}
|
||||||
|
assert result.created_at is not None
|
||||||
|
assert result.updated_at is not None
|
||||||
|
|
||||||
|
# Verify user exists in database
|
||||||
|
user_in_db = user_service.get_user_by_id(str(result.id))
|
||||||
|
assert user_in_db is not None
|
||||||
|
assert user_in_db.id == result.id
|
||||||
|
assert user_in_db.username == sample_user_data["username"]
|
||||||
|
|
||||||
|
def test_i_cannot_create_user_with_duplicate_username(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test that duplicate username raises ValueError."""
|
||||||
|
# Create first user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Try to create user with same username but different email
|
||||||
|
duplicate_user_data = sample_user_data.copy()
|
||||||
|
duplicate_user_data["email"] = "different@example.com"
|
||||||
|
duplicate_user_create = UserCreate(**duplicate_user_data)
|
||||||
|
|
||||||
|
# Execute and verify exception
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
user_service.create_user(duplicate_user_create)
|
||||||
|
|
||||||
|
assert "already exists" in str(exc_info.value)
|
||||||
|
assert sample_user_data["username"] in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_i_cannot_create_user_with_duplicate_email(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test that duplicate email raises ValueError."""
|
||||||
|
# Create first user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Try to create user with same email but different username
|
||||||
|
duplicate_user_data = sample_user_data.copy()
|
||||||
|
duplicate_user_data["username"] = "differentuser"
|
||||||
|
duplicate_user_create = UserCreate(**duplicate_user_data)
|
||||||
|
|
||||||
|
# Execute and verify exception
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
user_service.create_user(duplicate_user_create)
|
||||||
|
|
||||||
|
assert "already exists" in str(exc_info.value)
|
||||||
|
assert sample_user_data["email"] in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetUserMethods:
|
||||||
|
"""Tests for user retrieval methods."""
|
||||||
|
|
||||||
|
def test_i_can_get_user_by_username(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test retrieving user by username."""
|
||||||
|
# Create a user first
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = user_service.get_user_by_username(sample_user_data["username"])
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is not None
|
||||||
|
assert result.id == created_user.id
|
||||||
|
assert result.username == sample_user_data["username"]
|
||||||
|
assert result.email == sample_user_data["email"]
|
||||||
|
|
||||||
|
def test_i_can_get_user_by_id(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test retrieving user by ID."""
|
||||||
|
# Create a user first
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = user_service.get_user_by_id(str(created_user.id))
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is not None
|
||||||
|
assert result.id == created_user.id
|
||||||
|
assert result.username == sample_user_data["username"]
|
||||||
|
assert result.email == sample_user_data["email"]
|
||||||
|
|
||||||
|
def test_i_can_check_user_exists(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test checking if user exists."""
|
||||||
|
# Initially should not exist
|
||||||
|
assert user_service.user_exists(sample_user_data["username"]) is False
|
||||||
|
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Now should exist
|
||||||
|
assert user_service.user_exists(sample_user_data["username"]) is True
|
||||||
|
|
||||||
|
def test_i_cannot_get_nonexistent_user_by_username(
|
||||||
|
self,
|
||||||
|
user_service
|
||||||
|
):
|
||||||
|
"""Test retrieving nonexistent user by username returns None."""
|
||||||
|
# Execute
|
||||||
|
result = user_service.get_user_by_username("nonexistentuser")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_i_cannot_get_nonexistent_user_by_id(
|
||||||
|
self,
|
||||||
|
user_service
|
||||||
|
):
|
||||||
|
"""Test retrieving nonexistent user by ID returns None."""
|
||||||
|
# Execute with random ObjectId
|
||||||
|
result = user_service.get_user_by_id(str(ObjectId()))
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthenticateUser:
|
||||||
|
"""Tests for authenticate_user method."""
|
||||||
|
|
||||||
|
def test_i_can_authenticate_user_with_valid_credentials(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test authenticating user with valid credentials."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute authentication
|
||||||
|
result = user_service.authenticate_user(
|
||||||
|
sample_user_data["username"],
|
||||||
|
sample_user_data["password"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is not None
|
||||||
|
assert result.id == created_user.id
|
||||||
|
assert result.username == sample_user_data["username"]
|
||||||
|
|
||||||
|
def test_i_cannot_authenticate_user_with_wrong_password(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test authenticating user with wrong password returns None."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute authentication with wrong password
|
||||||
|
result = user_service.authenticate_user(
|
||||||
|
sample_user_data["username"],
|
||||||
|
"WrongP@ssw0rd123"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_i_cannot_authenticate_user_with_wrong_username(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test authenticating user with wrong username returns None."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute authentication with wrong username
|
||||||
|
result = user_service.authenticate_user(
|
||||||
|
"wrongusername",
|
||||||
|
sample_user_data["password"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_i_cannot_authenticate_inactive_user(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test authenticating inactive user returns None."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Deactivate the user
|
||||||
|
user_service.update_user(str(created_user.id), UserUpdate(is_active=False))
|
||||||
|
|
||||||
|
# Execute authentication
|
||||||
|
result = user_service.authenticate_user(
|
||||||
|
sample_user_data["username"],
|
||||||
|
sample_user_data["password"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdateUser:
|
||||||
|
"""Tests for update_user method."""
|
||||||
|
|
||||||
|
def test_i_can_update_user_username(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test updating user username."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute update
|
||||||
|
new_username = "updatedusername"
|
||||||
|
result = user_service.update_user(
|
||||||
|
str(created_user.id),
|
||||||
|
UserUpdate(username=new_username)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is not None
|
||||||
|
assert result.username == new_username
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
updated_user = user_service.get_user_by_id(str(created_user.id))
|
||||||
|
assert updated_user.username == new_username
|
||||||
|
|
||||||
|
def test_i_can_update_user_email(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test updating user email."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute update
|
||||||
|
new_email = "newemail@example.com"
|
||||||
|
result = user_service.update_user(
|
||||||
|
str(created_user.id),
|
||||||
|
UserUpdate(email=new_email)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is not None
|
||||||
|
assert result.email == new_email
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
updated_user = user_service.get_user_by_id(str(created_user.id))
|
||||||
|
assert updated_user.email == new_email
|
||||||
|
|
||||||
|
def test_i_can_update_user_role(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test updating user role."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute update
|
||||||
|
result = user_service.update_user(
|
||||||
|
str(created_user.id),
|
||||||
|
UserUpdate(role=UserRole.ADMIN)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is not None
|
||||||
|
assert result.role == UserRole.ADMIN
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
updated_user = user_service.get_user_by_id(str(created_user.id))
|
||||||
|
assert updated_user.role == UserRole.ADMIN
|
||||||
|
|
||||||
|
def test_i_can_update_user_is_active(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test updating user is_active status."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute update
|
||||||
|
result = user_service.update_user(
|
||||||
|
str(created_user.id),
|
||||||
|
UserUpdate(is_active=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is not None
|
||||||
|
assert result.is_active is False
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
updated_user = user_service.get_user_by_id(str(created_user.id))
|
||||||
|
assert updated_user.is_active is False
|
||||||
|
|
||||||
|
def test_i_cannot_update_user_with_duplicate_username(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data,
|
||||||
|
sample_user_data_2
|
||||||
|
):
|
||||||
|
"""Test that updating to existing username raises ValueError."""
|
||||||
|
# Create two users
|
||||||
|
user_create_1 = UserCreate(**sample_user_data)
|
||||||
|
user_1 = user_service.create_user(user_create_1)
|
||||||
|
|
||||||
|
user_create_2 = UserCreate(**sample_user_data_2)
|
||||||
|
user_2 = user_service.create_user(user_create_2)
|
||||||
|
|
||||||
|
# Try to update user_2 with user_1's username
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
user_service.update_user(
|
||||||
|
str(user_2.id),
|
||||||
|
UserUpdate(username=sample_user_data["username"])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "already taken" in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_i_cannot_update_user_with_duplicate_email(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data,
|
||||||
|
sample_user_data_2
|
||||||
|
):
|
||||||
|
"""Test that updating to existing email raises ValueError."""
|
||||||
|
# Create two users
|
||||||
|
user_create_1 = UserCreate(**sample_user_data)
|
||||||
|
user_1 = user_service.create_user(user_create_1)
|
||||||
|
|
||||||
|
user_create_2 = UserCreate(**sample_user_data_2)
|
||||||
|
user_2 = user_service.create_user(user_create_2)
|
||||||
|
|
||||||
|
# Try to update user_2 with user_1's email
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
user_service.update_user(
|
||||||
|
str(user_2.id),
|
||||||
|
UserUpdate(email=sample_user_data["email"])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "already taken" in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_i_cannot_update_nonexistent_user(
|
||||||
|
self,
|
||||||
|
user_service
|
||||||
|
):
|
||||||
|
"""Test updating nonexistent user returns None."""
|
||||||
|
# Execute update with random ObjectId
|
||||||
|
result = user_service.update_user(
|
||||||
|
str(ObjectId()),
|
||||||
|
UserUpdate(username="newusername")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeleteUser:
|
||||||
|
"""Tests for delete_user method."""
|
||||||
|
|
||||||
|
def test_i_can_delete_existing_user(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test deleting an existing user."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Verify user exists
|
||||||
|
user_before_delete = user_service.get_user_by_id(str(created_user.id))
|
||||||
|
assert user_before_delete is not None
|
||||||
|
|
||||||
|
# Execute deletion
|
||||||
|
result = user_service.delete_user(str(created_user.id))
|
||||||
|
|
||||||
|
# Verify deletion
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
# Verify user no longer exists
|
||||||
|
deleted_user = user_service.get_user_by_id(str(created_user.id))
|
||||||
|
assert deleted_user is None
|
||||||
|
|
||||||
|
def test_i_cannot_delete_nonexistent_user(
|
||||||
|
self,
|
||||||
|
user_service
|
||||||
|
):
|
||||||
|
"""Test deleting a nonexistent user returns False."""
|
||||||
|
# Execute deletion with random ObjectId
|
||||||
|
result = user_service.delete_user(str(ObjectId()))
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestListAndCountMethods:
|
||||||
|
"""Tests for list_users and count_users methods."""
|
||||||
|
|
||||||
|
def test_i_can_list_users(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data,
|
||||||
|
sample_user_data_2
|
||||||
|
):
|
||||||
|
"""Test listing all users."""
|
||||||
|
# Create multiple users
|
||||||
|
user_create_1 = UserCreate(**sample_user_data)
|
||||||
|
user_1 = user_service.create_user(user_create_1)
|
||||||
|
|
||||||
|
user_create_2 = UserCreate(**sample_user_data_2)
|
||||||
|
user_2 = user_service.create_user(user_create_2)
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = user_service.list_users()
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert len(result) == 2
|
||||||
|
usernames = [user.username for user in result]
|
||||||
|
assert sample_user_data["username"] in usernames
|
||||||
|
assert sample_user_data_2["username"] in usernames
|
||||||
|
|
||||||
|
def test_i_can_list_users_with_pagination(
|
||||||
|
self,
|
||||||
|
user_service
|
||||||
|
):
|
||||||
|
"""Test listing users with pagination."""
|
||||||
|
# Create 5 users
|
||||||
|
for i in range(5):
|
||||||
|
user_data = UserCreateNoValidation(
|
||||||
|
username=f"user{i}",
|
||||||
|
email=f"user{i}@example.com",
|
||||||
|
password="SecureP@ssw0rd123"
|
||||||
|
)
|
||||||
|
user_service.create_user(user_data)
|
||||||
|
|
||||||
|
# Test skip and limit
|
||||||
|
result_page_1 = user_service.list_users(skip=0, limit=2)
|
||||||
|
assert len(result_page_1) == 2
|
||||||
|
|
||||||
|
result_page_2 = user_service.list_users(skip=2, limit=2)
|
||||||
|
assert len(result_page_2) == 2
|
||||||
|
|
||||||
|
result_page_3 = user_service.list_users(skip=4, limit=2)
|
||||||
|
assert len(result_page_3) == 1
|
||||||
|
|
||||||
|
# Verify different users in each page
|
||||||
|
page_1_usernames = [user.username for user in result_page_1]
|
||||||
|
page_2_usernames = [user.username for user in result_page_2]
|
||||||
|
assert page_1_usernames != page_2_usernames
|
||||||
|
|
||||||
|
def test_i_can_count_users(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data,
|
||||||
|
sample_user_data_2
|
||||||
|
):
|
||||||
|
"""Test counting users."""
|
||||||
|
# Initially no users
|
||||||
|
assert user_service.count_users() == 0
|
||||||
|
|
||||||
|
# Create first user
|
||||||
|
user_create_1 = UserCreate(**sample_user_data)
|
||||||
|
user_service.create_user(user_create_1)
|
||||||
|
assert user_service.count_users() == 1
|
||||||
|
|
||||||
|
# Create second user
|
||||||
|
user_create_2 = UserCreate(**sample_user_data_2)
|
||||||
|
user_service.create_user(user_create_2)
|
||||||
|
assert user_service.count_users() == 2
|
||||||
|
|
||||||
|
def test_list_users_returns_empty_list_when_no_users(
|
||||||
|
self,
|
||||||
|
user_service
|
||||||
|
):
|
||||||
|
"""Test listing users returns empty list when no users exist."""
|
||||||
|
# Execute
|
||||||
|
result = user_service.list_users()
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserPreferences:
|
||||||
|
"""Tests for user preferences methods."""
|
||||||
|
|
||||||
|
def test_i_can_get_user_preference(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test getting user preference."""
|
||||||
|
# Create a user with preferences
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Set a preference
|
||||||
|
user_service.set_preference(str(created_user.id), "theme", "dark")
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = user_service.get_preference(str(created_user.id), "theme")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result == "dark"
|
||||||
|
|
||||||
|
def test_i_can_set_user_preference(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test setting user preference."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = user_service.set_preference(str(created_user.id), "language", "fr")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is not None
|
||||||
|
assert result.preferences.get("language") == "fr"
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
updated_user = user_service.get_user_by_id(str(created_user.id))
|
||||||
|
assert updated_user.preferences.get("language") == "fr"
|
||||||
|
|
||||||
|
def test_i_cannot_get_preference_for_nonexistent_user(
|
||||||
|
self,
|
||||||
|
user_service
|
||||||
|
):
|
||||||
|
"""Test getting preference for nonexistent user returns None."""
|
||||||
|
# Execute with random ObjectId
|
||||||
|
result = user_service.get_preference(str(ObjectId()), "theme")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_i_cannot_set_preference_for_nonexistent_user(
|
||||||
|
self,
|
||||||
|
user_service
|
||||||
|
):
|
||||||
|
"""Test setting preference for nonexistent user returns None."""
|
||||||
|
# Execute with random ObjectId
|
||||||
|
result = user_service.set_preference(str(ObjectId()), "theme", "dark")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_get_preference_returns_none_for_nonexistent_key(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test getting nonexistent preference key returns None."""
|
||||||
|
# Create a user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = user_service.get_preference(str(created_user.id), "nonexistent_key")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserLifecycle:
|
||||||
|
"""Tests for complete user lifecycle scenarios."""
|
||||||
|
|
||||||
|
def test_complete_user_lifecycle(
|
||||||
|
self,
|
||||||
|
user_service,
|
||||||
|
sample_user_data
|
||||||
|
):
|
||||||
|
"""Test complete user lifecycle: create → authenticate → update → preferences → delete."""
|
||||||
|
# Create user
|
||||||
|
user_create = UserCreate(**sample_user_data)
|
||||||
|
created_user = user_service.create_user(user_create)
|
||||||
|
assert created_user is not None
|
||||||
|
assert created_user.username == sample_user_data["username"]
|
||||||
|
|
||||||
|
# Authenticate user
|
||||||
|
authenticated_user = user_service.authenticate_user(
|
||||||
|
sample_user_data["username"],
|
||||||
|
sample_user_data["password"]
|
||||||
|
)
|
||||||
|
assert authenticated_user is not None
|
||||||
|
assert authenticated_user.id == created_user.id
|
||||||
|
|
||||||
|
# Update user
|
||||||
|
updated_user = user_service.update_user(
|
||||||
|
str(created_user.id),
|
||||||
|
UserUpdate(role=UserRole.ADMIN)
|
||||||
|
)
|
||||||
|
assert updated_user.role == UserRole.ADMIN
|
||||||
|
|
||||||
|
# Set preference
|
||||||
|
user_with_pref = user_service.set_preference(
|
||||||
|
str(created_user.id),
|
||||||
|
"theme",
|
||||||
|
"dark"
|
||||||
|
)
|
||||||
|
assert user_with_pref.preferences.get("theme") == "dark"
|
||||||
|
|
||||||
|
# Get preference
|
||||||
|
pref_value = user_service.get_preference(str(created_user.id), "theme")
|
||||||
|
assert pref_value == "dark"
|
||||||
|
|
||||||
|
# Delete user
|
||||||
|
delete_result = user_service.delete_user(str(created_user.id))
|
||||||
|
assert delete_result is True
|
||||||
|
|
||||||
|
# Verify user no longer exists
|
||||||
|
deleted_user = user_service.get_user_by_id(str(created_user.id))
|
||||||
|
assert deleted_user is None
|
||||||
|
|
||||||
|
def test_user_operations_with_empty_database(
|
||||||
|
self,
|
||||||
|
user_service
|
||||||
|
):
|
||||||
|
"""Test user operations when database is empty."""
|
||||||
|
# Try to get nonexistent user
|
||||||
|
result = user_service.get_user_by_id(str(ObjectId()))
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
# Try to get user by username
|
||||||
|
result = user_service.get_user_by_username("nonexistent")
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
# Try to list users
|
||||||
|
users = user_service.list_users()
|
||||||
|
assert users == []
|
||||||
|
|
||||||
|
# Try to count users
|
||||||
|
count = user_service.count_users()
|
||||||
|
assert count == 0
|
||||||
|
|
||||||
|
# Try to delete nonexistent user
|
||||||
|
delete_result = user_service.delete_user(str(ObjectId()))
|
||||||
|
assert delete_result is False
|
||||||
|
|
||||||
|
# Try to check user existence
|
||||||
|
exists = user_service.user_exists("nonexistent")
|
||||||
|
assert exists is False
|
||||||
Reference in New Issue
Block a user