This commit is contained in:
Danilo Reyes
2025-11-02 15:05:18 -06:00
parent 948fe591dc
commit c68a6a7d01
14 changed files with 1599 additions and 74 deletions

View File

@@ -0,0 +1,302 @@
"""Tests for board sharing endpoints."""
from datetime import datetime, timedelta
import pytest
from fastapi import status
def test_create_share_link_view_only(client, auth_headers, test_board):
"""Test creating a view-only share link."""
response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only"},
headers=auth_headers,
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["permission_level"] == "view-only"
assert data["board_id"] == str(test_board.id)
assert data["token"] is not None
assert len(data["token"]) == 64
assert data["is_revoked"] == False # noqa: E712
assert data["access_count"] == 0
def test_create_share_link_view_comment(client, auth_headers, test_board):
"""Test creating a view-comment share link."""
response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-comment"},
headers=auth_headers,
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["permission_level"] == "view-comment"
def test_create_share_link_with_expiration(client, auth_headers, test_board):
"""Test creating a share link with expiration."""
expires_at = (datetime.utcnow() + timedelta(days=7)).isoformat()
response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only", "expires_at": expires_at},
headers=auth_headers,
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["expires_at"] is not None
def test_create_share_link_invalid_permission(client, auth_headers, test_board):
"""Test creating share link with invalid permission level."""
response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "invalid-permission"},
headers=auth_headers,
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_create_share_link_unauthorized(client, test_board):
"""Test creating share link without authentication."""
response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only"},
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_create_share_link_not_owner(client, other_auth_headers, test_board):
"""Test creating share link for board user doesn't own."""
response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only"},
headers=other_auth_headers,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_list_share_links(client, auth_headers, test_board):
"""Test listing all share links for a board."""
# Create multiple share links
client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only"},
headers=auth_headers,
)
client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-comment"},
headers=auth_headers,
)
response = client.get(
f"/api/boards/{test_board.id}/share-links",
headers=auth_headers,
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data) >= 2
assert all("token" in link for link in data)
def test_list_share_links_unauthorized(client, test_board):
"""Test listing share links without authentication."""
response = client.get(
f"/api/boards/{test_board.id}/share-links",
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_revoke_share_link(client, auth_headers, test_board):
"""Test revoking a share link."""
# Create a share link
create_response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only"},
headers=auth_headers,
)
link_id = create_response.json()["id"]
# Revoke it
response = client.delete(
f"/api/boards/{test_board.id}/share-links/{link_id}",
headers=auth_headers,
)
assert response.status_code == status.HTTP_204_NO_CONTENT
# Verify it's revoked by listing
list_response = client.get(
f"/api/boards/{test_board.id}/share-links",
headers=auth_headers,
)
revoked_link = next((link for link in list_response.json() if link["id"] == link_id), None)
assert revoked_link is not None
assert revoked_link["is_revoked"] == True # noqa: E712
def test_revoke_share_link_not_found(client, auth_headers, test_board):
"""Test revoking non-existent share link."""
import uuid
fake_id = uuid.uuid4()
response = client.delete(
f"/api/boards/{test_board.id}/share-links/{fake_id}",
headers=auth_headers,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_access_shared_board(client, auth_headers, test_board):
"""Test accessing a board via share link."""
# Create share link
create_response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only"},
headers=auth_headers,
)
token = create_response.json()["token"]
# Access shared board (no auth required)
response = client.get(f"/api/shared/{token}")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(test_board.id)
assert data["title"] == test_board.title
def test_access_shared_board_invalid_token(client):
"""Test accessing board with invalid token."""
response = client.get("/api/shared/invalid-token-12345")
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_access_shared_board_revoked_token(client, auth_headers, test_board):
"""Test accessing board with revoked token."""
# Create and revoke share link
create_response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only"},
headers=auth_headers,
)
data = create_response.json()
token = data["token"]
link_id = data["id"]
client.delete(
f"/api/boards/{test_board.id}/share-links/{link_id}",
headers=auth_headers,
)
# Try to access with revoked token
response = client.get(f"/api/shared/{token}")
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_create_comment_on_shared_board(client, auth_headers, test_board):
"""Test creating a comment via share link with view-comment permission."""
# Create view-comment share link
create_response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-comment"},
headers=auth_headers,
)
token = create_response.json()["token"]
# Create comment (no auth required, just token)
comment_data = {
"author_name": "Test Viewer",
"content": "This is a test comment",
"position": {"x": 100, "y": 200},
}
response = client.post(f"/api/shared/{token}/comments", json=comment_data)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["author_name"] == "Test Viewer"
assert data["content"] == "This is a test comment"
assert data["position"]["x"] == 100
def test_create_comment_view_only_permission_denied(client, auth_headers, test_board):
"""Test creating comment with view-only permission fails."""
# Create view-only share link
create_response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only"},
headers=auth_headers,
)
token = create_response.json()["token"]
# Try to create comment (should fail)
comment_data = {
"author_name": "Test Viewer",
"content": "This should fail",
}
response = client.post(f"/api/shared/{token}/comments", json=comment_data)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_list_comments_on_shared_board(client, auth_headers, test_board):
"""Test listing comments via share link."""
# Create view-comment share link
create_response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-comment"},
headers=auth_headers,
)
token = create_response.json()["token"]
# Create a comment
client.post(
f"/api/shared/{token}/comments",
json={"author_name": "Viewer 1", "content": "Comment 1"},
)
# List comments
response = client.get(f"/api/shared/{token}/comments")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data) >= 1
assert data[0]["content"] == "Comment 1"
def test_list_board_comments_as_owner(client, auth_headers, test_board):
"""Test board owner listing all comments."""
# Create share link and comment
create_response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-comment"},
headers=auth_headers,
)
token = create_response.json()["token"]
client.post(
f"/api/shared/{token}/comments",
json={"author_name": "Viewer", "content": "Test comment"},
)
# Owner lists comments
response = client.get(
f"/api/boards/{test_board.id}/comments",
headers=auth_headers,
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data) >= 1
def test_token_uniqueness(client, auth_headers, test_board):
"""Test that generated tokens are unique."""
tokens = set()
for _ in range(10):
response = client.post(
f"/api/boards/{test_board.id}/share-links",
json={"permission_level": "view-only"},
headers=auth_headers,
)
token = response.json()["token"]
tokens.add(token)
# All tokens should be unique
assert len(tokens) == 10

View File

@@ -104,3 +104,106 @@ def test_user_data_no_uppercase() -> dict:
"""
return {"email": "test@example.com", "password": "testpassword123"}
@pytest.fixture
def test_user(client: TestClient, test_user_data: dict):
"""
Create and return a test user.
Args:
client: Test client
test_user_data: User credentials
Returns:
User object
"""
from app.database.models.user import User
response = client.post("/api/v1/auth/register", json=test_user_data)
user_id = response.json()["id"]
# Get user from database (use same db session)
from app.core.deps import get_db
db_gen = next(app.dependency_overrides[get_db]())
user = db_gen.query(User).filter(User.id == user_id).first()
return user
@pytest.fixture
def auth_headers(client: TestClient, test_user_data: dict) -> dict:
"""
Create authenticated headers with JWT token.
Args:
client: Test client
test_user_data: User credentials
Returns:
Dictionary with Authorization header
"""
# Register and login
client.post("/api/v1/auth/register", json=test_user_data)
login_response = client.post("/api/v1/auth/login", json=test_user_data)
token = login_response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
@pytest.fixture
def other_user_data() -> dict:
"""
Data for a second test user.
Returns:
Dictionary with test user credentials
"""
return {"email": "other@example.com", "password": "OtherPassword123"}
@pytest.fixture
def other_auth_headers(client: TestClient, other_user_data: dict) -> dict:
"""
Create authenticated headers for a second user.
Args:
client: Test client
other_user_data: Other user credentials
Returns:
Dictionary with Authorization header
"""
# Register and login
client.post("/api/v1/auth/register", json=other_user_data)
login_response = client.post("/api/v1/auth/login", json=other_user_data)
token = login_response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
@pytest.fixture
def test_board(client: TestClient, auth_headers: dict):
"""
Create a test board.
Args:
client: Test client
auth_headers: Authentication headers
Returns:
Board object
"""
from app.database.models.board import Board
response = client.post(
"/api/v1/boards",
json={"title": "Test Board", "description": "Test description"},
headers=auth_headers,
)
board_id = response.json()["id"]
# Get board from database
from app.core.deps import get_db
db_gen = next(app.dependency_overrides[get_db]())
board = db_gen.query(Board).filter(Board.id == board_id).first()
return board