Implement Sprint 1: Notebook Management CRUD
- Add NotebookService with full CRUD operations
- Add POST /api/v1/notebooks (create notebook)
- Add GET /api/v1/notebooks (list with pagination)
- Add GET /api/v1/notebooks/{id} (get by ID)
- Add PATCH /api/v1/notebooks/{id} (partial update)
- Add DELETE /api/v1/notebooks/{id} (delete)
- Add Pydantic models for requests/responses
- Add custom exceptions (ValidationError, NotFoundError, NotebookLMError)
- Add comprehensive unit tests (31 tests, 97% coverage)
- Add API integration tests (26 tests)
- Fix router prefix duplication
- Fix JSON serialization in error responses
BREAKING CHANGE: None
640 lines
23 KiB
Python
640 lines
23 KiB
Python
"""Tests for notebooks API routes.
|
|
|
|
TDD for DEV-002: POST /api/v1/notebooks
|
|
TDD for DEV-003: GET /api/v1/notebooks
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from notebooklm_agent.api.main import app
|
|
from notebooklm_agent.core.exceptions import ValidationError
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCreateNotebookEndpoint:
|
|
"""Test suite for POST /api/v1/notebooks endpoint."""
|
|
|
|
def test_create_notebook_valid_data_returns_201(self):
|
|
"""Should return 201 Created for valid notebook data."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.id = notebook_id
|
|
mock_response.title = "Test Notebook"
|
|
mock_response.description = None
|
|
mock_response.created_at = datetime.utcnow()
|
|
mock_response.updated_at = datetime.utcnow()
|
|
mock_service.create.return_value = mock_response
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.post(
|
|
"/api/v1/notebooks",
|
|
json={"title": "Test Notebook", "description": None},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["data"]["title"] == "Test Notebook"
|
|
assert data["data"]["id"] == notebook_id
|
|
|
|
def test_create_notebook_with_description_returns_201(self):
|
|
"""Should return 201 for notebook with description."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.id = notebook_id
|
|
mock_response.title = "Test Notebook"
|
|
mock_response.description = "A description"
|
|
mock_response.created_at = datetime.utcnow()
|
|
mock_response.updated_at = datetime.utcnow()
|
|
mock_service.create.return_value = mock_response
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.post(
|
|
"/api/v1/notebooks",
|
|
json={"title": "Test Notebook", "description": "A description"},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["data"]["description"] == "A description"
|
|
|
|
def test_create_notebook_short_title_returns_400(self):
|
|
"""Should return 400 for title too short."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_service.create.side_effect = ValidationError("Title must be at least 3 characters")
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.post(
|
|
"/api/v1/notebooks",
|
|
json={"title": "AB", "description": None},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert data["success"] is False
|
|
assert data["error"]["code"] == "VALIDATION_ERROR"
|
|
|
|
def test_create_notebook_missing_title_returns_422(self):
|
|
"""Should return 422 for missing title (Pydantic validation)."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
# Act
|
|
response = client.post(
|
|
"/api/v1/notebooks",
|
|
json={"description": "A description"},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert "detail" in data
|
|
|
|
def test_create_notebook_empty_title_returns_400(self):
|
|
"""Should return 400 for empty title."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_service.create.side_effect = ValidationError("Title cannot be empty")
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.post(
|
|
"/api/v1/notebooks",
|
|
json={"title": "", "description": None},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert data["success"] is False
|
|
|
|
def test_create_notebook_response_has_meta(self):
|
|
"""Should include meta in response."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.id = notebook_id
|
|
mock_response.title = "Test Notebook"
|
|
mock_response.description = None
|
|
mock_response.created_at = datetime.utcnow()
|
|
mock_response.updated_at = datetime.utcnow()
|
|
mock_service.create.return_value = mock_response
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.post(
|
|
"/api/v1/notebooks",
|
|
json={"title": "Test Notebook", "description": None},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert "meta" in data
|
|
assert "timestamp" in data["meta"]
|
|
assert "request_id" in data["meta"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestListNotebooksEndpoint:
|
|
"""Test suite for GET /api/v1/notebooks endpoint."""
|
|
|
|
def test_list_notebooks_returns_200(self):
|
|
"""Should return 200 with list of notebooks."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_notebook = MagicMock()
|
|
mock_notebook.id = str(uuid4())
|
|
mock_notebook.title = "Notebook 1"
|
|
mock_notebook.description = None
|
|
mock_notebook.created_at = datetime.utcnow()
|
|
mock_notebook.updated_at = datetime.utcnow()
|
|
|
|
mock_paginated = MagicMock()
|
|
mock_paginated.items = [mock_notebook]
|
|
mock_paginated.pagination.total = 1
|
|
mock_paginated.pagination.limit = 20
|
|
mock_paginated.pagination.offset = 0
|
|
mock_paginated.pagination.has_more = False
|
|
|
|
mock_service.list.return_value = mock_paginated
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.get("/api/v1/notebooks")
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert len(data["data"]["items"]) == 1
|
|
assert data["data"]["pagination"]["total"] == 1
|
|
|
|
def test_list_notebooks_with_pagination_returns_correct_page(self):
|
|
"""Should return correct page with limit and offset."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_paginated = MagicMock()
|
|
mock_paginated.items = []
|
|
mock_paginated.pagination.total = 100
|
|
mock_paginated.pagination.limit = 10
|
|
mock_paginated.pagination.offset = 20
|
|
mock_paginated.pagination.has_more = True
|
|
|
|
mock_service.list.return_value = mock_paginated
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.get("/api/v1/notebooks?limit=10&offset=20")
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["data"]["pagination"]["limit"] == 10
|
|
assert data["data"]["pagination"]["offset"] == 20
|
|
assert data["data"]["pagination"]["has_more"] is True
|
|
|
|
def test_list_notebooks_with_sort_returns_sorted(self):
|
|
"""Should sort notebooks by specified field."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_paginated = MagicMock()
|
|
mock_paginated.items = []
|
|
mock_paginated.pagination.total = 0
|
|
mock_paginated.pagination.limit = 20
|
|
mock_paginated.pagination.offset = 0
|
|
mock_paginated.pagination.has_more = False
|
|
|
|
mock_service.list.return_value = mock_paginated
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.get("/api/v1/notebooks?sort=title&order=asc")
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
mock_service.list.assert_called_once_with(limit=20, offset=0, sort="title", order="asc")
|
|
|
|
def test_list_notebooks_empty_list_returns_200(self):
|
|
"""Should return 200 with empty list when no notebooks."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_paginated = MagicMock()
|
|
mock_paginated.items = []
|
|
mock_paginated.pagination.total = 0
|
|
mock_paginated.pagination.limit = 20
|
|
mock_paginated.pagination.offset = 0
|
|
mock_paginated.pagination.has_more = False
|
|
|
|
mock_service.list.return_value = mock_paginated
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.get("/api/v1/notebooks")
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["data"]["items"] == []
|
|
assert data["data"]["pagination"]["total"] == 0
|
|
|
|
def test_list_notebooks_invalid_limit_returns_422(self):
|
|
"""Should return 422 for invalid limit parameter."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
# Act
|
|
response = client.get("/api/v1/notebooks?limit=200")
|
|
|
|
# Assert
|
|
assert response.status_code == 422
|
|
|
|
def test_list_notebooks_invalid_sort_returns_422(self):
|
|
"""Should return 422 for invalid sort parameter."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
# Act
|
|
response = client.get("/api/v1/notebooks?sort=invalid_field")
|
|
|
|
# Assert
|
|
assert response.status_code == 422
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestGetNotebookEndpoint:
|
|
"""Test suite for GET /api/v1/notebooks/{id} endpoint."""
|
|
|
|
def test_get_notebook_existing_id_returns_200(self):
|
|
"""Should return 200 with notebook for existing ID."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.id = notebook_id
|
|
mock_response.title = "Test Notebook"
|
|
mock_response.description = "A description"
|
|
mock_response.created_at = datetime.utcnow()
|
|
mock_response.updated_at = datetime.utcnow()
|
|
mock_service.get.return_value = mock_response
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.get(f"/api/v1/notebooks/{notebook_id}")
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["data"]["id"] == notebook_id
|
|
assert data["data"]["title"] == "Test Notebook"
|
|
|
|
def test_get_notebook_nonexistent_id_returns_404(self):
|
|
"""Should return 404 for non-existent notebook ID."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
from notebooklm_agent.core.exceptions import NotFoundError
|
|
|
|
mock_service.get.side_effect = NotFoundError("Notebook", notebook_id)
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.get(f"/api/v1/notebooks/{notebook_id}")
|
|
|
|
# Assert
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert data["success"] is False
|
|
assert data["error"]["code"] == "NOT_FOUND"
|
|
|
|
def test_get_notebook_invalid_uuid_returns_400(self):
|
|
"""Should return 400 for invalid UUID format."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
# Act
|
|
response = client.get("/api/v1/notebooks/invalid-uuid")
|
|
|
|
# Assert
|
|
assert response.status_code == 400
|
|
|
|
def test_get_notebook_response_has_meta(self):
|
|
"""Should include meta in response."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.id = notebook_id
|
|
mock_response.title = "Test Notebook"
|
|
mock_response.description = None
|
|
mock_response.created_at = datetime.utcnow()
|
|
mock_response.updated_at = datetime.utcnow()
|
|
mock_service.get.return_value = mock_response
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.get(f"/api/v1/notebooks/{notebook_id}")
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "meta" in data
|
|
assert "timestamp" in data["meta"]
|
|
assert "request_id" in data["meta"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestDeleteNotebookEndpoint:
|
|
"""Test suite for DELETE /api/v1/notebooks/{id} endpoint."""
|
|
|
|
def test_delete_notebook_valid_id_returns_204(self):
|
|
"""Should return 204 No Content for valid notebook ID."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_service.delete.return_value = None
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.delete(f"/api/v1/notebooks/{notebook_id}")
|
|
|
|
# Assert
|
|
assert response.status_code == 204
|
|
assert response.content == b""
|
|
|
|
def test_delete_notebook_nonexistent_id_returns_404(self):
|
|
"""Should return 404 for non-existent notebook ID."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
from notebooklm_agent.core.exceptions import NotFoundError
|
|
|
|
mock_service.delete.side_effect = NotFoundError("Notebook", notebook_id)
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.delete(f"/api/v1/notebooks/{notebook_id}")
|
|
|
|
# Assert
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert data["detail"]["success"] is False
|
|
assert data["detail"]["error"]["code"] == "NOT_FOUND"
|
|
|
|
def test_delete_notebook_invalid_uuid_returns_400(self):
|
|
"""Should return 400 for invalid UUID format."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
# Act
|
|
response = client.delete("/api/v1/notebooks/invalid-uuid")
|
|
|
|
# Assert
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert data["detail"]["success"] is False
|
|
assert data["detail"]["error"]["code"] == "VALIDATION_ERROR"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestUpdateNotebookEndpoint:
|
|
"""Test suite for PATCH /api/v1/notebooks/{id} endpoint."""
|
|
|
|
def test_update_notebook_title_returns_200(self):
|
|
"""Should return 200 with updated notebook when updating title."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.id = notebook_id
|
|
mock_response.title = "Updated Title"
|
|
mock_response.description = None
|
|
mock_response.created_at = datetime.utcnow()
|
|
mock_response.updated_at = datetime.utcnow()
|
|
mock_service.update.return_value = mock_response
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.patch(
|
|
f"/api/v1/notebooks/{notebook_id}",
|
|
json={"title": "Updated Title"},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["data"]["title"] == "Updated Title"
|
|
|
|
def test_update_notebook_description_only_returns_200(self):
|
|
"""Should return 200 when updating only description."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.id = notebook_id
|
|
mock_response.title = "Original Title"
|
|
mock_response.description = "New Description"
|
|
mock_response.created_at = datetime.utcnow()
|
|
mock_response.updated_at = datetime.utcnow()
|
|
mock_service.update.return_value = mock_response
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.patch(
|
|
f"/api/v1/notebooks/{notebook_id}",
|
|
json={"description": "New Description"},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["data"]["description"] == "New Description"
|
|
|
|
def test_update_notebook_both_fields_returns_200(self):
|
|
"""Should return 200 when updating both title and description."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.id = notebook_id
|
|
mock_response.title = "Updated Title"
|
|
mock_response.description = "Updated Description"
|
|
mock_response.created_at = datetime.utcnow()
|
|
mock_response.updated_at = datetime.utcnow()
|
|
mock_service.update.return_value = mock_response
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.patch(
|
|
f"/api/v1/notebooks/{notebook_id}",
|
|
json={"title": "Updated Title", "description": "Updated Description"},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["data"]["title"] == "Updated Title"
|
|
assert data["data"]["description"] == "Updated Description"
|
|
|
|
def test_update_notebook_nonexistent_id_returns_404(self):
|
|
"""Should return 404 for non-existent notebook ID."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
from notebooklm_agent.core.exceptions import NotFoundError
|
|
|
|
mock_service.update.side_effect = NotFoundError("Notebook", notebook_id)
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.patch(
|
|
f"/api/v1/notebooks/{notebook_id}",
|
|
json={"title": "Updated Title"},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert data["success"] is False
|
|
assert data["error"]["code"] == "NOT_FOUND"
|
|
|
|
def test_update_notebook_invalid_title_returns_400(self):
|
|
"""Should return 400 for invalid title."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
from notebooklm_agent.core.exceptions import ValidationError
|
|
|
|
mock_service.update.side_effect = ValidationError("Title too short")
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.patch(
|
|
f"/api/v1/notebooks/{notebook_id}",
|
|
json={"title": "AB"},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert data["success"] is False
|
|
assert data["error"]["code"] == "VALIDATION_ERROR"
|
|
|
|
def test_update_notebook_invalid_uuid_returns_400(self):
|
|
"""Should return 400 for invalid UUID format."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
|
|
# Act
|
|
response = client.patch(
|
|
"/api/v1/notebooks/invalid-uuid",
|
|
json={"title": "Updated Title"},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 400
|
|
|
|
def test_update_notebook_empty_body_returns_200(self):
|
|
"""Should return 200 with unchanged notebook for empty body."""
|
|
# Arrange
|
|
client = TestClient(app)
|
|
notebook_id = str(uuid4())
|
|
|
|
with patch("notebooklm_agent.api.routes.notebooks.NotebookService") as mock_service_class:
|
|
mock_service = AsyncMock()
|
|
mock_response = MagicMock()
|
|
mock_response.id = notebook_id
|
|
mock_response.title = "Original Title"
|
|
mock_response.description = "Original Description"
|
|
mock_response.created_at = datetime.utcnow()
|
|
mock_response.updated_at = datetime.utcnow()
|
|
mock_service.update.return_value = mock_response
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Act
|
|
response = client.patch(
|
|
f"/api/v1/notebooks/{notebook_id}",
|
|
json={},
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|