chore: bring the test suite up to ruff's standards
Mostly automatic: empty f-strings, unused imports, formatting. The manual half was duplicated import blocks stranded mid-file and `== None` assertions, which become `is None` here because they compare plain attributes -- unlike the identical rule in filters/book.py, where they build SQL. Two unused variables are the tests never checking the disk in either delete_files direction. Left in place under a noqa so the gap stays visible.
This commit is contained in:
@@ -114,6 +114,24 @@ CMD ["litestar", "--app-dir", "chitai", "run", "--host", "0.0.0.0", "--port", "8
|
|||||||
`litestar run` is the CLI development runner. Production should invoke uvicorn or granian
|
`litestar run` is the CLI development runner. Production should invoke uvicorn or granian
|
||||||
directly, with a worker count.
|
directly, with a worker count.
|
||||||
|
|
||||||
|
### The delete_files flag is untested in both directions
|
||||||
|
|
||||||
|
`backend/tests/integration/test_book.py` — `test_remove_file_with_delete_files_false_keeps_filesystem_file`
|
||||||
|
and `test_remove_file_with_delete_files_true_removes_filesystem_file`
|
||||||
|
|
||||||
|
Both tests capture the file's path and then assert only `response.status_code == 204`.
|
||||||
|
Neither looks at the disk. So the flag that decides whether removing a file from a book
|
||||||
|
also **erases it from the filesystem** is covered in name only, in both directions.
|
||||||
|
|
||||||
|
Ruff surfaced this as two `F841` unused variables; the variables carry a `# noqa: F841`
|
||||||
|
and a comment rather than being deleted, so the gap stays visible. Remove the noqa when
|
||||||
|
the assertions land.
|
||||||
|
|
||||||
|
The reason it is not a two-line fix: `FileMetadata.path` is stored relative to `book.path`,
|
||||||
|
so the test has to resolve it against the library root to know what to stat. That
|
||||||
|
resolution is the same thing `BookService.get_files` is recorded as getting wrong (see
|
||||||
|
`backend/AGENTS.md`), so it is worth settling once and using in both places.
|
||||||
|
|
||||||
### No type checker on the backend
|
### No type checker on the backend
|
||||||
|
|
||||||
Formatting, linting and tests now run in CI (`.gitea/workflows/ci.yml`) and block, and the
|
Formatting, linting and tests now run in CI (`.gitea/workflows/ci.yml`) and block, and the
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from pathlib import Path
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from chitai import services
|
||||||
|
|
||||||
from advanced_alchemy.base import UUIDAuditBase
|
from advanced_alchemy.base import UUIDAuditBase
|
||||||
from litestar.testing import AsyncTestClient
|
from litestar.testing import AsyncTestClient
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
@@ -155,7 +157,6 @@ async def other_authenticated_client(
|
|||||||
|
|
||||||
|
|
||||||
# Service fixtures
|
# Service fixtures
|
||||||
from chitai import services
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from litestar.status_codes import HTTP_400_BAD_REQUEST
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -218,7 +219,9 @@ async def test_list_books_by_id(populated_authenticated_client: AsyncClient) ->
|
|||||||
compare a bigint primary key against them. Nothing called it until a screen needed
|
compare a bigint primary key against them. Nothing called it until a screen needed
|
||||||
to fetch a handful of books by id.
|
to fetch a handful of books by id.
|
||||||
"""
|
"""
|
||||||
response = await populated_authenticated_client.get("/books?ids=1&ids=2&pageSize=10")
|
response = await populated_authenticated_client.get(
|
||||||
|
"/books?ids=1&ids=2&pageSize=10"
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert sorted(book["id"] for book in response.json()["items"]) == [1, 2]
|
assert sorted(book["id"] for book in response.json()["items"]) == [1, 2]
|
||||||
@@ -228,7 +231,7 @@ async def test_get_book_by_id(populated_authenticated_client: AsyncClient) -> No
|
|||||||
"""Test retrieving a specific book by ID."""
|
"""Test retrieving a specific book by ID."""
|
||||||
|
|
||||||
# Retrieve the book
|
# Retrieve the book
|
||||||
response = await populated_authenticated_client.get(f"/books/1")
|
response = await populated_authenticated_client.get("/books/1")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
book_data = response.json()
|
book_data = response.json()
|
||||||
@@ -300,13 +303,13 @@ async def test_delete_book_metadata_only(
|
|||||||
|
|
||||||
# Delete book without deleting files
|
# Delete book without deleting files
|
||||||
response = await populated_authenticated_client.delete(
|
response = await populated_authenticated_client.delete(
|
||||||
f"/books?book_ids=3&delete_files=false&library_id=1"
|
"/books?book_ids=3&delete_files=false&library_id=1"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
|
|
||||||
# Verify book is deleted
|
# Verify book is deleted
|
||||||
get_response = await populated_authenticated_client.get(f"/books/3")
|
get_response = await populated_authenticated_client.get("/books/3")
|
||||||
assert get_response.status_code == 404
|
assert get_response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
@@ -317,7 +320,7 @@ async def test_delete_book_with_files(
|
|||||||
|
|
||||||
# Delete book and files
|
# Delete book and files
|
||||||
response = await populated_authenticated_client.delete(
|
response = await populated_authenticated_client.delete(
|
||||||
f"/books?book_ids=3&delete_files=true&library_id=1"
|
"/books?book_ids=3&delete_files=true&library_id=1"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
@@ -330,7 +333,7 @@ async def test_delete_specific_book_files(
|
|||||||
|
|
||||||
# Delete specific file
|
# Delete specific file
|
||||||
response = await populated_authenticated_client.delete(
|
response = await populated_authenticated_client.delete(
|
||||||
f"/books/1/files?file_ids=1",
|
"/books/1/files?file_ids=1",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
@@ -347,7 +350,7 @@ async def test_update_reading_progress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = await populated_authenticated_client.post(
|
response = await populated_authenticated_client.post(
|
||||||
f"/books/progress/1",
|
"/books/progress/1",
|
||||||
json=progress_data,
|
json=progress_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -423,7 +426,9 @@ async def test_create_books_groups_formats_within_one_folder(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Picking a book's own folder yields one book with both formats, not two books."""
|
"""Picking a book's own folder yields one book with both formats, not two books."""
|
||||||
epub = Path("tests/data_files/Metamorphosis - Franz Kafka.epub").read_bytes()
|
epub = Path("tests/data_files/Metamorphosis - Franz Kafka.epub").read_bytes()
|
||||||
pdf = Path("tests/data_files/Calculus Made Easy - Silvanus Thompson.pdf").read_bytes()
|
pdf = Path(
|
||||||
|
"tests/data_files/Calculus Made Easy - Silvanus Thompson.pdf"
|
||||||
|
).read_bytes()
|
||||||
|
|
||||||
files = [
|
files = [
|
||||||
("files", ("Metamorphosis/Metamorphosis.epub", epub, "application/epub+zip")),
|
("files", ("Metamorphosis/Metamorphosis.epub", epub, "application/epub+zip")),
|
||||||
@@ -534,7 +539,9 @@ class TestDuplicateHandling:
|
|||||||
"files",
|
"files",
|
||||||
(
|
(
|
||||||
"war.epub",
|
"war.epub",
|
||||||
Path("tests/data_files/The Art of War - Sun Tzu.epub").read_bytes(),
|
Path(
|
||||||
|
"tests/data_files/The Art of War - Sun Tzu.epub"
|
||||||
|
).read_bytes(),
|
||||||
"application/epub+zip",
|
"application/epub+zip",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -550,7 +557,9 @@ class TestDuplicateHandling:
|
|||||||
"files",
|
"files",
|
||||||
(
|
(
|
||||||
"war.epub",
|
"war.epub",
|
||||||
Path("tests/data_files/The Art of War - Sun Tzu.epub").read_bytes(),
|
Path(
|
||||||
|
"tests/data_files/The Art of War - Sun Tzu.epub"
|
||||||
|
).read_bytes(),
|
||||||
"application/epub+zip",
|
"application/epub+zip",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -736,7 +745,9 @@ class TestDuplicateBooks:
|
|||||||
assert len(merged["files"]) == 2
|
assert len(merged["files"]) == 2
|
||||||
|
|
||||||
# The folded record is gone, and the group it formed with it.
|
# The folded record is gone, and the group it formed with it.
|
||||||
assert (await authenticated_client.get(f"/books/{fold['id']}")).status_code == 404
|
assert (
|
||||||
|
await authenticated_client.get(f"/books/{fold['id']}")
|
||||||
|
).status_code == 404
|
||||||
groups = await authenticated_client.get("/books/duplicate-books?library_id=1")
|
groups = await authenticated_client.get("/books/duplicate-books?library_id=1")
|
||||||
assert groups.json() == []
|
assert groups.json() == []
|
||||||
|
|
||||||
@@ -786,16 +797,6 @@ class TestDuplicateBooks:
|
|||||||
# async def test_edit_book_metadata(authenticated_client: AsyncClient) -> None:
|
# async def test_edit_book_metadata(authenticated_client: AsyncClient) -> None:
|
||||||
# raise NotImplementedError()
|
# raise NotImplementedError()
|
||||||
|
|
||||||
import pytest
|
|
||||||
import aiofiles
|
|
||||||
from httpx import AsyncClient
|
|
||||||
from pathlib import Path
|
|
||||||
from litestar.status_codes import HTTP_400_BAD_REQUEST
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from httpx import AsyncClient
|
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
|
|
||||||
class TestMetadataUpdates:
|
class TestMetadataUpdates:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -849,8 +850,10 @@ class TestMetadataUpdates:
|
|||||||
(
|
(
|
||||||
"authors", # Update with new authors
|
"authors", # Update with new authors
|
||||||
["New Author 1", "New Author 2"],
|
["New Author 1", "New Author 2"],
|
||||||
lambda data: {a["name"] for a in data["authors"]}
|
lambda data: (
|
||||||
== {"New Author 1", "New Author 2"},
|
{a["name"] for a in data["authors"]}
|
||||||
|
== {"New Author 1", "New Author 2"}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"authors", # Clear authors
|
"authors", # Clear authors
|
||||||
@@ -860,8 +863,9 @@ class TestMetadataUpdates:
|
|||||||
(
|
(
|
||||||
"tags", # Update with new tags
|
"tags", # Update with new tags
|
||||||
["Tag 1", "Tag 2", "Tag 3"],
|
["Tag 1", "Tag 2", "Tag 3"],
|
||||||
lambda data: {t["name"] for t in data["tags"]}
|
lambda data: (
|
||||||
== {"Tag 1", "Tag 2", "Tag 3"},
|
{t["name"] for t in data["tags"]} == {"Tag 1", "Tag 2", "Tag 3"}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"tags", # Clear tags
|
"tags", # Clear tags
|
||||||
@@ -881,8 +885,10 @@ class TestMetadataUpdates:
|
|||||||
(
|
(
|
||||||
"identifiers", # Update with new identifiers
|
"identifiers", # Update with new identifiers
|
||||||
{"isbn-13": "978-1234567890", "doi": "10.example/id"},
|
{"isbn-13": "978-1234567890", "doi": "10.example/id"},
|
||||||
lambda data: data["identifiers"]
|
lambda data: (
|
||||||
== {"isbn-13": "978-1234567890", "doi": "10.example/id"},
|
data["identifiers"]
|
||||||
|
== {"isbn-13": "978-1234567890", "doi": "10.example/id"}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"identifiers", # Clear identifiers
|
"identifiers", # Clear identifiers
|
||||||
@@ -1053,7 +1059,7 @@ class TestMetadataUpdates:
|
|||||||
|
|
||||||
result = response.json()
|
result = response.json()
|
||||||
|
|
||||||
assert result[updated_field] == None
|
assert result[updated_field] is None
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("updated_field"),
|
("updated_field"),
|
||||||
@@ -1215,7 +1221,9 @@ class TestFileManagement:
|
|||||||
pytest.skip("Book has no files")
|
pytest.skip("Book has no files")
|
||||||
|
|
||||||
file_id = book_data["files"][0]["id"]
|
file_id = book_data["files"][0]["id"]
|
||||||
filename = book_data["files"][0].get("path")
|
# TODO: this test asserts only the 204 and never checks the disk, so the flag it is
|
||||||
|
# named for is untested. See TODO.md; drop the noqa when the assertion lands.
|
||||||
|
filename = book_data["files"][0].get("path") # noqa: F841
|
||||||
|
|
||||||
# Remove file without deleting from filesystem
|
# Remove file without deleting from filesystem
|
||||||
response = await populated_authenticated_client.delete(
|
response = await populated_authenticated_client.delete(
|
||||||
@@ -1240,7 +1248,8 @@ class TestFileManagement:
|
|||||||
|
|
||||||
book_data = add_response.json()
|
book_data = add_response.json()
|
||||||
file_id = book_data["files"][-1]["id"]
|
file_id = book_data["files"][-1]["id"]
|
||||||
file_path = book_data["files"][-1].get("path")
|
# TODO: as above -- the file is never checked for removal from disk.
|
||||||
|
file_path = book_data["files"][-1].get("path") # noqa: F841
|
||||||
|
|
||||||
# Remove file with deletion from filesystem
|
# Remove file with deletion from filesystem
|
||||||
response = await populated_authenticated_client.delete(
|
response = await populated_authenticated_client.delete(
|
||||||
@@ -1295,7 +1304,9 @@ class TestUnnameableFormats:
|
|||||||
self, authenticated_client: AsyncClient
|
self, authenticated_client: AsyncClient
|
||||||
) -> None:
|
) -> None:
|
||||||
response = await authenticated_client.post(
|
response = await authenticated_client.post(
|
||||||
"/books?library_id=1", files=self.upload("Dune.mobi"), data={"library_id": 1}
|
"/books?library_id=1",
|
||||||
|
files=self.upload("Dune.mobi"),
|
||||||
|
data={"library_id": 1},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
@@ -1323,9 +1334,7 @@ class TestUnnameableFormats:
|
|||||||
assert detail.status_code == 200
|
assert detail.status_code == 200
|
||||||
assert detail.json()["files"][0]["content_type"] is None
|
assert detail.json()["files"][0]["content_type"] is None
|
||||||
|
|
||||||
async def test_the_file_downloads(
|
async def test_the_file_downloads(self, authenticated_client: AsyncClient) -> None:
|
||||||
self, authenticated_client: AsyncClient
|
|
||||||
) -> None:
|
|
||||||
"""Litestar supplies its own media type when the row carries none."""
|
"""Litestar supplies its own media type when the row carries none."""
|
||||||
created = await authenticated_client.post(
|
created = await authenticated_client.post(
|
||||||
"/books?library_id=1",
|
"/books?library_id=1",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import pytest
|
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +199,6 @@ async def test_remove_books_from_shelf(
|
|||||||
"/books", params={"shelves": shelf_id}
|
"/books", params={"shelves": shelf_id}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
assert books_response.status_code == 200
|
assert books_response.status_code == 200
|
||||||
assert books_response.json()["total"] == 2
|
assert books_response.json()["total"] == 2
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ def fx_source(tmp_path: Path) -> Path:
|
|||||||
cover=True,
|
cover=True,
|
||||||
formats={"EPUB": EPUB},
|
formats={"EPUB": EPUB},
|
||||||
)
|
)
|
||||||
fixture.add_book(2, "The Art of War", authors=["Sun Tzu"], formats={"EPUB": OTHER_EPUB})
|
fixture.add_book(
|
||||||
|
2, "The Art of War", authors=["Sun Tzu"], formats={"EPUB": OTHER_EPUB}
|
||||||
|
)
|
||||||
fixture.add_book(3, "Metadata Only", authors=["Nobody"])
|
fixture.add_book(3, "Metadata Only", authors=["Nobody"])
|
||||||
|
|
||||||
return fixture.commit()
|
return fixture.commit()
|
||||||
@@ -95,7 +97,8 @@ async def test_an_uploaded_library_imports(
|
|||||||
authenticated_client: AsyncClient, source: Path, tmp_path: Path
|
authenticated_client: AsyncClient, source: Path, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
status, job = await upload(
|
status, job = await upload(
|
||||||
authenticated_client, zip_of(source, tmp_path / "out", prefix="Calibre Library/")
|
authenticated_client,
|
||||||
|
zip_of(source, tmp_path / "out", prefix="Calibre Library/"),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert status == 202
|
assert status == 202
|
||||||
@@ -278,7 +281,9 @@ async def test_a_second_copy_is_counted_as_a_possible_duplicate(
|
|||||||
padded.write_bytes(EPUB.read_bytes() + b"\0" * 64)
|
padded.write_bytes(EPUB.read_bytes() + b"\0" * 64)
|
||||||
|
|
||||||
fixture = CalibreFixture(tmp_path / "calibre")
|
fixture = CalibreFixture(tmp_path / "calibre")
|
||||||
fixture.add_book(1, "The Metamorphosis", authors=["Franz Kafka"], formats={"EPUB": EPUB})
|
fixture.add_book(
|
||||||
|
1, "The Metamorphosis", authors=["Franz Kafka"], formats={"EPUB": EPUB}
|
||||||
|
)
|
||||||
fixture.add_book(
|
fixture.add_book(
|
||||||
2, "The Metamorphosis", authors=["Franz Kafka"], formats={"EPUB": padded}
|
2, "The Metamorphosis", authors=["Franz Kafka"], formats={"EPUB": padded}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ from pathlib import Path
|
|||||||
# Known KOReader hashes for test files
|
# Known KOReader hashes for test files
|
||||||
TEST_FILES = {
|
TEST_FILES = {
|
||||||
"Moby Dick; Or, The Whale - Herman Melville.epub": {
|
"Moby Dick; Or, The Whale - Herman Melville.epub": {
|
||||||
"path": Path("tests/data_files/Moby Dick; Or, The Whale - Herman Melville.epub"),
|
"path": Path(
|
||||||
|
"tests/data_files/Moby Dick; Or, The Whale - Herman Melville.epub"
|
||||||
|
),
|
||||||
"hash": "ceeef909ec65653ba77e1380dff998fb",
|
"hash": "ceeef909ec65653ba77e1380dff998fb",
|
||||||
"content_type": "application/epub+zip",
|
"content_type": "application/epub+zip",
|
||||||
},
|
},
|
||||||
@@ -59,7 +61,9 @@ async def test_add_file_to_book_generates_correct_hash(
|
|||||||
first_book = TEST_FILES["Moby Dick; Or, The Whale - Herman Melville.epub"]
|
first_book = TEST_FILES["Moby Dick; Or, The Whale - Herman Melville.epub"]
|
||||||
first_content = first_book["path"].read_bytes()
|
first_content = first_book["path"].read_bytes()
|
||||||
|
|
||||||
files = [("files", (first_book["path"].name, first_content, first_book["content_type"]))]
|
files = [
|
||||||
|
("files", (first_book["path"].name, first_content, first_book["content_type"]))
|
||||||
|
]
|
||||||
data = {"library_id": "1"}
|
data = {"library_id": "1"}
|
||||||
|
|
||||||
create_response = await authenticated_client.post(
|
create_response = await authenticated_client.post(
|
||||||
@@ -75,7 +79,12 @@ async def test_add_file_to_book_generates_correct_hash(
|
|||||||
second_book = TEST_FILES["Calculus Made Easy - Silvanus Thompson.pdf"]
|
second_book = TEST_FILES["Calculus Made Easy - Silvanus Thompson.pdf"]
|
||||||
second_content = second_book["path"].read_bytes()
|
second_content = second_book["path"].read_bytes()
|
||||||
|
|
||||||
add_files = [("data", (second_book["path"].name, second_content, second_book["content_type"]))]
|
add_files = [
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
(second_book["path"].name, second_content, second_book["content_type"]),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
add_response = await authenticated_client.post(
|
add_response = await authenticated_client.post(
|
||||||
f"/books/{book_id}/files",
|
f"/books/{book_id}/files",
|
||||||
|
|||||||
@@ -40,5 +40,5 @@ async def test_create_library(
|
|||||||
assert result["name"] == "Test Library"
|
assert result["name"] == "Test Library"
|
||||||
assert result["root_path"] == f"{tmp_path}/books"
|
assert result["root_path"] == f"{tmp_path}/books"
|
||||||
assert result["path_template"] == "{author}/{title}"
|
assert result["path_template"] == "{author}/{title}"
|
||||||
assert result["read_only"] == False
|
assert result["read_only"] is False
|
||||||
assert result["description"] is None
|
assert result["description"] is None
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from chitai.services.filesystem_library import BookPathGenerator, sanitize_path_component
|
from chitai.services.filesystem_library import (
|
||||||
|
BookPathGenerator,
|
||||||
|
sanitize_path_component,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path("/library")
|
ROOT = Path("/library")
|
||||||
@@ -23,12 +26,15 @@ def test_a_book_with_no_authors() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_a_series_adds_a_level_and_pads_the_position() -> None:
|
def test_a_series_adds_a_level_and_pads_the_position() -> None:
|
||||||
assert path_for(
|
assert (
|
||||||
|
path_for(
|
||||||
title="Persepolis Rising",
|
title="Persepolis Rising",
|
||||||
authors=["James S. A. Corey"],
|
authors=["James S. A. Corey"],
|
||||||
series="The Expanse",
|
series="The Expanse",
|
||||||
series_position="7",
|
series_position="7",
|
||||||
) == ROOT / "James S. A. Corey" / "The Expanse" / "07 - Persepolis Rising"
|
)
|
||||||
|
== ROOT / "James S. A. Corey" / "The Expanse" / "07 - Persepolis Rising"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_a_slash_in_a_title_does_not_add_a_directory() -> None:
|
def test_a_slash_in_a_title_does_not_add_a_directory() -> None:
|
||||||
@@ -43,16 +49,25 @@ def test_a_slash_in_a_title_does_not_add_a_directory() -> None:
|
|||||||
generated = path_for(title="Back in Black: AC/DC", authors=["Murray Engleheart"])
|
generated = path_for(title="Back in Black: AC/DC", authors=["Murray Engleheart"])
|
||||||
|
|
||||||
assert generated == ROOT / "Murray Engleheart" / "Back in Black: AC_DC"
|
assert generated == ROOT / "Murray Engleheart" / "Back in Black: AC_DC"
|
||||||
assert generated.relative_to(ROOT).parts == ("Murray Engleheart", "Back in Black: AC_DC")
|
assert generated.relative_to(ROOT).parts == (
|
||||||
|
"Murray Engleheart",
|
||||||
|
"Back in Black: AC_DC",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_a_slash_in_an_author_or_series_is_handled_too() -> None:
|
def test_a_slash_in_an_author_or_series_is_handled_too() -> None:
|
||||||
assert path_for(title="Split", authors=["A/B Collective"]) == (
|
assert path_for(title="Split", authors=["A/B Collective"]) == (
|
||||||
ROOT / "A_B Collective" / "Split"
|
ROOT / "A_B Collective" / "Split"
|
||||||
)
|
)
|
||||||
assert path_for(
|
assert (
|
||||||
title="Volume One", authors=["Someone"], series="Either/Or", series_position="1"
|
path_for(
|
||||||
) == ROOT / "Someone" / "Either_Or" / "01 - Volume One"
|
title="Volume One",
|
||||||
|
authors=["Someone"],
|
||||||
|
series="Either/Or",
|
||||||
|
series_position="1",
|
||||||
|
)
|
||||||
|
== ROOT / "Someone" / "Either_Or" / "01 - Volume One"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_control_characters_are_removed() -> None:
|
def test_control_characters_are_removed() -> None:
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ class TestNormalizeTitle:
|
|||||||
assert normalize_title("The") == "the"
|
assert normalize_title("The") == "the"
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"title", ["Catch 22", "Fahrenheit 451", "Blade Runner 2049", "1984", "Apollo 13"]
|
"title",
|
||||||
|
["Catch 22", "Fahrenheit 451", "Blade Runner 2049", "1984", "Apollo 13"],
|
||||||
)
|
)
|
||||||
def test_a_number_is_not_an_edition(self, title: str) -> None:
|
def test_a_number_is_not_an_edition(self, title: str) -> None:
|
||||||
"""Edition stripping keys on the `e`; a bare number is part of the title."""
|
"""Edition stripping keys on the `e`; a bare number is part of the title."""
|
||||||
@@ -163,8 +164,13 @@ class TestNormalizeIdentifier:
|
|||||||
|
|
||||||
def test_uuids_are_refused(self) -> None:
|
def test_uuids_are_refused(self) -> None:
|
||||||
"""Generated per build, so they only re-find what the hash check catches."""
|
"""Generated per build, so they only re-find what the hash check catches."""
|
||||||
assert normalize_identifier("uuid", "3f2b1c4e-1111-2222-3333-444455556666") is None
|
assert (
|
||||||
assert normalize_identifier("urn:uuid", "3f2b1c4e-1111-2222-3333-444455556666") is None
|
normalize_identifier("uuid", "3f2b1c4e-1111-2222-3333-444455556666") is None
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
normalize_identifier("urn:uuid", "3f2b1c4e-1111-2222-3333-444455556666")
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
|
||||||
def test_other_schemes_keep_their_own_key(self) -> None:
|
def test_other_schemes_keep_their_own_key(self) -> None:
|
||||||
assert normalize_identifier("asin", "B000FC0PDA") == "asin:b000fc0pda"
|
assert normalize_identifier("asin", "B000FC0PDA") == "asin:b000fc0pda"
|
||||||
@@ -187,7 +193,9 @@ class TestIsbnConversion:
|
|||||||
assert isbn10_to_isbn13("043942089X") == "9780439420891"
|
assert isbn10_to_isbn13("043942089X") == "9780439420891"
|
||||||
|
|
||||||
@pytest.mark.parametrize("isbn", ["0486282113", "9780486282114", "nonsense"])
|
@pytest.mark.parametrize("isbn", ["0486282113", "9780486282114", "nonsense"])
|
||||||
def test_anything_that_is_not_an_isbn_10_converts_to_nothing(self, isbn: str) -> None:
|
def test_anything_that_is_not_an_isbn_10_converts_to_nothing(
|
||||||
|
self, isbn: str
|
||||||
|
) -> None:
|
||||||
assert isbn10_to_isbn13(isbn) is None
|
assert isbn10_to_isbn13(isbn) is None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class TestEpubExtractor:
|
|||||||
assert metadata["published_date"] == date(year=2001, month=7, day=1)
|
assert metadata["published_date"] == date(year=2001, month=7, day=1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
EPUB = Path("tests/data_files/Metamorphosis - Franz Kafka.epub")
|
EPUB = Path("tests/data_files/Metamorphosis - Franz Kafka.epub")
|
||||||
PDF = Path("tests/data_files/Calculus Made Easy - Silvanus Thompson.pdf")
|
PDF = Path("tests/data_files/Calculus Made Easy - Silvanus Thompson.pdf")
|
||||||
|
|
||||||
@@ -31,7 +30,9 @@ PDF = Path("tests/data_files/Calculus Made Easy - Silvanus Thompson.pdf")
|
|||||||
class TestIdentifierMerging:
|
class TestIdentifierMerging:
|
||||||
"""A book's formats each contribute identifiers; none of them replaces the rest."""
|
"""A book's formats each contribute identifiers; none of them replaces the rest."""
|
||||||
|
|
||||||
async def test_every_format_contributes(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
async def test_every_format_contributes(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Identifiers are a collection, not a single value.
|
Identifiers are a collection, not a single value.
|
||||||
|
|
||||||
@@ -47,7 +48,9 @@ class TestIdentifierMerging:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def pdf(_file):
|
async def pdf(_file):
|
||||||
return {"identifiers": {"isbn-13": "9781718500402", "isbn-10": "1593270356"}}
|
return {
|
||||||
|
"identifiers": {"isbn-13": "9781718500402", "isbn-10": "1593270356"}
|
||||||
|
}
|
||||||
|
|
||||||
monkeypatch.setattr(EpubExtractor, "extract_metadata", epub)
|
monkeypatch.setattr(EpubExtractor, "extract_metadata", epub)
|
||||||
monkeypatch.setattr(PdfExtractor, "extract_metadata", pdf)
|
monkeypatch.setattr(PdfExtractor, "extract_metadata", pdf)
|
||||||
@@ -133,7 +136,9 @@ class TestSplitEdition:
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_editions_are_split_out(self, title: str, stripped: str, edition: int) -> None:
|
def test_editions_are_split_out(
|
||||||
|
self, title: str, stripped: str, edition: int
|
||||||
|
) -> None:
|
||||||
assert split_edition(title) == (stripped, edition)
|
assert split_edition(title) == (stripped, edition)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -171,7 +176,10 @@ class TestEditionFromFiles:
|
|||||||
"""The PDF fixture calls itself a 2nd edition in its own metadata title."""
|
"""The PDF fixture calls itself a 2nd edition in its own metadata title."""
|
||||||
metadata = await Extractor.extract_metadata([PDF])
|
metadata = await Extractor.extract_metadata([PDF])
|
||||||
|
|
||||||
assert metadata["title"] == "The Project Gutenberg eBook #33283: Calculus Made Easy"
|
assert (
|
||||||
|
metadata["title"]
|
||||||
|
== "The Project Gutenberg eBook #33283: Calculus Made Easy"
|
||||||
|
)
|
||||||
assert metadata["edition"] == 2
|
assert metadata["edition"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ PDF = DATA_FILES / "Calculus Made Easy - Silvanus Thompson.pdf"
|
|||||||
def upload(path: Path, name: str | None = None) -> UploadFile:
|
def upload(path: Path, name: str | None = None) -> UploadFile:
|
||||||
"""An uploaded file carrying the bytes of one of the test fixtures."""
|
"""An uploaded file carrying the bytes of one of the test fixtures."""
|
||||||
return UploadFile(
|
return UploadFile(
|
||||||
content_type="application/pdf" if path.suffix == ".pdf" else "application/epub+zip",
|
content_type="application/pdf"
|
||||||
|
if path.suffix == ".pdf"
|
||||||
|
else "application/epub+zip",
|
||||||
filename=name or path.name,
|
filename=name or path.name,
|
||||||
file_data=path.read_bytes(),
|
file_data=path.read_bytes(),
|
||||||
)
|
)
|
||||||
@@ -538,9 +540,7 @@ class TestBookPathCollisions:
|
|||||||
|
|
||||||
assert original.path != forced.path
|
assert original.path != forced.path
|
||||||
|
|
||||||
paths = {
|
paths = {Path(book.path) / book.files[0].path for book in (original, forced)}
|
||||||
Path(book.path) / book.files[0].path for book in (original, forced)
|
|
||||||
}
|
|
||||||
assert len(paths) == 2
|
assert len(paths) == 2
|
||||||
assert all(path.is_file() for path in paths)
|
assert all(path.is_file() for path in paths)
|
||||||
|
|
||||||
@@ -581,7 +581,10 @@ class TestBookPathCollisions:
|
|||||||
# Renamed onto the first book's author and title.
|
# Renamed onto the first book's author and title.
|
||||||
await books_service.update_book(
|
await books_service.update_book(
|
||||||
second.books[0].id,
|
second.books[0].id,
|
||||||
{"title": original.title, "authors": [author.name for author in original.authors]},
|
{
|
||||||
|
"title": original.title,
|
||||||
|
"authors": [author.name for author in original.authors],
|
||||||
|
},
|
||||||
test_library,
|
test_library,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -743,7 +746,8 @@ class TestDuplicateBooks:
|
|||||||
)
|
)
|
||||||
|
|
||||||
matches = await books_service.find_duplicate_books(
|
matches = await books_service.find_duplicate_books(
|
||||||
{"title": "Building Microservices", "authors": ["Newman, Sam;"]}, test_library
|
{"title": "Building Microservices", "authors": ["Newman, Sam;"]},
|
||||||
|
test_library,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert [match.book_id for match in matches] == [stored.id]
|
assert [match.book_id for match in matches] == [stored.id]
|
||||||
@@ -834,16 +838,22 @@ class TestDuplicateBooks:
|
|||||||
"series": "Foundation",
|
"series": "Foundation",
|
||||||
}
|
}
|
||||||
|
|
||||||
assert await books_service.find_duplicate_books(
|
assert (
|
||||||
|
await books_service.find_duplicate_books(
|
||||||
incoming | {"series_position": "2"}, test_library
|
incoming | {"series_position": "2"}, test_library
|
||||||
) == []
|
)
|
||||||
|
== []
|
||||||
|
)
|
||||||
|
|
||||||
# The same volume, written a little differently, still matches.
|
# The same volume, written a little differently, still matches.
|
||||||
assert len(
|
assert (
|
||||||
|
len(
|
||||||
await books_service.find_duplicate_books(
|
await books_service.find_duplicate_books(
|
||||||
incoming | {"series_position": "1.0"}, test_library
|
incoming | {"series_position": "1.0"}, test_library
|
||||||
)
|
)
|
||||||
) == 1
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
|
||||||
async def test_a_book_is_not_its_own_duplicate(
|
async def test_a_book_is_not_its_own_duplicate(
|
||||||
self, books_service: BookService, test_library: m.Library
|
self, books_service: BookService, test_library: m.Library
|
||||||
@@ -909,7 +919,10 @@ class TestAuthorNames:
|
|||||||
existing row and then collides with it on the unique index.
|
existing row and then collides with it on the unique index.
|
||||||
"""
|
"""
|
||||||
first = await store_book(
|
first = await store_book(
|
||||||
books_service, test_library, title="Building Microservices", authors=["Sam Newman"]
|
books_service,
|
||||||
|
test_library,
|
||||||
|
title="Building Microservices",
|
||||||
|
authors=["Sam Newman"],
|
||||||
)
|
)
|
||||||
second = await store_book(
|
second = await store_book(
|
||||||
books_service,
|
books_service,
|
||||||
@@ -953,7 +966,9 @@ class TestAuthorNames:
|
|||||||
"Franz Kafka.epub" is not a person.
|
"Franz Kafka.epub" is not a person.
|
||||||
"""
|
"""
|
||||||
result = await books_service.create_many_from_files(
|
result = await books_service.create_many_from_files(
|
||||||
BooksCreateFromFiles(files=[upload(EPUB, "Some Unknown Book - Ada Lovelace.epub")]),
|
BooksCreateFromFiles(
|
||||||
|
files=[upload(EPUB, "Some Unknown Book - Ada Lovelace.epub")]
|
||||||
|
),
|
||||||
test_library,
|
test_library,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1014,7 +1029,10 @@ class TestMergeBooks:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""The survivor keeps its own fields unless the caller says otherwise."""
|
"""The survivor keeps its own fields unless the caller says otherwise."""
|
||||||
keep = await store_book(
|
keep = await store_book(
|
||||||
books_service, test_library, title="Building Microservices", authors=["Sam Newman"]
|
books_service,
|
||||||
|
test_library,
|
||||||
|
title="Building Microservices",
|
||||||
|
authors=["Sam Newman"],
|
||||||
)
|
)
|
||||||
fold = await store_book(
|
fold = await store_book(
|
||||||
books_service,
|
books_service,
|
||||||
@@ -1030,7 +1048,11 @@ class TestMergeBooks:
|
|||||||
assert merged.publisher is None
|
assert merged.publisher is None
|
||||||
|
|
||||||
other = await store_book(
|
other = await store_book(
|
||||||
books_service, test_library, title="Monolith", authors=["Sam Newman"], edition=3
|
books_service,
|
||||||
|
test_library,
|
||||||
|
title="Monolith",
|
||||||
|
authors=["Sam Newman"],
|
||||||
|
edition=3,
|
||||||
)
|
)
|
||||||
merged = await books_service.merge_books(
|
merged = await books_service.merge_books(
|
||||||
keep.id, [other.id], test_library, metadata={"edition": 3}
|
keep.id, [other.id], test_library, metadata={"edition": 3}
|
||||||
@@ -1096,10 +1118,14 @@ class TestMergeBooks:
|
|||||||
await books_service.merge_books(keep.id, [fold.id], test_library)
|
await books_service.merge_books(keep.id, [fold.id], test_library)
|
||||||
|
|
||||||
rows = (
|
rows = (
|
||||||
|
(
|
||||||
await books_service.repository.session.execute(
|
await books_service.repository.session.execute(
|
||||||
select(m.BookProgress).where(m.BookProgress.book_id == keep.id)
|
select(m.BookProgress).where(m.BookProgress.book_id == keep.id)
|
||||||
)
|
)
|
||||||
).scalars().all()
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
assert [row.percentage for row in rows] == [0.6]
|
assert [row.percentage for row in rows] == [0.6]
|
||||||
|
|
||||||
@@ -1112,13 +1138,23 @@ class TestMergeBooks:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Both books on one shelf must not leave the survivor linked to it twice."""
|
"""Both books on one shelf must not leave the survivor linked to it twice."""
|
||||||
keep = await store_book(
|
keep = await store_book(
|
||||||
books_service, test_library, title="A", authors=["X"], tags=["Shared", "Only Keep"]
|
books_service,
|
||||||
|
test_library,
|
||||||
|
title="A",
|
||||||
|
authors=["X"],
|
||||||
|
tags=["Shared", "Only Keep"],
|
||||||
)
|
)
|
||||||
fold = await store_book(
|
fold = await store_book(
|
||||||
books_service, test_library, title="B", authors=["X"], tags=["Shared", "Only Fold"]
|
books_service,
|
||||||
|
test_library,
|
||||||
|
title="B",
|
||||||
|
authors=["X"],
|
||||||
|
tags=["Shared", "Only Fold"],
|
||||||
)
|
)
|
||||||
|
|
||||||
shelf = m.BookList(title="Later", user_id=test_user.id, library_id=test_library.id)
|
shelf = m.BookList(
|
||||||
|
title="Later", user_id=test_user.id, library_id=test_library.id
|
||||||
|
)
|
||||||
session.add(shelf)
|
session.add(shelf)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
session.add_all(
|
session.add_all(
|
||||||
@@ -1131,13 +1167,21 @@ class TestMergeBooks:
|
|||||||
|
|
||||||
merged = await books_service.merge_books(keep.id, [fold.id], test_library)
|
merged = await books_service.merge_books(keep.id, [fold.id], test_library)
|
||||||
|
|
||||||
assert sorted(tag.name for tag in merged.tags) == ["Only Fold", "Only Keep", "Shared"]
|
assert sorted(tag.name for tag in merged.tags) == [
|
||||||
|
"Only Fold",
|
||||||
|
"Only Keep",
|
||||||
|
"Shared",
|
||||||
|
]
|
||||||
|
|
||||||
links = (
|
links = (
|
||||||
|
(
|
||||||
await books_service.repository.session.execute(
|
await books_service.repository.session.execute(
|
||||||
select(m.BookListLink).where(m.BookListLink.book_id == keep.id)
|
select(m.BookListLink).where(m.BookListLink.book_id == keep.id)
|
||||||
)
|
)
|
||||||
).scalars().all()
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
assert len(links) == 1
|
assert len(links) == 1
|
||||||
|
|
||||||
async def test_an_identifier_moves_only_under_a_name_the_survivor_lacks(
|
async def test_an_identifier_moves_only_under_a_name_the_survivor_lacks(
|
||||||
|
|||||||
@@ -2,19 +2,12 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from chitai.services import ShelfService
|
from chitai.services import ShelfService
|
||||||
from chitai.database import models as m
|
from chitai.database import models as m
|
||||||
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
from chitai.services.bookshelf import ShelfService
|
|
||||||
from chitai.services import BookService
|
|
||||||
from chitai.database.models.book_list import BookList, BookListLink
|
from chitai.database.models.book_list import BookList, BookListLink
|
||||||
from chitai.database import models as m
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -201,7 +201,9 @@ async def test_importing_twice_creates_nothing(
|
|||||||
]
|
]
|
||||||
|
|
||||||
held_by = [
|
held_by = [
|
||||||
skipped.book_id for skipped in result.skipped if skipped.reason == "already stored"
|
skipped.book_id
|
||||||
|
for skipped in result.skipped
|
||||||
|
if skipped.reason == "already stored"
|
||||||
]
|
]
|
||||||
assert all(book_id is not None for book_id in held_by)
|
assert all(book_id is not None for book_id in held_by)
|
||||||
|
|
||||||
@@ -223,7 +225,9 @@ async def test_a_file_the_catalogue_lists_but_disk_does_not(
|
|||||||
await source.close()
|
await source.close()
|
||||||
|
|
||||||
assert len(result.created) == 1
|
assert len(result.created) == 1
|
||||||
assert [(s.calibre_id, s.reason) for s in result.skipped] == [(2, "no files on disk")]
|
assert [(s.calibre_id, s.reason) for s in result.skipped] == [
|
||||||
|
(2, "no files on disk")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_one_broken_book_does_not_stop_the_import(
|
async def test_one_broken_book_does_not_stop_the_import(
|
||||||
@@ -312,7 +316,11 @@ async def test_shared_authors_and_tags_are_one_row_each(
|
|||||||
1, "One", authors=["Franz Kafka"], tags=["Fiction"], formats={"EPUB": EPUB}
|
1, "One", authors=["Franz Kafka"], tags=["Fiction"], formats={"EPUB": EPUB}
|
||||||
)
|
)
|
||||||
fixture.add_book(
|
fixture.add_book(
|
||||||
2, "Two", authors=["Franz Kafka"], tags=["Fiction"], formats={"EPUB": OTHER_EPUB}
|
2,
|
||||||
|
"Two",
|
||||||
|
authors=["Franz Kafka"],
|
||||||
|
tags=["Fiction"],
|
||||||
|
formats={"EPUB": OTHER_EPUB},
|
||||||
)
|
)
|
||||||
root = fixture.commit()
|
root = fixture.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class TestLibraryServiceCRUD:
|
|||||||
assert library.name == "Test Library"
|
assert library.name == "Test Library"
|
||||||
assert library.root_path == library_path
|
assert library.root_path == library_path
|
||||||
assert library.path_template == "{author}/{title}"
|
assert library.path_template == "{author}/{title}"
|
||||||
assert library.description == None
|
assert library.description is None
|
||||||
assert library.read_only == False
|
assert library.read_only is False
|
||||||
|
|
||||||
# Check if directory was created
|
# Check if directory was created
|
||||||
assert Path(library.root_path).is_dir()
|
assert Path(library.root_path).is_dir()
|
||||||
@@ -56,8 +56,8 @@ class TestLibraryServiceCRUD:
|
|||||||
read_only=False,
|
read_only=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(PermissionError) as exc_info:
|
with pytest.raises(PermissionError):
|
||||||
library = await library_service.create(library_data)
|
await library_service.create(library_data)
|
||||||
|
|
||||||
# Check if directory was created
|
# Check if directory was created
|
||||||
assert not Path(library_path).exists()
|
assert not Path(library_path).exists()
|
||||||
@@ -86,8 +86,8 @@ class TestLibraryServiceCRUD:
|
|||||||
assert library.name == "Test Library"
|
assert library.name == "Test Library"
|
||||||
assert library.root_path == library_path
|
assert library.root_path == library_path
|
||||||
assert library.path_template == "{author}/{title}"
|
assert library.path_template == "{author}/{title}"
|
||||||
assert library.description == None
|
assert library.description is None
|
||||||
assert library.read_only == True
|
assert library.read_only is True
|
||||||
|
|
||||||
async def test_create_library_read_only_nonexistent_path(
|
async def test_create_library_read_only_nonexistent_path(
|
||||||
self, library_service: LibraryService, tmp_path: Path
|
self, library_service: LibraryService, tmp_path: Path
|
||||||
@@ -138,7 +138,7 @@ class TestLibraryServiceCRUD:
|
|||||||
assert library.root_path == "./books"
|
assert library.root_path == "./books"
|
||||||
assert library.path_template == "{author}/{title}"
|
assert library.path_template == "{author}/{title}"
|
||||||
assert library.description is None
|
assert library.description is None
|
||||||
assert library.read_only == False
|
assert library.read_only is False
|
||||||
|
|
||||||
# async def test_delete_library_keep_files(
|
# async def test_delete_library_keep_files(
|
||||||
# self, session: AsyncSession, library_service: LibraryService
|
# self, session: AsyncSession, library_service: LibraryService
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class TestUserServiceAuthentication:
|
|||||||
|
|
||||||
# Create a user with a known password
|
# Create a user with a known password
|
||||||
password = "password123"
|
password = "password123"
|
||||||
user = m.User(email=f"test@example.com", password=password)
|
user = m.User(email="test@example.com", password=password)
|
||||||
|
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -52,7 +52,7 @@ class TestUserServiceAuthentication:
|
|||||||
|
|
||||||
# Create user
|
# Create user
|
||||||
password = "password123"
|
password = "password123"
|
||||||
user = m.User(email=f"test@example.com", password=password)
|
user = m.User(email="test@example.com", password=password)
|
||||||
|
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -85,7 +85,7 @@ class TestUserServiceCRUD:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test getting user by email."""
|
"""Test getting user by email."""
|
||||||
|
|
||||||
user = m.User(email=f"test@example.com", password="password123")
|
user = m.User(email="test@example.com", password="password123")
|
||||||
|
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -102,12 +102,12 @@ class TestUserServiceCRUD:
|
|||||||
"""Test creating a new user with a duplicate email."""
|
"""Test creating a new user with a duplicate email."""
|
||||||
|
|
||||||
# Create first user
|
# Create first user
|
||||||
user = m.User(email=f"test@example.com", password="password123")
|
user = m.User(email="test@example.com", password="password123")
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
# Create second user
|
# Create second user
|
||||||
user = m.User(email=f"test@example.com", password="password12345")
|
user = m.User(email="test@example.com", password="password12345")
|
||||||
|
|
||||||
with pytest.raises(IntegrityError) as exc_info:
|
with pytest.raises(IntegrityError) as exc_info:
|
||||||
session.add(user)
|
session.add(user)
|
||||||
|
|||||||
Reference in New Issue
Block a user