feat: implement Instagram clone SocialPhoto API
- FastAPI backend with SQLite database - JWT authentication (register, login) - User profiles with follow/unfollow - Posts with image upload and likes/dislikes - Comments with likes - Global and personalized feed - Comprehensive pytest test suite (37 tests) TASK-ID: 758f4029-702
This commit is contained in:
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests package for SocialPhoto."""
|
||||
129
tests/conftest.py
Normal file
129
tests/conftest.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Pytest fixtures for SocialPhoto tests."""
|
||||
import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
# Set test database path before importing app
|
||||
TEST_DB = Path(__file__).parent / "test_socialphoto.db"
|
||||
|
||||
|
||||
def override_db_path():
|
||||
"""Override database path for tests."""
|
||||
return TEST_DB
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db() -> Generator[sqlite3.Connection, None, None]:
|
||||
"""Create a fresh database for each test."""
|
||||
# Remove existing test database
|
||||
if TEST_DB.exists():
|
||||
TEST_DB.unlink()
|
||||
|
||||
# Patch the database path before importing
|
||||
import app.database as db_module
|
||||
original_path = db_module.DATABASE_PATH
|
||||
db_module.DATABASE_PATH = TEST_DB
|
||||
|
||||
# Initialize database with new path
|
||||
db_module.init_db()
|
||||
|
||||
conn = db_module.get_db_connection()
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
yield conn
|
||||
|
||||
conn.close()
|
||||
db_module.DATABASE_PATH = original_path
|
||||
if TEST_DB.exists():
|
||||
TEST_DB.unlink()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def client(db: sqlite3.Connection) -> Generator[TestClient, None, None]:
|
||||
"""Create a test client."""
|
||||
from app.main import app
|
||||
from app.database import get_db
|
||||
|
||||
# Override database dependency
|
||||
def override_get_db():
|
||||
yield db
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
with TestClient(app) as test_client:
|
||||
yield test_client
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def get_user_id_from_token(token: str) -> int:
|
||||
"""Extract user ID from JWT token (for test fixtures only)."""
|
||||
from jose import jwt
|
||||
from app.auth import SECRET_KEY, ALGORITHM
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
return int(payload["sub"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registered_user(client: TestClient) -> dict:
|
||||
"""Register a test user and return user data with token."""
|
||||
response = client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "testpass123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
token = data["access_token"]
|
||||
user_id = get_user_id_from_token(token)
|
||||
return {
|
||||
"id": user_id,
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "testpass123",
|
||||
"token": token,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_users(client: TestClient) -> dict:
|
||||
"""Create two test users."""
|
||||
# User 1
|
||||
response = client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "user1",
|
||||
"email": "user1@example.com",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
token1 = response.json()["access_token"]
|
||||
|
||||
# User 2
|
||||
response = client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "user2",
|
||||
"email": "user2@example.com",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
token2 = response.json()["access_token"]
|
||||
|
||||
return {
|
||||
"user1_id": get_user_id_from_token(token1),
|
||||
"user2_id": get_user_id_from_token(token2),
|
||||
"user1_token": token1,
|
||||
"user2_token": token2,
|
||||
"user1_username": "user1",
|
||||
"user2_username": "user2",
|
||||
}
|
||||
158
tests/test_auth.py
Normal file
158
tests/test_auth.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Tests for authentication routes."""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_register_success(client: TestClient):
|
||||
"""Test successful user registration."""
|
||||
response = client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "newuser",
|
||||
"email": "newuser@example.com",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert data["token_type"] == "bearer"
|
||||
|
||||
|
||||
def test_register_duplicate_username(client: TestClient):
|
||||
"""Test registration with duplicate username."""
|
||||
# First registration
|
||||
client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "duplicate",
|
||||
"email": "first@example.com",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
|
||||
# Second registration with same username
|
||||
response = client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "duplicate",
|
||||
"email": "second@example.com",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "Username already registered" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_register_duplicate_email(client: TestClient):
|
||||
"""Test registration with duplicate email."""
|
||||
# First registration
|
||||
client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "firstuser",
|
||||
"email": "same@example.com",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
|
||||
# Second registration with same email
|
||||
response = client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "seconduser",
|
||||
"email": "same@example.com",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "Email already registered" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_register_invalid_email(client: TestClient):
|
||||
"""Test registration with invalid email format."""
|
||||
response = client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "validuser",
|
||||
"email": "not-an-email",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
|
||||
def test_register_short_password(client: TestClient):
|
||||
"""Test registration with too short password."""
|
||||
response = client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "validuser",
|
||||
"email": "valid@example.com",
|
||||
"password": "12345",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
|
||||
def test_login_success(client: TestClient):
|
||||
"""Test successful login."""
|
||||
# Register first
|
||||
client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "loginuser",
|
||||
"email": "login@example.com",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
|
||||
# Login
|
||||
response = client.post(
|
||||
"/auth/login",
|
||||
json={
|
||||
"username": "loginuser",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert data["token_type"] == "bearer"
|
||||
|
||||
|
||||
def test_login_wrong_password(client: TestClient):
|
||||
"""Test login with wrong password."""
|
||||
# Register first
|
||||
client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "correctpassword",
|
||||
},
|
||||
)
|
||||
|
||||
# Login with wrong password
|
||||
response = client.post(
|
||||
"/auth/login",
|
||||
json={
|
||||
"username": "testuser",
|
||||
"password": "wrongpassword",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert "Incorrect username or password" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_login_nonexistent_user(client: TestClient):
|
||||
"""Test login with nonexistent username."""
|
||||
response = client.post(
|
||||
"/auth/login",
|
||||
json={
|
||||
"username": "nonexistent",
|
||||
"password": "password123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert "Incorrect username or password" in response.json()["detail"]
|
||||
292
tests/test_posts.py
Normal file
292
tests/test_posts.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""Tests for post routes."""
|
||||
import io
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_create_post_success(client: TestClient, two_users: dict):
|
||||
"""Test successful post creation."""
|
||||
token = two_users["user1_token"]
|
||||
|
||||
# Create a fake image
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
|
||||
response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "My first post!"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["caption"] == "My first post!"
|
||||
assert data["user_id"] is not None
|
||||
assert data["likes_count"] == 0
|
||||
assert data["dislikes_count"] == 0
|
||||
assert data["comments_count"] == 0
|
||||
|
||||
|
||||
def test_create_post_unauthorized(client: TestClient):
|
||||
"""Test creating post without authentication."""
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
|
||||
response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "My first post!"},
|
||||
)
|
||||
|
||||
assert response.status_code == 401 # No credentials
|
||||
|
||||
|
||||
def test_create_post_invalid_file_type(client: TestClient, two_users: dict):
|
||||
"""Test creating post with invalid file type."""
|
||||
token = two_users["user1_token"]
|
||||
|
||||
fake_file = io.BytesIO(b"fake content")
|
||||
fake_file.name = "test.txt"
|
||||
|
||||
response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.txt", fake_file, "text/plain")},
|
||||
data={"caption": "Invalid file"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "File type not allowed" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_get_posts_empty(client: TestClient):
|
||||
"""Test getting posts when database is empty."""
|
||||
response = client.get("/posts")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
def test_get_posts(client: TestClient, two_users: dict):
|
||||
"""Test getting posts."""
|
||||
token = two_users["user1_token"]
|
||||
|
||||
# Create a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Test post"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
|
||||
response = client.get("/posts")
|
||||
assert response.status_code == 200
|
||||
posts = response.json()
|
||||
assert len(posts) == 1
|
||||
assert posts[0]["caption"] == "Test post"
|
||||
|
||||
|
||||
def test_get_single_post(client: TestClient, two_users: dict):
|
||||
"""Test getting a single post."""
|
||||
token = two_users["user1_token"]
|
||||
|
||||
# Create a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Single post test"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
response = client.get(f"/posts/{post_id}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["caption"] == "Single post test"
|
||||
|
||||
|
||||
def test_get_nonexistent_post(client: TestClient):
|
||||
"""Test getting a post that doesn't exist."""
|
||||
response = client.get("/posts/9999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_delete_post_by_owner(client: TestClient, two_users: dict):
|
||||
"""Test deleting own post."""
|
||||
token = two_users["user1_token"]
|
||||
|
||||
# Create a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "To be deleted"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
# Delete the post
|
||||
response = client.delete(
|
||||
f"/posts/{post_id}",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
# Verify it's deleted
|
||||
response = client.get(f"/posts/{post_id}")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_delete_post_by_non_owner(client: TestClient, two_users: dict):
|
||||
"""Test trying to delete someone else's post."""
|
||||
token1 = two_users["user1_token"]
|
||||
token2 = two_users["user2_token"]
|
||||
|
||||
# User 1 creates a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "User 1's post"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
# User 2 tries to delete it
|
||||
response = client.delete(
|
||||
f"/posts/{post_id}",
|
||||
headers={"Authorization": f"Bearer {token2}"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert "You can only delete your own posts" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_like_post(client: TestClient, two_users: dict):
|
||||
"""Test liking a post."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# User 1 creates a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Like test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
# Like the post
|
||||
response = client.post(
|
||||
f"/posts/{post_id}/like",
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["message"] == "Post liked"
|
||||
|
||||
# Verify like count
|
||||
response = client.get(f"/posts/{post_id}")
|
||||
assert response.json()["likes_count"] == 1
|
||||
|
||||
|
||||
def test_unlike_post(client: TestClient, two_users: dict):
|
||||
"""Test removing a like from a post."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# User 1 creates a post and likes it
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Unlike test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
client.post(
|
||||
f"/posts/{post_id}/like",
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
|
||||
# Unlike the post
|
||||
response = client.delete(
|
||||
f"/posts/{post_id}/like",
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["message"] == "Like removed"
|
||||
|
||||
# Verify like count
|
||||
response = client.get(f"/posts/{post_id}")
|
||||
assert response.json()["likes_count"] == 0
|
||||
|
||||
|
||||
def test_dislike_post(client: TestClient, two_users: dict):
|
||||
"""Test disliking a post."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# User 1 creates a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Dislike test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
# Dislike the post
|
||||
response = client.post(
|
||||
f"/posts/{post_id}/dislike",
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["message"] == "Post disliked"
|
||||
|
||||
# Verify dislike count
|
||||
response = client.get(f"/posts/{post_id}")
|
||||
assert response.json()["dislikes_count"] == 1
|
||||
|
||||
|
||||
def test_like_and_dislike_mutually_exclusive(client: TestClient, two_users: dict):
|
||||
"""Test that like and dislike are mutually exclusive."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# User 1 creates a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Mutual exclusion test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
# Like the post
|
||||
client.post(
|
||||
f"/posts/{post_id}/like",
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
|
||||
# Dislike the post (should replace like)
|
||||
response = client.post(
|
||||
f"/posts/{post_id}/dislike",
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Verify: no likes, 1 dislike
|
||||
response = client.get(f"/posts/{post_id}")
|
||||
data = response.json()
|
||||
assert data["likes_count"] == 0
|
||||
assert data["dislikes_count"] == 1
|
||||
358
tests/test_social.py
Normal file
358
tests/test_social.py
Normal file
@@ -0,0 +1,358 @@
|
||||
"""Tests for social features: users, follows, comments."""
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_get_user_profile(client: TestClient, two_users: dict):
|
||||
"""Test getting a user profile."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# Get user ID from token (simplified - in real app would decode)
|
||||
# Let's create a post first to get the user
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Test post"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
|
||||
# For now, just test that user endpoint exists and works
|
||||
# The actual user ID would need to be extracted from the token
|
||||
response = client.get("/users/1")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["username"] == "user1"
|
||||
|
||||
|
||||
def test_get_user_stats(client: TestClient, two_users: dict):
|
||||
"""Test getting user statistics."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# Create a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Stats test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
|
||||
# User 2 follows User 1
|
||||
client.post(
|
||||
"/users/1/follow",
|
||||
headers={"Authorization": f"Bearer {two_users['user2_token']}"},
|
||||
)
|
||||
|
||||
response = client.get("/users/1/stats")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["posts_count"] == 1
|
||||
assert data["followers_count"] == 1
|
||||
assert data["following_count"] == 0
|
||||
|
||||
|
||||
def test_follow_user(client: TestClient, two_users: dict):
|
||||
"""Test following a user."""
|
||||
# User 2 follows User 1
|
||||
response = client.post(
|
||||
"/users/1/follow",
|
||||
headers={"Authorization": f"Bearer {two_users['user2_token']}"},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["message"] == "Successfully followed user"
|
||||
|
||||
|
||||
def test_follow_self(client: TestClient, two_users: dict):
|
||||
"""Test that you cannot follow yourself."""
|
||||
response = client.post(
|
||||
"/users/1/follow",
|
||||
headers={"Authorization": f"Bearer {two_users['user1_token']}"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "You cannot follow yourself" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_follow_already_following(client: TestClient, two_users: dict):
|
||||
"""Test following someone you already follow."""
|
||||
# User 2 follows User 1
|
||||
client.post(
|
||||
"/users/1/follow",
|
||||
headers={"Authorization": f"Bearer {two_users['user2_token']}"},
|
||||
)
|
||||
|
||||
# Try to follow again
|
||||
response = client.post(
|
||||
"/users/1/follow",
|
||||
headers={"Authorization": f"Bearer {two_users['user2_token']}"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "You already follow this user" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_unfollow_user(client: TestClient, two_users: dict):
|
||||
"""Test unfollowing a user."""
|
||||
token2 = two_users["user2_token"]
|
||||
|
||||
# User 2 follows User 1
|
||||
client.post(
|
||||
"/users/1/follow",
|
||||
headers={"Authorization": f"Bearer {token2}"},
|
||||
)
|
||||
|
||||
# User 2 unfollows User 1
|
||||
response = client.delete(
|
||||
"/users/1/follow",
|
||||
headers={"Authorization": f"Bearer {token2}"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["message"] == "Successfully unfollowed user"
|
||||
|
||||
|
||||
def test_unfollow_not_following(client: TestClient, two_users: dict):
|
||||
"""Test unfollowing someone you don't follow."""
|
||||
response = client.delete(
|
||||
"/users/1/follow",
|
||||
headers={"Authorization": f"Bearer {two_users['user2_token']}"},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert "You are not following this user" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_get_user_posts(client: TestClient, two_users: dict):
|
||||
"""Test getting posts by a specific user."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# Create two posts as User 1
|
||||
for i in range(2):
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": f"User 1 post {i}"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
|
||||
response = client.get("/users/1/posts")
|
||||
assert response.status_code == 200
|
||||
posts = response.json()
|
||||
assert len(posts) == 2
|
||||
|
||||
|
||||
def test_create_comment(client: TestClient, two_users: dict):
|
||||
"""Test creating a comment."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# User 1 creates a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Comment test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
# User 2 comments on the post
|
||||
response = client.post(
|
||||
f"/posts/{post_id}/comments",
|
||||
json={"content": "Great post!"},
|
||||
headers={"Authorization": f"Bearer {two_users['user2_token']}"},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["content"] == "Great post!"
|
||||
assert data["username"] == "user2"
|
||||
|
||||
|
||||
def test_get_post_comments(client: TestClient, two_users: dict):
|
||||
"""Test getting comments for a post."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# User 1 creates a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Comments test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
# Add a comment
|
||||
client.post(
|
||||
f"/posts/{post_id}/comments",
|
||||
json={"content": "First comment!"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
|
||||
response = client.get(f"/posts/{post_id}/comments")
|
||||
assert response.status_code == 200
|
||||
comments = response.json()
|
||||
assert len(comments) == 1
|
||||
assert comments[0]["content"] == "First comment!"
|
||||
|
||||
|
||||
def test_delete_comment_by_owner(client: TestClient, two_users: dict):
|
||||
"""Test deleting own comment."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# User 1 creates a post and comments
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Delete comment test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
comment_response = client.post(
|
||||
f"/posts/{post_id}/comments",
|
||||
json={"content": "To be deleted"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
comment_id = comment_response.json()["id"]
|
||||
|
||||
# Delete the comment
|
||||
response = client.delete(
|
||||
f"/comments/{comment_id}",
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_delete_comment_by_non_owner(client: TestClient, two_users: dict):
|
||||
"""Test trying to delete someone else's comment."""
|
||||
token1 = two_users["user1_token"]
|
||||
token2 = two_users["user2_token"]
|
||||
|
||||
# User 1 creates a post and comments
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Unauthorized delete test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
comment_response = client.post(
|
||||
f"/posts/{post_id}/comments",
|
||||
json={"content": "User 1's comment"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
comment_id = comment_response.json()["id"]
|
||||
|
||||
# User 2 tries to delete it
|
||||
response = client.delete(
|
||||
f"/comments/{comment_id}",
|
||||
headers={"Authorization": f"Bearer {token2}"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert "You can only delete your own comments" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_like_comment(client: TestClient, two_users: dict):
|
||||
"""Test liking a comment."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# User 1 creates a post and comments
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
create_response = client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Comment like test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
post_id = create_response.json()["id"]
|
||||
|
||||
comment_response = client.post(
|
||||
f"/posts/{post_id}/comments",
|
||||
json={"content": "Comment to like"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
comment_id = comment_response.json()["id"]
|
||||
|
||||
# Like the comment
|
||||
response = client.post(
|
||||
f"/comments/{comment_id}/like",
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["message"] == "Comment liked"
|
||||
|
||||
|
||||
def test_feed_global(client: TestClient, two_users: dict):
|
||||
"""Test global feed."""
|
||||
token1 = two_users["user1_token"]
|
||||
|
||||
# User 1 creates a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Global feed test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
|
||||
response = client.get("/feed/global")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] >= 1
|
||||
assert len(data["posts"]) >= 1
|
||||
|
||||
|
||||
def test_feed_followed(client: TestClient, two_users: dict):
|
||||
"""Test followed users feed."""
|
||||
token1 = two_users["user1_token"]
|
||||
token2 = two_users["user2_token"]
|
||||
|
||||
# User 1 creates a post
|
||||
fake_image = io.BytesIO(b"fake image content")
|
||||
fake_image.name = "test.jpg"
|
||||
client.post(
|
||||
"/posts",
|
||||
files={"image": ("test.jpg", fake_image, "image/jpeg")},
|
||||
data={"caption": "Followed feed test"},
|
||||
headers={"Authorization": f"Bearer {token1}"},
|
||||
)
|
||||
|
||||
# User 2 follows User 1
|
||||
client.post(
|
||||
"/users/1/follow",
|
||||
headers={"Authorization": f"Bearer {token2}"},
|
||||
)
|
||||
|
||||
# Get followed feed for User 2
|
||||
response = client.get(
|
||||
"/feed",
|
||||
headers={"Authorization": f"Bearer {token2}"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] >= 1
|
||||
assert len(data["posts"]) >= 1
|
||||
|
||||
|
||||
def test_feed_followed_no_follows(client: TestClient, two_users: dict):
|
||||
"""Test followed feed when not following anyone."""
|
||||
response = client.get(
|
||||
"/feed",
|
||||
headers={"Authorization": f"Bearer {two_users['user2_token']}"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 0
|
||||
assert len(data["posts"]) == 0
|
||||
Reference in New Issue
Block a user