Compare commits

...
Author SHA1 Message Date
stumpylog 8849aff5d9 Chore: Run read-only migration tests through one migration cycle per class
Each TestMigrations test migrates back, seeds, migrates forward and returns to
the latest migration, which is slow. The fulltext query
prefix, sha256 checksum and skip archive file classes only read the migrated
data, so they now opt in to migrate_once: the first test runs the migration,
the rest reuse its state, and the schema is restored and the tables flushed in
tearDownClass.
2026-09-21 11:45:28 -07:00
Trenton H f502cd5e34 Chore: Add shared helpers for granting test permissions and use them (#14200) 2026-09-21 10:57:14 -07:00
Trenton H 99ce6b5db3 Chore: Share the API client fixtures and build test users cheaply (#14199) 2026-09-21 10:57:14 -07:00
Trenton H 2d955e9697 Chore: Randomize test order and seed Faker per run (#14173)
Enables pytest-randomly, which has sat commented out in pyproject.toml
since the Pytest 9 upgrade. Tests now run in a different order every
session, so a test cannot quietly depend on another having run first.
2026-09-21 10:57:13 -07:00
Trenton H f440e8d33c Chore: Move unittest directory setup onto the shared fixture (#14172)
The unittest side of the suite built its temp directory tree with
tempfile.mkdtemp and a manually enabled override_settings, cleaned up only if
tearDown ran. That is now gone. DirectoriesMixin lives alongside the layout it
bridges and does nothing but hand the paperless_dirs fixture to TestCase
subclasses as self.dirs, so both halves of the suite get the same twelve
settings, the same directory shapes and cleanup owned by tmp_path.

The mixin moves to paperless_testing.dirs rather than staying in the documents
test utilities, because modules in paperless and paperless_mail import it
across the app boundary. The thirty-eight consuming modules change only their
import line; self.dirs.scratch_dir and its siblings keep working.
2026-09-21 10:57:12 -07:00
Trenton H 4c264651e8 Chore: State the test directory layout in one place (#14171)
The temp directory layout used by the tests was written out four separate
times: once in the documents conftest, once in the paperless checks tests,
once in a fixture local to the NFC upload tests, and once in the helper
behind the old paperless_environment context manager. Each copy covered a
different subset of the settings, so which directories a test actually got
depended on which copy it happened to reach.

The layout now lives in paperless_testing.dirs. build_paperless_dirs owns
where things go and creates them, dirs_settings maps them onto Django
setting names and is pure, and a paperless_dirs fixture in the root conftest
applies that mapping through pytest-django's settings fixture so every app
can reach it. Tests that need a second environment part way through a test
body use the paperless_environment context manager from the same module,
which expresses the identical layout through override_settings. The three
redundant implementations and the old media settings fixture are gone, and
their consumers now take paperless_dirs.
2026-09-21 10:57:11 -07:00
Trenton H 48d97b78bb Chore: Move model factories to the shared test layer (#14170)
The model factories lived in the documents test package, but three other
apps needed them. The AI, mail and testing suites all reached across an app
boundary to import from documents.tests.factories, which made a private test
package into a shared dependency.

The factories now live in the shared testing package, where cross-app use is
the intended use.
2026-09-21 10:57:11 -07:00
Trenton H 659a0cb2ef Chore: Add a shared test support layer (#14168)
All four Django apps have test code in common, but the only place to put it
was the documents app's own tests package, so paperless, paperless_ai and
paperless_mail each reached across an app boundary to import helpers and
relied on fixtures that were only defined for the documents test path.

This adds a root src/conftest.py holding the fixtures every app needs and an
new src/paperless_testing package for shared helpers a test names
2026-09-21 10:57:11 -07:00
Trenton H d02d1e1711 Chore: Remove unused test helpers (#14166) 2026-09-21 10:57:10 -07:00
dependabot[bot]andstumpylog 3b41810e7b Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 2 updates (#14133)
* Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 2 updates

Bumps the pre-commit-dependencies group with 2 updates in the / directory: [https://github.com/astral-sh/ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit) and [https://github.com/tox-dev/pyproject-fmt](https://github.com/tox-dev/pyproject-fmt).


Updates `https://github.com/astral-sh/ruff-pre-commit` from v0.16.5 to 0.16.7
- [Release notes](https://github.com/astral-sh/ruff-pre-commit/releases)
- [Commits](https://github.com/astral-sh/ruff-pre-commit/compare/v0.16.5...v0.16.7)

Updates `https://github.com/tox-dev/pyproject-fmt` from v2.28.1 to 2.29.4
- [Release notes](https://github.com/tox-dev/pyproject-fmt/releases)
- [Commits](https://github.com/tox-dev/pyproject-fmt/compare/v2.28.1...v2.29.4)

---
updated-dependencies:
- dependency-name: https://github.com/astral-sh/ruff-pre-commit
  dependency-version: 0.16.6
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
- dependency-name: https://github.com/tox-dev/pyproject-fmt
  dependency-version: 2.29.4
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* Runs new formatting

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: stumpylog <797416+stumpylog@users.noreply.github.com>
2026-09-21 07:49:12 -07:00
Zhiyuan Zhengandzhzy0077 12314fcaa8 Fix: ignore invalid EXIF orientation when generating image archives (#14203)
Images with an out-of-spec EXIF orientation value (e.g. 0) fail
conversion with img2pdf.ExifOrientationError, which aborts archive
generation and, during consumption with OCR disabled, fails the whole
document.

Pass rotation=img2pdf.Rotation.ifvalid so invalid orientation values
are ignored while valid values (1, 3, 6, 8) are still applied.

Co-authored-by: zhzy0077 <zhzy0077@users.noreply.github.com>
2026-09-21 07:21:24 -07:00
github-actions[bot] 8705bd510a Changelog v3.2.1 - GHA (#14198)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-20 15:00:56 -07:00
92 changed files with 1353 additions and 1348 deletions
+2
View File
@@ -15,6 +15,8 @@
# Test related
**/.pytest_cache
**/tests
src/paperless_testing
src/conftest.py
**/*.spec.ts
**/htmlcov
# Local folders
+2 -2
View File
@@ -50,12 +50,12 @@ repos:
- 'prettier-plugin-organize-imports@4.3.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.5
rev: v0.16.7
hooks:
- id: ruff-check
- id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.28.1"
rev: "v2.29.4"
hooks:
- id: pyproject-fmt
additional_dependencies: [tomli]
+25
View File
@@ -1,5 +1,30 @@
# Changelog
## paperless-ngx 3.2.1
### Bug Fixes
- Fix: only pass --conf to flower when flowerconfig.py exists [@bitfoo1](https://github.com/bitfoo1) ([#14182](https://github.com/paperless-ngx/paperless-ngx/pull/14182))
- Fix: replace stale mail-fetch overlap check with a self-expiring lock [@stumpylog](https://github.com/stumpylog) ([#14189](https://github.com/paperless-ngx/paperless-ngx/pull/14189))
- Fix: bump ocrmypdf to 17.12 to pick up the ligature text-layer fix [@stumpylog](https://github.com/stumpylog) ([#14190](https://github.com/paperless-ngx/paperless-ngx/pull/14190))
- Fix: rebuild the search index automatically when it is missing Tantivy files [@stumpylog](https://github.com/stumpylog) ([#14180](https://github.com/paperless-ngx/paperless-ngx/pull/14180))
### Dependencies
- Chore(deps): Bump anyio from 4.12.1 to 4.14.2 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#14175](https://github.com/paperless-ngx/paperless-ngx/pull/14175))
### All App Changes
<details>
<summary>4 changes</summary>
- Chore(deps): Bump anyio from 4.12.1 to 4.14.2 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#14175](https://github.com/paperless-ngx/paperless-ngx/pull/14175))
- Fix: replace stale mail-fetch overlap check with a self-expiring lock [@stumpylog](https://github.com/stumpylog) ([#14189](https://github.com/paperless-ngx/paperless-ngx/pull/14189))
- Fix: bump ocrmypdf to 17.12 to pick up the ligature text-layer fix [@stumpylog](https://github.com/stumpylog) ([#14190](https://github.com/paperless-ngx/paperless-ngx/pull/14190))
- Fix: rebuild the search index automatically when it is missing Tantivy files [@stumpylog](https://github.com/stumpylog) ([#14180](https://github.com/paperless-ngx/paperless-ngx/pull/14180))
</details>
## paperless-ngx 3.2.0
### Features / Enhancements
+1
View File
@@ -150,6 +150,7 @@ pnpm ng build --configuration production
is loaded as well. However, the tests rely on the default
configuration. This is not ideal. But for now, make sure no settings
except for DEBUG are overridden when testing.
- Tests run in a random order each session, so that one test cannot quietly depend on another having run first. The seed is printed at the top of the run; pass `--randomly-seed=<seed>` to replay that exact order, or `--randomly-seed=last` to repeat the previous run.
!!! note
+10 -5
View File
@@ -1,7 +1,9 @@
[project]
name = "paperless-ngx"
version = "3.2.1"
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
description = """\
A community-supported supercharged document management system: scan, index and archive all your physical documents\
"""
readme = "README.md"
requires-python = ">=3.11"
classifiers = [
@@ -10,6 +12,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
]
# TODO: Move certain things to groups and then utilize that further
# This will allow testing to not install a webserver, mysql, etc
@@ -117,7 +120,7 @@ testing = [
"pytest-env~=1.7.0",
"pytest-httpx",
"pytest-mock~=3.15.1",
# "pytest-randomly~=4.0.1",
"pytest-randomly~=5.0.0",
"pytest-rerunfailures~=16.4",
"pytest-sugar",
"pytest-xdist~=3.8.0",
@@ -271,9 +274,9 @@ plugins = [
]
[tool.pyrefly]
baseline = ".pyrefly-baseline.json"
python-platform = "linux"
search-path = [ "src" ]
baseline = ".pyrefly-baseline.json"
[tool.django-stubs]
django_settings_module = "paperless.settings"
@@ -334,13 +337,15 @@ source = [
omit = [
"*/tests/*",
"manage.py",
"paperless/wsgi.py",
"paperless/auth.py",
"paperless/wsgi.py",
"src/conftest.py",
"src/paperless_testing/*",
]
[tool.coverage.report]
exclude_also = [
"if settings.AUDIT_LOG_ENABLED:",
"if AUDIT_LOG_ENABLED:",
"if settings.AUDIT_LOG_ENABLED:",
"if TYPE_CHECKING:",
]
+126
View File
@@ -0,0 +1,126 @@
"""Fixtures available to every Paperless-ngx app.
Loaded automatically for every test path. Keep module-scope imports minimal:
this file is imported for every session, so anything heavy belongs inside
the fixture body that needs it.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
if TYPE_CHECKING:
from collections.abc import Generator
from pathlib import Path
from django.contrib.auth.models import User
from pytest_django.fixtures import Settings
from rest_framework.test import APIClient
from paperless_testing.dirs import PaperlessDirs
@pytest.fixture(scope="session", autouse=True)
def faker_session_locale() -> str:
"""Pin Faker's locale so generated data does not follow the host locale.
The seed itself is left to pytest-randomly, which derives one per run.
"""
return "en_US"
@pytest.fixture(autouse=True)
def _clear_content_type_caches() -> None:
"""Clear Django's ContentType cache and guardian's lru_cache before each test.
Tests that delete and reinsert ContentType/Permission rows (e.g. the
importer) corrupt both caches. Without this fixture a subsequent test on
the same xdist worker sees stale ContentType objects and guardian raises
MixedContentTypeError.
"""
from django.contrib.contenttypes.models import ContentType
from guardian.shortcuts import clear_ct_cache
ContentType.objects.clear_cache()
clear_ct_cache()
@pytest.fixture(autouse=True)
def _clear_django_caches() -> None:
"""Clear every configured cache before each test.
Cached values outlive the test that wrote them: the classifier keys its
vectorized content on a hash of the content itself, so a second test
generating the same fixture data takes the cache-hit path and never calls
the code it is asserting against.
"""
from django.core.cache import caches
for cache in caches.all(initialized_only=False):
cache.clear()
@pytest.fixture
def paperless_dirs(
tmp_path: Path,
settings: Settings,
) -> Generator[PaperlessDirs, None, None]:
"""The standard temp directory layout, applied to Django settings."""
from documents.search import reset_backend
from paperless_testing.dirs import build_paperless_dirs
from paperless_testing.dirs import dirs_settings
dirs = build_paperless_dirs(tmp_path)
for name, value in dirs_settings(dirs).items():
setattr(settings, name, value)
# Not directory settings, but they are needed alongside the layout by the
# sanity checker tests.
settings.IGNORABLE_FILES = {".DS_Store", "Thumbs.db", "desktop.ini"}
settings.APP_LOGO = ""
reset_backend()
yield dirs
reset_backend()
@pytest.fixture
def rest_api_client() -> APIClient:
"""The basic DRF APIClient, unauthenticated."""
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture
def regular_user(db: None) -> User:
"""Unprivileged user for permission boundary tests."""
from paperless_testing.factories import UserFactory
return UserFactory(username="regular")
@pytest.fixture
def admin_client(rest_api_client: APIClient, admin_user: User) -> APIClient:
"""Admin client pre-authenticated and sending the v10 Accept header."""
rest_api_client.force_authenticate(user=admin_user)
rest_api_client.credentials(HTTP_ACCEPT="application/json; version=10")
return rest_api_client
@pytest.fixture
def v9_client(rest_api_client: APIClient, admin_user: User) -> APIClient:
"""Admin client pre-authenticated and sending the v9 Accept header."""
rest_api_client.force_authenticate(user=admin_user)
rest_api_client.credentials(HTTP_ACCEPT="application/json; version=9")
return rest_api_client
@pytest.fixture
def user_client(rest_api_client: APIClient, regular_user: User) -> APIClient:
"""Regular-user client pre-authenticated and sending the v10 Accept header."""
rest_api_client.force_authenticate(user=regular_user)
rest_api_client.credentials(HTTP_ACCEPT="application/json; version=10")
return rest_api_client
+7 -119
View File
@@ -1,34 +1,17 @@
import shutil
import zoneinfo
from collections.abc import Generator
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
import filelock
import pytest
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from guardian.shortcuts import clear_ct_cache
from pytest_django.fixtures import Settings
from rest_framework.test import APIClient
from documents.tests.factories import DocumentFactory
UserModelT = get_user_model()
from paperless_testing.factories import DocumentFactory
if TYPE_CHECKING:
from documents.models import Document
@dataclass(frozen=True, slots=True)
class PaperlessDirs:
"""Standard Paperless-ngx directory layout for tests."""
media: Path
originals: Path
archive: Path
thumbnails: Path
from paperless_testing.dirs import PaperlessDirs
@pytest.fixture(scope="session")
@@ -37,52 +20,24 @@ def samples_dir() -> Path:
return Path(__file__).parent / "samples" / "documents"
@pytest.fixture()
def paperless_dirs(tmp_path: Path) -> PaperlessDirs:
"""Create and return the directory structure for testing."""
media = tmp_path / "media"
dirs = PaperlessDirs(
media=media,
originals=media / "documents" / "originals",
archive=media / "documents" / "archive",
thumbnails=media / "documents" / "thumbnails",
)
for d in (dirs.originals, dirs.archive, dirs.thumbnails):
d.mkdir(parents=True)
return dirs
@pytest.fixture()
def _media_settings(paperless_dirs: PaperlessDirs, settings) -> None:
"""Configure Django settings to point at temp directories."""
settings.MEDIA_ROOT = paperless_dirs.media
settings.ORIGINALS_DIR = paperless_dirs.originals
settings.ARCHIVE_DIR = paperless_dirs.archive
settings.THUMBNAIL_DIR = paperless_dirs.thumbnails
settings.MEDIA_LOCK = paperless_dirs.media / "media.lock"
settings.IGNORABLE_FILES = {".DS_Store", "Thumbs.db", "desktop.ini"}
settings.APP_LOGO = ""
@pytest.fixture()
def sample_doc(
paperless_dirs: PaperlessDirs,
_media_settings: None,
paperless_dirs: "PaperlessDirs",
samples_dir: Path,
) -> "Document":
"""Create a document with valid files and matching checksums."""
with filelock.FileLock(paperless_dirs.media / "media.lock"):
with filelock.FileLock(paperless_dirs.media_lock):
shutil.copy(
samples_dir / "originals" / "0000001.pdf",
paperless_dirs.originals / "0000001.pdf",
paperless_dirs.originals_dir / "0000001.pdf",
)
shutil.copy(
samples_dir / "archive" / "0000001.pdf",
paperless_dirs.archive / "0000001.pdf",
paperless_dirs.archive_dir / "0000001.pdf",
)
shutil.copy(
samples_dir / "thumbnails" / "0000001.webp",
paperless_dirs.thumbnails / "0000001.webp",
paperless_dirs.thumbnail_dir / "0000001.webp",
)
return DocumentFactory(
@@ -117,73 +72,6 @@ def _search_index(
reset_backend()
@pytest.fixture()
def settings_timezone(settings: Settings) -> zoneinfo.ZoneInfo:
return zoneinfo.ZoneInfo(settings.TIME_ZONE)
@pytest.fixture
def rest_api_client():
"""
The basic DRF ApiClient
"""
yield APIClient()
@pytest.fixture()
def regular_user(django_user_model: type[UserModelT]) -> UserModelT:
"""Unprivileged authenticated user for permission boundary tests."""
return django_user_model.objects.create_user(username="regular", password="regular")
@pytest.fixture()
def admin_client(rest_api_client: APIClient, admin_user: UserModelT) -> APIClient:
"""Admin client pre-authenticated and sending the v10 Accept header."""
rest_api_client.force_authenticate(user=admin_user)
rest_api_client.credentials(HTTP_ACCEPT="application/json; version=10")
return rest_api_client
@pytest.fixture()
def v9_client(rest_api_client: APIClient, admin_user: UserModelT) -> APIClient:
"""Admin client pre-authenticated and sending the v9 Accept header."""
rest_api_client.force_authenticate(user=admin_user)
rest_api_client.credentials(HTTP_ACCEPT="application/json; version=9")
return rest_api_client
@pytest.fixture()
def user_client(rest_api_client: APIClient, regular_user: UserModelT) -> APIClient:
"""Regular-user client pre-authenticated and sending the v10 Accept header."""
rest_api_client.force_authenticate(user=regular_user)
rest_api_client.credentials(HTTP_ACCEPT="application/json; version=10")
return rest_api_client
@pytest.fixture(autouse=True)
def _clear_content_type_caches() -> None:
"""Clear Django's ContentType cache and guardian's lru_cache before each test.
Tests that delete and reinsert ContentType/Permission rows (e.g. the
importer) corrupt both caches. Without this fixture a subsequent test on
the same xdist worker sees stale ContentType objects and guardian raises
MixedContentTypeError.
"""
ContentType.objects.clear_cache()
clear_ct_cache()
@pytest.fixture(scope="session", autouse=True)
def faker_session_locale():
"""Set Faker locale for reproducibility."""
return "en_US"
@pytest.fixture(scope="session", autouse=True)
def faker_seed():
return 12345
@pytest.fixture
def indexed_document(_search_index: None) -> "Document":
"""One searchable document, for tests about what the search endpoint
@@ -15,11 +15,11 @@ from rich.console import Console
from documents.management.commands.document_sanity_checker import Command
from documents.sanity_checker import SanityCheckMessages
from documents.tests.factories import DocumentFactory
from paperless_testing.factories import DocumentFactory
if TYPE_CHECKING:
from documents.models import Document
from documents.tests.conftest import PaperlessDirs
from paperless_testing.dirs import PaperlessDirs
def _render_to_string(messages: SanityCheckMessages) -> str:
@@ -71,7 +71,7 @@ class TestRenderResultsWithIssues:
assert "INFO" in output
assert "No OCR data" in output
@pytest.mark.usefixtures("_media_settings")
@pytest.mark.usefixtures("paperless_dirs")
def test_global_message(self) -> None:
msgs = SanityCheckMessages()
msgs.warning(None, "Orphaned file: /tmp/stray.pdf")
@@ -87,7 +87,7 @@ class TestRenderResultsWithIssues:
assert "Thumbnail missing" in output
assert "Checksum mismatch" in output
@pytest.mark.usefixtures("_media_settings")
@pytest.mark.usefixtures("paperless_dirs")
def test_unknown_doc_pk(self) -> None:
msgs = SanityCheckMessages()
msgs.error(99999, "Ghost document")
@@ -184,7 +184,6 @@ class TestDocumentSanityCheckerCommand:
assert "ERROR" in output
assert "Original of document does not exist" in output
@pytest.mark.usefixtures("_media_settings")
def test_checksum_mismatch(self, paperless_dirs: PaperlessDirs) -> None:
"""Lightweight document with zero-byte files triggers checksum mismatch."""
doc = DocumentFactory(
+1 -1
View File
@@ -9,7 +9,7 @@ from documents.search._backend import TantivyBackend
from documents.search._backend import reset_backend
from documents.search._schema import build_schema
from documents.search._tokenizer import register_tokenizers
from documents.tests.factories import DocumentFactory
from paperless_testing.factories import DocumentFactory
if TYPE_CHECKING:
from collections.abc import Callable
@@ -14,7 +14,6 @@ from typing import TYPE_CHECKING
import pytest
import time_machine
from django.contrib.auth.models import User
from documents.models import CustomField
from documents.models import CustomFieldInstance
@@ -22,7 +21,8 @@ from documents.models import DocumentType
from documents.models import Note
from documents.models import StoragePath
from documents.search._query import parse_user_query
from documents.tests.factories import DocumentFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import UserFactory
if TYPE_CHECKING:
from collections.abc import Callable
@@ -148,7 +148,7 @@ class TestJsonSubpaths:
THEN:
- Only the document with alice's note matches
"""
alice = User.objects.create_user(username="alice")
alice = UserFactory(username="alice")
doc_with_note = DocumentFactory(
title="Has note",
content="x",
+13 -14
View File
@@ -3,10 +3,8 @@ from pathlib import Path
import pytest
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.db import connection
from django.test.utils import CaptureQueriesContext
from guardian.shortcuts import assign_perm
from pytest_mock import MockerFixture
from documents.models import CustomField
@@ -19,11 +17,12 @@ from documents.search._backend import WriteBatch
from documents.search._backend import get_backend
from documents.search._backend import reset_backend
from documents.signals.handlers import add_to_index
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentFactory
from documents.tests.factories import DocumentTypeFactory
from documents.tests.factories import TagFactory
from documents.tests.factories import UserFactory
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import DocumentTypeFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_object
pytestmark = [pytest.mark.search, pytest.mark.django_db]
@@ -189,7 +188,7 @@ class TestAddOrUpdateIds:
pk=1,
owner=owner,
)
assign_perm("view_document", user, doc)
grant_object(user, doc, "view_document")
with backend.batch_update() as batch:
batch.add_or_update_ids([doc.pk])
@@ -209,7 +208,7 @@ class TestAddOrUpdateIds:
pk=1,
owner=owner,
)
assign_perm("view_document", group, doc)
grant_object(group, doc, "view_document")
with backend.batch_update() as batch:
batch.add_or_update_ids([doc.pk])
@@ -763,8 +762,8 @@ class TestSearchIds:
def test_respects_permission_filter(self, backend: TantivyBackend) -> None:
"""search_ids must respect user permission filtering."""
owner = User.objects.create_user("ids_owner")
other = User.objects.create_user("ids_other")
owner = UserFactory(username="ids_owner")
other = UserFactory(username="ids_other")
doc = Document.objects.create(
title="private doc",
content="secret keyword",
@@ -843,7 +842,7 @@ class TestRebuild:
content="group secret keyword",
owner=owner,
)
assign_perm("view_document", group, doc)
grant_object(group, doc, "view_document")
backend.rebuild(Document.objects.all())
@@ -1071,7 +1070,7 @@ class TestFieldHandling:
def test_notes_include_user_information(self, backend: TantivyBackend) -> None:
"""Notes must be indexed with user information when available for structured queries."""
user = User.objects.create_user("notewriter")
user = UserFactory(username="notewriter")
doc = Document.objects.create(
title="Doc with notes",
content="test",
@@ -1173,7 +1172,7 @@ class TestHighlightHits:
notes.note: prefix so the query targets notes content directly, but
the snippet is generated from notes_text which stores the same text.
"""
user = User.objects.create_user("hl_noteuser")
user = UserFactory(username="hl_noteuser")
doc = Document.objects.create(
title="Doc with matching note",
content="unrelated content",
@@ -27,7 +27,7 @@ import time_machine
from documents.models import Note
from documents.models import Tag
from documents.search._errors import InvalidDateQuery
from documents.tests.factories import DocumentFactory
from paperless_testing.factories import DocumentFactory
if TYPE_CHECKING:
from collections.abc import Callable
@@ -29,7 +29,7 @@ from rest_framework import status
from documents.search._backend import SearchMode
from documents.search._query import parse_simple_text_highlight_query
from documents.tests.factories import DocumentFactory
from paperless_testing.factories import DocumentFactory
if TYPE_CHECKING:
from rest_framework.test import APIClient
@@ -17,12 +17,12 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from django.contrib.auth.models import User
from documents.models import CustomField
from documents.models import CustomFieldInstance
from documents.models import Note
from documents.tests.factories import DocumentFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import UserFactory
if TYPE_CHECKING:
from collections.abc import Callable
@@ -53,7 +53,7 @@ class TestBareJsonFieldPrefixes:
decoy's content match does not resurface through a demoted
text search
"""
alice = User.objects.create_user(username="alice")
alice = UserFactory(username="alice")
with_note = DocumentFactory(title="Has note", content="x")
Note.objects.create(document=with_note, user=alice, note="crocodile")
backend.add_or_update(with_note)
@@ -116,7 +116,7 @@ class TestBareJsonFieldPrefixes:
document; the default-subpath resolution for the bare
prefix does not interfere with explicit subpath addressing
"""
bob = User.objects.create_user(username="bob")
bob = UserFactory(username="bob")
doc = DocumentFactory(title="Bob note", content="x")
Note.objects.create(document=doc, user=bob, note="remark")
backend.add_or_update(doc)
@@ -20,7 +20,6 @@ from typing import TYPE_CHECKING
import pytest
import tantivy
from django.contrib.auth.models import User
from whoosh_compat import FieldKind
from documents.models import CustomField
@@ -28,6 +27,7 @@ from documents.models import CustomFieldInstance
from documents.models import Document
from documents.models import Note
from documents.search._fields import PUBLIC_FIELDS
from paperless_testing.factories import UserFactory
if TYPE_CHECKING:
from documents.search._backend import TantivyBackend
@@ -49,7 +49,7 @@ class TestJsonSubpathsAreWrittenAtIndexTime:
- Every subpath PUBLIC_FIELDS declares for notes/custom_fields
is present as a key in the document's stored JSON payload
"""
user = User.objects.create_user(username="completeness-user")
user = UserFactory(username="completeness-user")
field = CustomField.objects.create(
name="Completeness Field",
data_type=CustomField.FieldDataType.STRING,
@@ -15,7 +15,7 @@ from documents.search._backend import SearchIndexLockError
from documents.search._backend import TantivyBackend
from documents.tasks import index_document
from documents.tasks import remove_document_from_index
from documents.tests.factories import DocumentFactory
from paperless_testing.factories import DocumentFactory
if TYPE_CHECKING:
from collections.abc import Generator
@@ -8,6 +8,7 @@ pytestmark = pytest.mark.search
class TestMigrateFulltextQueryFieldPrefixes(TestMigrations):
migrate_from = "0016_sha256_checksums"
migrate_to = "0017_migrate_fulltext_query_field_prefixes"
migrate_once = True
def setUpBeforeMigration(self, apps) -> None:
User = apps.get_model("auth", "User")
@@ -18,13 +18,14 @@ from typing import TYPE_CHECKING
import pytest
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from guardian.shortcuts import assign_perm
from documents.models import Correspondent
from documents.models import Document
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_object
if TYPE_CHECKING:
from documents.search._backend import TantivyBackend
@@ -34,22 +35,22 @@ pytestmark = [pytest.mark.search, pytest.mark.django_db]
@pytest.fixture
def owner() -> User:
return User.objects.create_user(username="owner")
return UserFactory(username="owner")
@pytest.fixture
def stranger() -> User:
return User.objects.create_user(username="stranger")
return UserFactory(username="stranger")
@pytest.fixture
def viewer() -> User:
return User.objects.create_user(username="viewer")
return UserFactory(username="viewer")
@pytest.fixture
def group_member() -> User:
user = User.objects.create_user(username="group_member")
user = UserFactory(username="group_member")
user.groups.add(Group.objects.create(name="accounting"))
return user
@@ -127,7 +128,7 @@ class TestPermissionFilteringOnIndexedDocuments:
checksum="perm-shared-user",
owner=owner,
)
assign_perm("view_document", viewer, doc)
grant_object(viewer, doc, "view_document")
backend.add_or_update(doc)
assert backend.search_ids("invoice", user=viewer) == [doc.pk]
@@ -157,7 +158,7 @@ class TestPermissionFilteringOnIndexedDocuments:
checksum="perm-shared-group",
owner=owner,
)
assign_perm("view_document", group_member.groups.first(), doc)
grant_object(group_member.groups.first(), doc, "view_document")
backend.add_or_update(doc)
assert backend.search_ids("invoice", user=group_member) == [doc.pk]
+4 -4
View File
@@ -16,11 +16,11 @@ from documents.models import Document
from documents.models import Tag
from documents.search import get_backend
from documents.search import reset_backend
from documents.tests.factories import DocumentFactory
from documents.tests.factories import TagFactory
from documents.tests.factories import UserFactory
from documents.tests.utils import DirectoriesMixin
from paperless.admin import PaperlessUserAdmin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
@pytest.fixture
+4 -4
View File
@@ -3,7 +3,6 @@ from io import BytesIO
from pathlib import Path
from unittest.mock import patch
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import override_settings
from PIL import Image
@@ -11,10 +10,11 @@ from PIL.PngImagePlugin import PngInfo
from rest_framework import status
from rest_framework.test import APITestCase
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import read_streaming_response
from paperless.models import ApplicationConfiguration
from paperless.models import ColorConvertChoices
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
class TestApiAppConfig(DirectoriesMixin, APITestCase):
@@ -23,7 +23,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
user = User.objects.create_superuser(username="temp_admin")
user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=user)
def test_api_get_config(self) -> None:
@@ -267,7 +267,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
THEN:
- old app_logo file is deleted
"""
admin = User.objects.create_superuser(username="admin")
admin = UserFactory(username="admin", superuser=True)
self.client.force_login(user=admin)
response = self.client.get("/logo/")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@@ -4,8 +4,6 @@ import json
import shutil
import zipfile
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import override_settings
from django.utils import timezone
from rest_framework import status
@@ -14,9 +12,11 @@ from rest_framework.test import APITestCase
from documents.models import Correspondent
from documents.models import Document
from documents.models import DocumentType
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import SampleDirMixin
from documents.tests.utils import read_streaming_response
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
@@ -25,7 +25,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
self.doc1 = Document.objects.create(title="unrelated", checksum="A")
@@ -326,10 +326,8 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
)
def test_download_insufficient_permissions(self) -> None:
user = User.objects.create_user(username="temp_user")
user.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
user = UserFactory(username="temp_user")
grant_global(user, "view_document")
self.client.force_authenticate(user=user)
self.doc2.owner = self.user
+26 -34
View File
@@ -2,10 +2,8 @@ import json
from unittest import mock
from auditlog.models import LogEntry
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import override_settings
from guardian.shortcuts import assign_perm
from rest_framework import status
from rest_framework.test import APITestCase
@@ -15,14 +13,18 @@ from documents.models import Document
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_all_global
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
class TestBulkEditAPI(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
user = User.objects.create_superuser(username="temp_admin")
user = UserFactory(username="temp_admin", superuser=True)
self.user = user
self.client.force_authenticate(user=user)
@@ -284,9 +286,9 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
m,
) -> None:
self.setup_mock(m, "modify_custom_fields")
user = User.objects.create_user(username="doc-owner")
user.user_permissions.add(Permission.objects.get(codename="change_document"))
other_user = User.objects.create_user(username="other-user")
user = UserFactory(username="doc-owner")
grant_global(user, "change_document")
other_user = UserFactory(username="other-user")
source_doc = Document.objects.create(
checksum="source",
title="Source",
@@ -787,10 +789,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.set_storage_path")
def test_api_bulk_edit_with_all_true_resolves_owned_duplicates(self, m) -> None:
self.setup_mock(m, "set_storage_path")
user = User.objects.create_user(username="duplicate-owner")
user.user_permissions.add(
Permission.objects.get(codename="change_document"),
)
user = UserFactory(username="duplicate-owner")
grant_global(user, "change_document")
first_duplicate = Document.objects.create(
checksum="owned-duplicate",
title="First duplicate",
@@ -1178,7 +1178,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
user1 = User.objects.create(username="user1")
self.client.force_authenticate(user=user1)
assign_perm("view_document", user1, self.doc2)
grant_object(user1, self.doc2, "view_document")
response = self.client.post(
"/api/documents/selection_data/",
@@ -1188,9 +1188,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
user1.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_global(user1, "view_document")
user1 = User.objects.get(pk=user1.pk)
self.client.force_authenticate(user=user1)
response = self.client.post(
@@ -1533,7 +1531,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.doc1.owner = User.objects.get(username="temp_admin")
self.doc1.save()
user1 = User.objects.create(username="user1")
user1.user_permissions.add(*Permission.objects.all())
grant_all_global(user1)
user1.save()
self.client.force_authenticate(user=user1)
@@ -1587,8 +1585,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.doc1.owner = User.objects.get(username="temp_admin")
self.doc1.save()
user1 = User.objects.create(username="user1")
assign_perm("view_document", user1, self.doc1)
user1.user_permissions.add(*Permission.objects.all())
grant_object(user1, self.doc1, "view_document")
grant_all_global(user1)
user1.save()
self.client.force_authenticate(user=user1)
@@ -1609,7 +1607,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
m.assert_not_called()
self.assertEqual(response.content, b"Insufficient permissions")
assign_perm("change_document", user1, self.doc1)
grant_object(user1, self.doc1, "change_document")
response = self.client.post(
"/api/documents/bulk_edit/",
@@ -1819,7 +1817,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.doc1.owner = User.objects.get(username="temp_admin")
self.doc1.save()
user1 = User.objects.create(username="user1")
user1.user_permissions.add(*Permission.objects.all())
grant_all_global(user1)
user1.save()
self.client.force_authenticate(user=user1)
@@ -1880,7 +1878,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.doc1.owner = User.objects.get(username="temp_admin")
self.doc1.save()
user1 = User.objects.create(username="user1")
user1.user_permissions.add(*Permission.objects.all())
grant_all_global(user1)
user1.save()
self.client.force_authenticate(user=user1)
@@ -1919,11 +1917,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.views.bulk_edit.merge")
def test_merge_and_delete_requires_change_permission(self, m) -> None:
self.setup_mock(m, "merge")
user = User.objects.create_user(username="no-change")
user.user_permissions.add(
Permission.objects.get(codename="add_document"),
Permission.objects.get(codename="delete_document"),
)
user = UserFactory(username="no-change")
grant_global(user, "add_document", "delete_document")
self.client.force_authenticate(user=user)
response = self.client.post(
@@ -2310,7 +2305,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.doc1.owner = User.objects.get(username="temp_admin")
self.doc1.save()
user1 = User.objects.create(username="user1")
user1.user_permissions.add(*Permission.objects.all())
grant_all_global(user1)
user1.save()
self.client.force_authenticate(user=user1)
@@ -2345,7 +2340,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.views.bulk_edit.edit_pdf")
def test_edit_pdf_update_requires_change_permission(self, m) -> None:
self.setup_mock(m, "edit_pdf")
user = User.objects.create_user(username="no-change")
user = UserFactory(username="no-change")
self.client.force_authenticate(user=user)
response = self.client.post(
@@ -2372,11 +2367,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
) -> None:
self.setup_mock(edit_pdf_mock, "edit_pdf")
self.setup_mock(remove_password_mock, "remove_password")
user = User.objects.create_user(username="no-delete")
user.user_permissions.add(
Permission.objects.get(codename="add_document"),
Permission.objects.get(codename="change_document"),
)
user = UserFactory(username="no-delete")
grant_global(user, "add_document", "change_document")
self.client.force_authenticate(user=user)
cases = [
@@ -2463,7 +2455,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.doc1.owner = User.objects.get(username="temp_admin")
self.doc1.save()
user1 = User.objects.create(username="user1")
user1.user_permissions.add(*Permission.objects.all())
grant_all_global(user1)
user1.save()
self.client.force_authenticate(user=user1)
+6 -6
View File
@@ -4,20 +4,22 @@ from typing import TYPE_CHECKING
from unittest import mock
import pytest
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APIClient
from rest_framework.test import APITestCase
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
if TYPE_CHECKING:
from django.contrib.auth.models import User
from pytest_mock import MockerFixture
class TestChatStreamingViewInputValidation(APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
def _mock_ai_enabled(self) -> mock.MagicMock:
@@ -113,9 +115,7 @@ class TestChatStreamingViewUnrestrictedFlag:
needs to reach the view at all. Model-level only: says nothing
about which documents (if any) this user can actually see.
"""
regular_user.user_permissions.add(
*Permission.objects.filter(codename="view_document"),
)
grant_global(regular_user, "view_document")
return user_client
@pytest.mark.parametrize(
+13 -16
View File
@@ -4,23 +4,24 @@ from unittest import mock
from unittest.mock import ANY
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import override_settings
from guardian.shortcuts import assign_perm
from rest_framework import status
from rest_framework.test import APITestCase
from documents.models import CustomField
from documents.models import CustomFieldInstance
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
ENDPOINT = "/api/custom_fields/"
def setUp(self) -> None:
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
return super().setUp()
@@ -1174,11 +1175,9 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
def test_documentlink_patch_requires_change_permission_on_target_documents(
self,
) -> None:
source_owner = User.objects.create_user(username="source-owner")
source_owner.user_permissions.add(
Permission.objects.get(codename="change_document"),
)
other_user = User.objects.create_user(username="other-user")
source_owner = UserFactory(username="source-owner")
grant_global(source_owner, "change_document")
other_user = UserFactory(username="other-user")
source_doc = Document.objects.create(
title="Source",
@@ -1221,11 +1220,9 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
def test_documentlink_patch_allowed_with_change_permission_on_target_documents(
self,
) -> None:
source_owner = User.objects.create_user(username="source-owner")
source_owner.user_permissions.add(
Permission.objects.get(codename="change_document"),
)
other_user = User.objects.create_user(username="other-user")
source_owner = UserFactory(username="source-owner")
grant_global(source_owner, "change_document")
other_user = UserFactory(username="other-user")
source_doc = Document.objects.create(
title="Source",
@@ -1244,7 +1241,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
data_type=CustomField.FieldDataType.DOCUMENTLINK,
)
assign_perm("change_document", source_owner, target_doc)
grant_object(source_owner, target_doc, "change_document")
self.client.force_authenticate(user=source_owner)
resp = self.client.patch(
@@ -1337,7 +1334,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
self.assertEqual(results[0]["document_count"], 1)
# Test as user without access to the document
non_superuser = User.objects.create_user(username="non_superuser")
non_superuser = UserFactory(username="non_superuser")
non_superuser.user_permissions.add(
*Permission.objects.exclude(codename="view_global_statistics"),
)
@@ -5,8 +5,6 @@ from typing import TYPE_CHECKING
from unittest import mock
from auditlog.models import LogEntry # type: ignore[import-untyped]
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase as DjangoTestCase
@@ -18,10 +16,12 @@ from documents.data_models import DocumentSource
from documents.filters import EffectiveContentFilter
from documents.filters import TitleContentFilter
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import read_streaming_response
from documents.versioning import annotate_effective_content
from documents.views import DocumentSelectionMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
if TYPE_CHECKING:
from pathlib import Path
@@ -31,7 +31,7 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
def _make_pdf_upload(self, name: str = "version.pdf") -> SimpleUploadedFile:
@@ -89,11 +89,9 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
def test_root_endpoint_returns_403_when_user_lacks_permission(self) -> None:
owner = User.objects.create_user(username="owner")
viewer = User.objects.create_user(username="viewer")
viewer.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
owner = UserFactory(username="owner")
viewer = UserFactory(username="viewer")
grant_global(viewer, "view_document")
root = Document.objects.create(
title="root",
checksum="root",
@@ -283,11 +281,9 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
self.assertEqual(mock_backend.add_or_update.call_args[0][0].id, root.id)
def test_delete_version_returns_403_without_permission(self) -> None:
owner = User.objects.create_user(username="owner")
other = User.objects.create_user(username="other")
other.user_permissions.add(
Permission.objects.get(codename="delete_document"),
)
owner = UserFactory(username="owner")
other = UserFactory(username="other")
grant_global(other, "delete_document")
root = Document.objects.create(
title="root",
checksum="root",
@@ -371,11 +367,9 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
self.assertTrue(resp.data["is_root"])
def test_update_version_label_returns_403_without_permission(self) -> None:
owner = User.objects.create_user(username="owner")
other = User.objects.create_user(username="other")
other.user_permissions.add(
Permission.objects.get(codename="change_document"),
)
owner = UserFactory(username="owner")
other = UserFactory(username="other")
grant_global(other, "change_document")
root = Document.objects.create(
title="root",
checksum="root",
@@ -553,11 +547,9 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
def test_metadata_returns_403_when_user_lacks_permission(self) -> None:
owner = User.objects.create_user(username="owner")
other = User.objects.create_user(username="other")
other.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
owner = UserFactory(username="owner")
other = UserFactory(username="other")
grant_global(other, "view_document")
doc = Document.objects.create(
title="root",
checksum="root",
@@ -653,8 +645,8 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
def test_update_version_returns_403_without_permission(self) -> None:
owner = User.objects.create_user(username="owner")
other = User.objects.create_user(username="other")
owner = UserFactory(username="owner")
other = UserFactory(username="other")
root = Document.objects.create(
title="root",
checksum="root",
@@ -672,8 +664,8 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
def test_update_version_requires_global_change_permission(self) -> None:
user = User.objects.create_user(username="add-only")
user.user_permissions.add(Permission.objects.get(codename="add_document"))
user = UserFactory(username="add-only")
grant_global(user, "add_document")
root = Document.objects.create(
title="root",
checksum="root",
@@ -978,7 +970,7 @@ class TestVersionAwareFilters(DjangoTestCase):
superseded content -- selecting documents the list view, filtered by
the same term, does not show.
"""
user = User.objects.create_superuser(username="bulk_selection")
user = UserFactory(username="bulk_selection", superuser=True)
selected = DocumentSelectionMixin()._resolve_document_ids(
user=user,
@@ -1005,7 +997,7 @@ class TestBulkSelectionExcludesVersions(DjangoTestCase):
"Select all matching" reconstructs the document list, which never
contains version documents as rows of their own.
"""
user = User.objects.create_superuser(username="bulk_versions")
user = UserFactory(username="bulk_versions", superuser=True)
root = Document.objects.create(
title="shared-title root",
checksum="bulk-root",
+86 -99
View File
@@ -23,7 +23,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import DataError
from django.test import override_settings
from django.utils import timezone
from guardian.shortcuts import assign_perm
from rest_framework import status
from rest_framework.test import APITestCase
@@ -48,18 +47,22 @@ from documents.models import Workflow
from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from documents.signals.handlers import run_workflows
from documents.tests.factories import DocumentFactory
from documents.tests.factories import TagFactory
from documents.tests.utils import ConsumeTaskMixin
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import read_streaming_response
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_all_global
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
cache.clear()
@@ -357,10 +360,10 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
with Path(filename).open("wb") as f:
f.write(content)
user1 = User.objects.create_user(username="test1")
user2 = User.objects.create_user(username="test2")
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
user2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
user1 = UserFactory(username="test1")
user2 = UserFactory(username="test2")
grant_global(user1, "view_document")
grant_global(user2, "view_document")
self.client.force_authenticate(user2)
@@ -383,7 +386,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
assign_perm("view_document", user2, doc)
grant_object(user2, doc, "view_document")
response = self.client.get(f"/api/documents/{doc.pk}/download/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -760,8 +763,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
- History is returned
"""
# No auditlog permissions
user = User.objects.create_user(username="test")
user.user_permissions.add(*Permission.objects.filter(codename="view_document"))
user = UserFactory(username="test")
grant_global(user, "view_document")
self.client.force_authenticate(user=user)
doc = Document.objects.create(
title="First title",
@@ -776,7 +779,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
# superuser
user.is_superuser = True
user.save()
user2 = User.objects.create_user(username="test2")
user2 = UserFactory(username="test2")
doc2 = Document.objects.create(
title="Second title",
checksum="456",
@@ -1073,11 +1076,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
self.assertEqual(response.data["duplicate_documents"], [])
def test_has_duplicates_filter_respects_document_permissions(self) -> None:
owner = User.objects.create_user(username="duplicate-owner")
requester = User.objects.create_user(username="duplicate-requester")
requester.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
owner = UserFactory(username="duplicate-owner")
requester = UserFactory(username="duplicate-requester")
grant_global(requester, "view_document")
visible_document = Document.objects.create(
title="visible document",
checksum="permission-match",
@@ -1096,7 +1097,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
[document["id"] for document in response.data["results"]],
)
assign_perm("view_document", requester, hidden_duplicate)
grant_object(requester, hidden_duplicate, "view_document")
response = self.client.get("/api/documents/?has_duplicates=true")
self.assertIn(
visible_document.id,
@@ -1317,10 +1318,10 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- Owner filters work correctly but still respect permissions
"""
u1 = User.objects.create_user("user1")
u2 = User.objects.create_user("user2")
u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
u1 = UserFactory(username="user1")
u2 = UserFactory(username="user2")
grant_global(u1, "view_document")
grant_global(u2, "view_document")
u1_doc1 = Document.objects.create(
title="none1",
@@ -1353,7 +1354,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
)
self.client.force_authenticate(user=u1)
assign_perm("view_document", u1, u2_doc2)
grant_object(u1, u2_doc2, "view_document")
# Will not show any u1 docs or u2_doc1 which isn't shared
response = self.client.get(f"/api/documents/?owner__id__none={u1.id}")
@@ -1400,7 +1401,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
[u1_doc1.id, u1_doc2.id, u2_doc2.id],
)
assign_perm("view_document", u2, u1_doc1)
grant_object(u2, u1_doc1, "view_document")
# Will show only documents shared by user
response = self.client.get(f"/api/documents/?shared_by__id={u1.id}")
@@ -1424,8 +1425,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
- The document is returned exactly once, not once per permission path
(regression test for https://github.com/paperless-ngx/paperless-ngx/issues/13331)
"""
user = User.objects.create_user("user1")
user.user_permissions.add(*Permission.objects.filter(codename="view_document"))
user = UserFactory(username="user1")
grant_global(user, "view_document")
group = Group.objects.create(name="group1")
user.groups.add(group)
@@ -1433,7 +1434,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
tag2 = TagFactory()
doc = DocumentFactory(title="shared", owner=user)
doc.tags.add(tag1, tag2)
assign_perm("view_document", group, doc)
grant_object(group, doc, "view_document")
self.client.force_authenticate(user=user)
response = self.client.get(
@@ -1452,11 +1453,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- The document does not appear in their results
"""
owner = User.objects.create_user("owner1")
stranger = User.objects.create_user("stranger1")
stranger.user_permissions.add(
*Permission.objects.filter(codename="view_document"),
)
owner = UserFactory(username="owner1")
stranger = UserFactory(username="stranger1")
grant_global(stranger, "view_document")
DocumentFactory(title="private", owner=owner)
@@ -1474,17 +1473,17 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- Only the group member sees the document
"""
owner = User.objects.create_user("owner2")
member = User.objects.create_user("member1")
non_member = User.objects.create_user("nonmember1")
owner = UserFactory(username="owner2")
member = UserFactory(username="member1")
non_member = UserFactory(username="nonmember1")
for u in (member, non_member):
u.user_permissions.add(*Permission.objects.filter(codename="view_document"))
grant_global(u, "view_document")
group = Group.objects.create(name="group2")
member.groups.add(group)
doc = DocumentFactory(title="shared2", owner=owner)
assign_perm("view_document", group, doc)
grant_object(group, doc, "view_document")
self.client.force_authenticate(user=member)
response = self.client.get("/api/documents/")
@@ -1785,8 +1784,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- Statistics only include inbox counts for tags accessible by the user
"""
u1 = User.objects.create_user("user1")
u2 = User.objects.create_user("user2")
u1 = UserFactory(username="user1")
u2 = UserFactory(username="user2")
inbox_tag_u1 = Tag.objects.create(name="inbox_u1", is_inbox_tag=True, owner=u1)
Tag.objects.create(name="inbox_u2", is_inbox_tag=True, owner=u2)
doc_u1 = Document.objects.create(
@@ -1816,11 +1815,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
self.assertEqual(response.data["documents_inbox"], 0)
def test_statistics_with_statistics_permission(self) -> None:
owner = User.objects.create_user("owner")
stats_user = User.objects.create_user("stats-user")
stats_user.user_permissions.add(
Permission.objects.get(codename="view_global_statistics"),
)
owner = UserFactory(username="owner")
stats_user = UserFactory(username="stats-user")
grant_global(stats_user, "view_global_statistics")
inbox_tag = Tag.objects.create(
name="stats_inbox",
@@ -1986,7 +1983,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
def test_upload_insufficient_permissions(self) -> None:
self.client.force_authenticate(user=User.objects.create_user("testuser2"))
self.client.force_authenticate(user=UserFactory(username="testuser2"))
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
@@ -2782,9 +2779,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
mock_get_date_parser.assert_not_called()
def test_saved_views(self) -> None:
u1 = User.objects.create_user("user1")
u2 = User.objects.create_user("user2")
u3 = User.objects.create_user("user3")
u1 = UserFactory(username="user1")
u2 = UserFactory(username="user2")
u3 = UserFactory(username="user3")
view_perm = Permission.objects.get(codename="view_savedview")
change_perm = Permission.objects.get(codename="change_savedview")
@@ -2807,9 +2804,9 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
sort_field="",
)
assign_perm("view_savedview", u1, v2)
assign_perm("change_savedview", u1, v2)
assign_perm("view_savedview", u1, v3)
grant_object(u1, v2, "view_savedview")
grant_object(u1, v2, "change_savedview")
grant_object(u1, v3, "view_savedview")
self.client.force_authenticate(user=u1)
@@ -3064,7 +3061,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
self.assertListEqual(saved_view_settings["sidebar_views_visible_ids"], [v2.id])
def test_saved_view_create_update_patch(self) -> None:
User.objects.create_user("user1")
UserFactory(username="user1")
view = {
"name": "test",
@@ -3127,7 +3124,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
- Display options are updated
- Display fields are validated
"""
User.objects.create_user("user1")
UserFactory(username="user1")
view = {
"name": "test",
@@ -3568,11 +3565,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- Notes are neither created nor deleted
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.all())
user1 = UserFactory(username="test1")
grant_all_global(user1)
user1.save()
user2 = User.objects.create_user(username="test2")
user2 = UserFactory(username="test2")
user2.save()
doc = Document.objects.create(
@@ -3592,7 +3589,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
self.assertEqual(resp.content, b"Insufficient permissions to view notes")
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
assign_perm("view_document", user1, doc)
grant_object(user1, doc, "view_document")
resp = self.client.post(
f"/api/documents/{doc.pk}/notes/",
@@ -3616,12 +3613,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_notes_require_global_document_permissions(self) -> None:
user = User.objects.create_user(username="note_editor")
user.user_permissions.add(
*Permission.objects.filter(
codename__in=["view_note", "add_note", "delete_note"],
),
)
user = UserFactory(username="note_editor")
grant_global(user, "view_note", "add_note", "delete_note")
doc = Document.objects.create(
title="test",
mime_type="application/pdf",
@@ -3634,9 +3627,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
response = self.client.get(f"/api/documents/{doc.pk}/notes/")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
user.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_global(user, "view_document")
user = User.objects.get(pk=user.pk)
self.client.force_authenticate(user)
response = self.client.get(f"/api/documents/{doc.pk}/notes/")
@@ -3648,9 +3639,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
user.user_permissions.add(
Permission.objects.get(codename="change_document"),
)
grant_global(user, "change_document")
user = User.objects.get(pk=user.pk)
self.client.force_authenticate(user)
response = self.client.post(
@@ -3797,12 +3786,12 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
- Unique items are created
- Non-unique items are not allowed
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.filter(codename="add_tag"))
user1 = UserFactory(username="test1")
grant_global(user1, "add_tag")
user1.save()
user2 = User.objects.create_user(username="test2")
user2.user_permissions.add(*Permission.objects.filter(codename="add_tag"))
user2 = UserFactory(username="test2")
grant_global(user2, "add_tag")
user2.save()
# User 1 creates tag 1 owned by user 1 by default
@@ -3857,12 +3846,12 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
- Unique items are created
- Non-unique items are not allowed on update
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.filter(codename="change_tag"))
user1 = UserFactory(username="test1")
grant_global(user1, "change_tag")
user1.save()
user2 = User.objects.create_user(username="test2")
user2.user_permissions.add(*Permission.objects.filter(codename="change_tag"))
user2 = UserFactory(username="test2")
grant_global(user2, "change_tag")
user2.save()
# Create name tag 1 owned by user 1
@@ -3993,11 +3982,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- Links only shown if user has permissions
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.all())
user1 = UserFactory(username="test1")
grant_all_global(user1)
user1.save()
user2 = User.objects.create_user(username="test2")
user2 = UserFactory(username="test2")
user2.save()
doc = Document.objects.create(
@@ -4017,7 +4006,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
self.assertEqual(resp.content, b"Insufficient permissions to add share link")
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
assign_perm("change_document", user1, doc)
grant_object(user1, doc, "change_document")
resp = self.client.get(
f"/api/documents/{doc.pk}/share_links/",
@@ -4034,11 +4023,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- Share link creation is denied until view permission is granted
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.filter(codename="add_sharelink"))
user1 = UserFactory(username="test1")
grant_global(user1, "add_sharelink")
user1.save()
user2 = User.objects.create_user(username="test2")
user2 = UserFactory(username="test2")
user2.save()
doc = Document.objects.create(
@@ -4060,7 +4049,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
)
self.assertEqual(create_resp.status_code, status.HTTP_403_FORBIDDEN)
assign_perm("view_document", user1, doc)
grant_object(user1, doc, "view_document")
create_resp = self.client.post(
"/api/share_links/",
@@ -4072,9 +4061,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
)
self.assertEqual(create_resp.status_code, status.HTTP_403_FORBIDDEN)
user1.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_global(user1, "view_document")
user1 = User.objects.get(pk=user1.pk)
self.client.force_authenticate(user1)
create_resp = self.client.post(
@@ -4097,11 +4084,11 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- ASN +1 from user2's doc is returned for user1
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.all())
user1 = UserFactory(username="test1")
grant_all_global(user1)
user1.save()
user2 = User.objects.create_user(username="test2")
user2 = UserFactory(username="test2")
user2.save()
doc1 = Document.objects.create(
@@ -4141,8 +4128,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- ASN 1 is returned
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.all())
user1 = UserFactory(username="test1")
grant_all_global(user1)
user1.save()
doc1 = Document.objects.create(
@@ -4170,7 +4157,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- Explicit error is returned
"""
user1 = User.objects.create_superuser(username="test1")
user1 = UserFactory(username="test1", superuser=True)
self.client.force_authenticate(user1)
@@ -4348,8 +4335,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
THEN:
- Error response is returned
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.all())
user1 = UserFactory(username="test1")
grant_all_global(user1)
user1.save()
doc = Document.objects.create(
@@ -4454,7 +4441,7 @@ class TestDocumentApiTagColors(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
@@ -4534,7 +4521,7 @@ class TestDocumentApiCustomFieldsSorting(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
self.doc1 = Document.objects.create(
+8 -8
View File
@@ -2,16 +2,16 @@ import json
import shutil
from unittest import mock
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.core import mail
from django.test import override_settings
from rest_framework import status
from rest_framework.test import APITestCase
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import SampleDirMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
@@ -20,7 +20,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
self.doc1 = Document.objects.create(
@@ -303,8 +303,8 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
THEN:
- Forbidden response is returned
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
user1 = UserFactory(username="test1")
grant_global(user1, "view_document")
doc_owned = Document.objects.create(
title="owned_doc",
@@ -338,8 +338,8 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
THEN:
- Request succeeds
"""
user1 = User.objects.create_user(username="test1")
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
user1 = UserFactory(username="test1")
grant_global(user1, "view_document")
self.client.force_authenticate(user1)
@@ -4,7 +4,6 @@ from collections.abc import Callable
from datetime import date
from urllib.parse import quote
from django.contrib.auth.models import User
from rest_framework.test import APITestCase
from documents.models import CustomField
@@ -13,7 +12,8 @@ from documents.models import Document
from documents.models import SavedView
from documents.models import SavedViewFilterRule
from documents.serialisers import DocumentSerializer
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
class DocumentWrapper:
@@ -35,7 +35,7 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
# Create one custom field per type. The fields are called f"{type}_field".
+35 -54
View File
@@ -3,12 +3,10 @@ import json
from unittest import mock
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.db import connection
from django.test import override_settings
from django.test.utils import CaptureQueriesContext
from guardian.shortcuts import assign_perm
from guardian.shortcuts import get_groups_with_perms
from guardian.shortcuts import get_users_with_perms
from rest_framework import status
@@ -21,14 +19,17 @@ from documents.models import Document
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
class TestApiObjects(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
user = User.objects.create_superuser(username="temp_admin")
user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=user)
self.tag1 = Tag.objects.create(name="t1", is_inbox_tag=True)
@@ -153,7 +154,7 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
# A newer document owned by another user must not leak through the
# aggregate for a non-superuser who cannot view it
other = User.objects.create_user(username="other")
other = UserFactory(username="other")
Document.objects.create(
mime_type="application/pdf",
correspondent=self.c1,
@@ -162,10 +163,8 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
owner=other,
)
user = User.objects.create_user(username="regular")
user.user_permissions.add(
Permission.objects.get(codename="view_correspondent"),
)
user = UserFactory(username="regular")
grant_global(user, "view_correspondent")
self.client.force_authenticate(user=user)
response = self.client.get("/api/correspondents/?last_correspondence=true")
@@ -200,7 +199,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
user = User.objects.create_superuser(username="temp_admin")
user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=user)
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
@@ -455,11 +454,9 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
self.assertEqual(response.data, "folder/Something.pdf")
def test_test_storage_path_requires_document_view_permission(self) -> None:
owner = User.objects.create_user(username="owner")
unprivileged = User.objects.create_user(username="unprivileged")
unprivileged.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
owner = UserFactory(username="owner")
unprivileged = UserFactory(username="unprivileged")
grant_global(unprivileged, "view_document")
document = Document.objects.create(
mime_type="application/pdf",
owner=owner,
@@ -481,15 +478,15 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
self.assertIn("document", response.data)
def test_test_storage_path_allows_shared_document_view_permission(self) -> None:
owner = User.objects.create_user(username="owner")
viewer = User.objects.create_user(username="viewer")
owner = UserFactory(username="owner")
viewer = UserFactory(username="viewer")
document = Document.objects.create(
mime_type="application/pdf",
owner=owner,
title="Shared",
checksum="123",
)
assign_perm("view_document", viewer, document)
grant_object(viewer, document, "view_document")
self.client.force_authenticate(user=viewer)
response = self.client.post(
@@ -504,9 +501,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
viewer.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_global(viewer, "view_document")
viewer = User.objects.get(pk=viewer.pk)
self.client.force_authenticate(user=viewer)
response = self.client.post(
@@ -545,14 +540,12 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
def test_test_storage_path_exposes_basic_document_context_but_not_sensitive_owner_data(
self,
) -> None:
owner = User.objects.create_user(
owner = UserFactory(
username="owner",
password="password",
email="owner@example.com",
)
owner.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_global(owner, "view_document")
document = Document.objects.create(
mime_type="application/pdf",
owner=owner,
@@ -614,8 +607,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
def test_test_storage_path_includes_related_objects_for_visible_document(
self,
) -> None:
owner = User.objects.create_user(username="owner")
viewer = User.objects.create_user(username="viewer")
owner = UserFactory(username="owner")
viewer = UserFactory(username="viewer")
private_correspondent = Correspondent.objects.create(
name="Private Correspondent",
owner=owner,
@@ -627,10 +620,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
title="Document",
checksum="123",
)
assign_perm("view_document", viewer, document)
viewer.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_object(viewer, document, "view_document")
grant_global(viewer, "view_document")
self.client.force_authenticate(user=viewer)
response = self.client.post(
@@ -662,7 +653,7 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
self.assertEqual(response.data, "Private Correspondent.pdf")
def test_test_storage_path_superuser_can_view_private_related_objects(self) -> None:
owner = User.objects.create_user(username="owner")
owner = UserFactory(username="owner")
private_correspondent = Correspondent.objects.create(
name="Private Correspondent",
owner=owner,
@@ -693,8 +684,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
def test_test_storage_path_includes_doc_type_storage_path_and_tags(
self,
) -> None:
owner = User.objects.create_user(username="owner")
viewer = User.objects.create_user(username="viewer")
owner = UserFactory(username="owner")
viewer = UserFactory(username="viewer")
private_document_type = DocumentType.objects.create(
name="Private Type",
owner=owner,
@@ -717,10 +708,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
checksum="123",
)
document.tags.add(private_tag)
assign_perm("view_document", viewer, document)
viewer.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_object(viewer, document, "view_document")
grant_global(viewer, "view_document")
self.client.force_authenticate(user=viewer)
response = self.client.post(
@@ -756,8 +745,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
def test_test_storage_path_includes_custom_fields_for_visible_document(
self,
) -> None:
owner = User.objects.create_user(username="owner")
viewer = User.objects.create_user(username="viewer")
owner = UserFactory(username="owner")
viewer = UserFactory(username="viewer")
document = Document.objects.create(
mime_type="application/pdf",
owner=owner,
@@ -773,10 +762,8 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase):
field=custom_field,
value_int=42,
)
assign_perm("view_document", viewer, document)
viewer.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_object(viewer, document, "view_document")
grant_global(viewer, "view_document")
self.client.force_authenticate(user=viewer)
response = self.client.post(
@@ -798,7 +785,7 @@ class TestBulkEditObjects(APITestCase):
def setUp(self) -> None:
super().setUp()
self.temp_admin = User.objects.create_superuser(username="temp_admin")
self.temp_admin = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.temp_admin)
self.t1 = Tag.objects.create(name="t1")
@@ -1030,9 +1017,7 @@ class TestBulkEditObjects(APITestCase):
THEN:
- User is able to delete objects
"""
self.user1.user_permissions.add(
*Permission.objects.filter(codename="delete_tag"),
)
grant_global(self.user1, "delete_tag")
self.user1.save()
self.client.force_authenticate(user=self.user1)
@@ -1062,9 +1047,7 @@ class TestBulkEditObjects(APITestCase):
self.t2.owner = User.objects.get(username="temp_admin")
self.t2.save()
self.user1.user_permissions.add(
*Permission.objects.filter(codename="delete_tag"),
)
grant_global(self.user1, "delete_tag")
self.user1.save()
self.client.force_authenticate(user=self.user1)
@@ -1097,9 +1080,7 @@ class TestBulkEditObjects(APITestCase):
self.t2.owner = User.objects.get(username="temp_admin")
self.t2.save()
self.user1.user_permissions.add(
*Permission.objects.filter(codename="delete_tag"),
)
grant_global(self.user1, "delete_tag")
self.user1.save()
self.client.force_authenticate(user=self.user1)
+62 -76
View File
@@ -5,9 +5,7 @@ from unittest import mock
from allauth.mfa.models import Authenticator
from allauth.mfa.totp.internal import auth as totp_auth
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from guardian.shortcuts import assign_perm
from guardian.shortcuts import get_perms
from guardian.shortcuts import get_users_with_perms
from rest_framework import status
@@ -19,7 +17,11 @@ from documents.models import DocumentType
from documents.models import MatchingModel
from documents.models import StoragePath
from documents.models import Tag
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_all_global
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
class TestApiAuth(DirectoriesMixin, APITestCase):
@@ -93,14 +95,14 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
self.assertNotIn("X-Version", response)
def test_api_version_with_auth(self) -> None:
user = User.objects.create_superuser(username="test")
user = UserFactory(username="test", superuser=True)
self.client.force_authenticate(user)
response = self.client.get("/api/documents/")
self.assertIn("X-Api-Version", response)
self.assertIn("X-Version", response)
def test_api_insufficient_permissions(self) -> None:
user = User.objects.create_user(username="test")
user = UserFactory(username="test")
self.client.force_authenticate(user)
Document.objects.create(title="Test")
@@ -137,8 +139,8 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
)
def test_api_sufficient_permissions(self) -> None:
user = User.objects.create_user(username="test")
user.user_permissions.add(*Permission.objects.all())
user = UserFactory(username="test")
grant_all_global(user)
user.is_staff = True
self.client.force_authenticate(user)
@@ -166,9 +168,9 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
)
def test_api_get_object_permissions(self) -> None:
user1 = User.objects.create_user(username="test1")
user2 = User.objects.create_user(username="test2")
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
user1 = UserFactory(username="test1")
user2 = UserFactory(username="test2")
grant_global(user1, "view_document")
self.client.force_authenticate(user1)
self.assertEqual(
@@ -205,7 +207,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
THEN:
- Object created with current user as owner
"""
user1 = User.objects.create_superuser(username="user1")
user1 = UserFactory(username="user1", superuser=True)
self.client.force_authenticate(user1)
@@ -234,7 +236,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
THEN:
- Object created with no owner
"""
user1 = User.objects.create_superuser(username="user1")
user1 = UserFactory(username="user1", superuser=True)
self.client.force_authenticate(user1)
@@ -265,7 +267,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
THEN:
- Object permissions are set appropriately
"""
user1 = User.objects.create_superuser(username="user1")
user1 = UserFactory(username="user1", superuser=True)
user2 = User.objects.create(username="user2")
group1 = Group.objects.create(name="group1")
@@ -313,7 +315,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
THEN:
- Object permissions are set appropriately
"""
user1 = User.objects.create_superuser(username="user1")
user1 = UserFactory(username="user1", superuser=True)
user2 = User.objects.create(username="user2")
group1 = Group.objects.create(name="group1")
@@ -363,7 +365,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
mime_type="application/pdf",
content="this is a document",
)
user1 = User.objects.create_superuser(username="user1")
user1 = UserFactory(username="user1", superuser=True)
user2 = User.objects.create(username="user2")
group1 = Group.objects.create(name="group1")
@@ -413,16 +415,16 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
mime_type="application/pdf",
content="this is a document",
)
user1 = User.objects.create_superuser(username="user1")
user1 = UserFactory(username="user1", superuser=True)
user2 = User.objects.create(username="user2")
group1 = Group.objects.create(name="group1")
doc.owner = user1
doc.save()
assign_perm("view_document", user2, doc)
assign_perm("change_document", user2, doc)
assign_perm("view_document", group1, doc)
assign_perm("change_document", group1, doc)
grant_object(user2, doc, "view_document")
grant_object(user2, doc, "change_document")
grant_object(group1, doc, "view_document")
grant_object(group1, doc, "change_document")
self.client.force_authenticate(user1)
@@ -446,11 +448,9 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
self.assertIn("change_document", get_perms(group1, doc))
def test_document_permissions_change_requires_owner(self) -> None:
owner = User.objects.create_user(username="owner")
editor = User.objects.create_user(username="editor")
editor.user_permissions.add(
*Permission.objects.all(),
)
owner = UserFactory(username="owner")
editor = UserFactory(username="editor")
grant_all_global(editor)
doc = Document.objects.create(
title="Ownered doc",
@@ -460,8 +460,8 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
owner=owner,
)
assign_perm("view_document", editor, doc)
assign_perm("change_document", editor, doc)
grant_object(editor, doc, "view_document")
grant_object(editor, doc, "change_document")
self.client.force_authenticate(editor)
response = self.client.patch(
@@ -499,9 +499,9 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_dynamic_permissions_fields(self) -> None:
user1 = User.objects.create_user(username="user1")
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
user2 = User.objects.create_user(username="user2")
user1 = UserFactory(username="user1")
grant_global(user1, "view_document")
user2 = UserFactory(username="user2")
Document.objects.create(title="Test", content="content 1", checksum="1")
doc2 = Document.objects.create(
@@ -523,10 +523,10 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
owner=user1,
)
assign_perm("view_document", user1, doc2)
assign_perm("view_document", user1, doc3)
assign_perm("change_document", user1, doc3)
assign_perm("view_document", user2, doc4)
grant_object(user1, doc2, "view_document")
grant_object(user1, doc3, "view_document")
grant_object(user1, doc3, "change_document")
grant_object(user2, doc4, "view_document")
self.client.force_authenticate(user1)
@@ -574,8 +574,8 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
owned by someone else with no explicit guardian grant -- mirrors
guardian's own ObjectPermissionChecker.has_perm() superuser shortcut.
"""
superuser = User.objects.create_superuser(username="admin")
other_user = User.objects.create_user(username="user2")
superuser = UserFactory(username="admin", superuser=True)
other_user = UserFactory(username="user2")
Document.objects.create(
title="Test",
content="content",
@@ -602,7 +602,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
THEN:
- MFA required error is returned
"""
user1 = User.objects.create_user(username="user1")
user1 = UserFactory(username="user1")
user1.set_password("password")
user1.save()
@@ -626,7 +626,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
THEN:
- MFA code is required
"""
user1 = User.objects.create_user(username="user1")
user1 = UserFactory(username="user1")
user1.set_password("password")
user1.save()
@@ -688,7 +688,7 @@ class TestApiUser(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
def test_get_users(self) -> None:
@@ -858,10 +858,8 @@ class TestApiUser(DirectoriesMixin, APITestCase):
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
regular_user = User.objects.create_user(username="regular_user")
regular_user.user_permissions.add(
*Permission.objects.all(),
)
regular_user = UserFactory(username="regular_user")
grant_all_global(regular_user)
self.client.force_authenticate(regular_user)
Authenticator.objects.create(
user=user1,
@@ -885,9 +883,9 @@ class TestApiUser(DirectoriesMixin, APITestCase):
- Only superusers can change superuser status
"""
user1 = User.objects.create_user(username="user1")
user1.user_permissions.add(*Permission.objects.all())
user2 = User.objects.create_superuser(username="user2")
user1 = UserFactory(username="user1")
grant_all_global(user1)
user2 = UserFactory(username="user2", superuser=True)
self.client.force_authenticate(user1)
@@ -972,9 +970,9 @@ class TestApiUser(DirectoriesMixin, APITestCase):
- Only superusers can change staff status
"""
user1 = User.objects.create_user(username="user1")
user1.user_permissions.add(*Permission.objects.all())
user2 = User.objects.create_superuser(username="user2")
user1 = UserFactory(username="user1")
grant_all_global(user1)
user2 = UserFactory(username="user2", superuser=True)
self.client.force_authenticate(user1)
@@ -1027,7 +1025,7 @@ class TestApiGroup(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
def test_get_groups(self) -> None:
@@ -1128,7 +1126,7 @@ class TestBulkEditObjectPermissions(APITestCase):
def setUp(self) -> None:
super().setUp()
self.temp_admin = User.objects.create_superuser(username="temp_admin")
self.temp_admin = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.temp_admin)
self.t1 = Tag.objects.create(name="t1")
@@ -1276,7 +1274,7 @@ class TestBulkEditObjectPermissions(APITestCase):
},
}
assign_perm("view_tag", self.user3, self.t1)
grant_object(self.user3, self.t1, "view_tag")
self.t1.owner = self.user3
self.t1.save()
@@ -1373,13 +1371,9 @@ class TestBulkEditObjectPermissions(APITestCase):
"""
self.t1.owner = self.user2
self.t1.save()
assign_perm("view_tag", self.user1, self.t1)
assign_perm("change_tag", self.user1, self.t1)
self.user1.user_permissions.add(
*Permission.objects.filter(
codename__in=["view_tag", "change_tag"],
),
)
grant_object(self.user1, self.t1, "view_tag")
grant_object(self.user1, self.t1, "change_tag")
grant_global(self.user1, "view_tag", "change_tag")
user1 = User.objects.get(pk=self.user1.pk)
self.client.force_authenticate(user=user1)
@@ -1426,13 +1420,9 @@ class TestBulkEditObjectPermissions(APITestCase):
"""
owned = Tag.objects.create(name="owned", owner=self.user1)
shared = Tag.objects.create(name="shared", owner=self.user2)
assign_perm("view_tag", self.user1, shared)
assign_perm("change_tag", self.user1, shared)
self.user1.user_permissions.add(
*Permission.objects.filter(
codename__in=["view_tag", "change_tag"],
),
)
grant_object(self.user1, shared, "view_tag")
grant_object(self.user1, shared, "change_tag")
grant_global(self.user1, "view_tag", "change_tag")
user1 = User.objects.get(pk=self.user1.pk)
self.client.force_authenticate(user=user1)
@@ -1473,14 +1463,10 @@ class TestBulkEditObjectPermissions(APITestCase):
"""
self.t1.owner = self.user2
self.t1.save()
assign_perm("view_tag", self.user1, self.t1)
assign_perm("change_tag", self.user1, self.t1)
assign_perm("delete_tag", self.user1, self.t1)
self.user1.user_permissions.add(
*Permission.objects.filter(
codename__in=["view_tag", "change_tag", "delete_tag"],
),
)
grant_object(self.user1, self.t1, "view_tag")
grant_object(self.user1, self.t1, "change_tag")
grant_object(self.user1, self.t1, "delete_tag")
grant_global(self.user1, "view_tag", "change_tag", "delete_tag")
user1 = User.objects.get(pk=self.user1.pk)
self.client.force_authenticate(user=user1)
@@ -1585,7 +1571,7 @@ class TestBulkEditObjectPermissions(APITestCase):
- Request succeeds and null is treated as an empty user list,
so the existing view permission is removed
"""
assign_perm("view_tag", self.user1, self.t1)
grant_object(self.user1, self.t1, "view_tag")
response = self.client.post(
"/api/bulk_edit_objects/",
@@ -1680,7 +1666,7 @@ class TestFullPermissionsFlag(APITestCase):
def setUp(self) -> None:
super().setUp()
self.admin = User.objects.create_superuser(username="admin")
self.admin = UserFactory(username="admin", superuser=True)
def test_full_perms_flag(self) -> None:
"""
@@ -1,3 +1,5 @@
from __future__ import annotations
import unicodedata
from typing import TYPE_CHECKING
from unittest import mock
@@ -7,8 +9,11 @@ import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
if TYPE_CHECKING:
from rest_framework.test import APIClient
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from paperless_testing.dirs import PaperlessDirs
@pytest.fixture()
@@ -18,22 +23,14 @@ def consume_file_mock():
yield m
@pytest.fixture()
def directories(tmp_path, settings, _media_settings):
scratch = tmp_path / "scratch"
scratch.mkdir()
settings.SCRATCH_DIR = scratch
return scratch
@pytest.mark.django_db
class TestPostDocumentNFCNormalization:
def test_nfd_filename_normalized_to_nfc(
self,
admin_client,
admin_client: APIClient,
consume_file_mock: mock.MagicMock,
directories,
):
paperless_dirs: PaperlessDirs,
) -> None:
"""Uploaded file with NFD filename must have its name stored as NFC."""
nfd = unicodedata.normalize("NFD", "Rechnung März.pdf")
nfc = unicodedata.normalize("NFC", "Rechnung März.pdf")
@@ -69,10 +66,10 @@ class TestPostDocumentNFCNormalization:
def test_already_nfc_filename_unchanged(
self,
admin_client,
admin_client: APIClient,
consume_file_mock: mock.MagicMock,
directories,
):
paperless_dirs: PaperlessDirs,
) -> None:
"""Uploaded file with already-NFC filename must pass through unchanged."""
nfc = unicodedata.normalize("NFC", "Invoice_2024.pdf")
+5 -3
View File
@@ -8,7 +8,8 @@ from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
# see allauth.socialaccount.providers.openid.provider.OpenIDProvider
@@ -55,10 +56,11 @@ class TestApiProfile(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(
self.user = UserFactory(
username="temp_admin",
first_name="firstname",
last_name="surname",
superuser=True,
)
self.client.force_authenticate(user=self.user)
@@ -401,7 +403,7 @@ class TestApiTOTPViews(APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
def test_get_totp(self) -> None:
+40 -46
View File
@@ -7,10 +7,8 @@ import time_machine
from dateutil.relativedelta import relativedelta
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import override_settings
from django.utils import timezone
from guardian.shortcuts import assign_perm
from rest_framework import status
from rest_framework.test import APITestCase
@@ -27,10 +25,13 @@ from documents.models import Tag
from documents.models import Workflow
from documents.search import get_backend
from documents.search import reset_backend
from documents.tests.factories import DocumentFactory
from documents.tests.utils import DirectoriesMixin
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
pytestmark = pytest.mark.search
@@ -40,7 +41,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
super().setUp()
reset_backend()
self.user = User.objects.create_superuser(username="temp_admin")
self.user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=self.user)
def tearDown(self) -> None:
@@ -949,9 +950,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
THEN:
- Terms only within docs user has access to are returned
"""
u1 = User.objects.create_user("user1")
u2 = User.objects.create_user("user2")
u1.user_permissions.add(Permission.objects.get(codename="view_document"))
u1 = UserFactory(username="user1")
u2 = UserFactory(username="user2")
grant_global(u1, "view_document")
self.client.force_authenticate(user=u1)
@@ -991,7 +992,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, ["applebaum", "apples"])
assign_perm("view_document", u1, d3)
grant_object(u1, d3, "view_document")
backend.add_or_update(d3)
response = self.client.get("/api/search/autocomplete/?term=app")
@@ -999,10 +1000,10 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.data, ["applebaum", "apples", "appletini"])
def test_search_autocomplete_group_revocation_is_immediate(self) -> None:
user = User.objects.create_user("group-user")
owner = User.objects.create_user("document-owner")
user = UserFactory(username="group-user")
owner = UserFactory(username="document-owner")
group = Group.objects.create(name="temporary-viewers")
user.user_permissions.add(Permission.objects.get(codename="view_document"))
grant_global(user, "view_document")
user.groups.add(group)
document = Document.objects.create(
@@ -1011,7 +1012,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
checksum="group-revocation",
owner=owner,
)
assign_perm("view_document", group, document)
grant_object(group, document, "view_document")
get_backend().add_or_update(document)
self.client.force_authenticate(user=user)
@@ -1091,11 +1092,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertIsNone(response.data["corrected_query"])
def test_search_spelling_suggestion_suppressed_for_private_terms(self) -> None:
owner = User.objects.create_user("owner")
attacker = User.objects.create_user("attacker")
attacker.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
owner = UserFactory(username="owner")
attacker = UserFactory(username="attacker")
grant_global(attacker, "view_document")
backend = get_backend()
for i in range(5):
@@ -1222,11 +1221,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
THEN:
- The request is rejected
"""
owner = User.objects.create_user("owner")
attacker = User.objects.create_user("attacker")
attacker.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
owner = UserFactory(username="owner")
attacker = UserFactory(username="attacker")
grant_global(attacker, "view_document")
private_seed = Document.objects.create(
title="private bank statement",
@@ -1534,11 +1531,11 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
- Only owned docs are returned for regular users
- All docs are returned for superuser
"""
superuser = User.objects.create_superuser("superuser")
u1 = User.objects.create_user("user1")
u2 = User.objects.create_user("user2")
u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
superuser = UserFactory(username="superuser", superuser=True)
u1 = UserFactory(username="user1")
u2 = UserFactory(username="user2")
grant_global(u1, "view_document")
grant_global(u2, "view_document")
Document.objects.create(checksum="1", content="test 1", owner=u1)
Document.objects.create(checksum="2", content="test 2", owner=u2)
@@ -1588,10 +1585,10 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
THEN:
- Only docs with granted view permissions are returned
"""
u1 = User.objects.create_user("user1")
u2 = User.objects.create_user("user2")
u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
u1 = UserFactory(username="user1")
u2 = UserFactory(username="user2")
grant_global(u1, "view_document")
grant_global(u2, "view_document")
d1 = Document.objects.create(checksum="1", content="test 1", owner=u1)
d2 = Document.objects.create(checksum="2", content="test 2", owner=u2)
@@ -1616,9 +1613,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
r = self.client.get("/api/documents/?query=test&owner__isnull=true")
self.assertEqual(r.data["count"], 1)
assign_perm("view_document", u1, d2)
assign_perm("view_document", u1, d3)
assign_perm("view_document", u2, d1)
grant_object(u1, d2, "view_document")
grant_object(u1, d3, "view_document")
grant_object(u2, d1, "view_document")
backend.add_or_update(d1)
backend.add_or_update(d2)
@@ -1641,8 +1638,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(r.data["count"], 1)
def test_search_sorting(self) -> None:
u1 = User.objects.create_user("user1")
u2 = User.objects.create_user("user2")
u1 = UserFactory(username="user1")
u2 = UserFactory(username="user2")
c1 = Correspondent.objects.create(name="corres Ax")
c2 = Correspondent.objects.create(name="corres Cx")
c3 = Correspondent.objects.create(name="corres Bx")
@@ -1892,8 +1889,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
pk=5,
)
user1 = User.objects.create_user("bank user1")
user2 = User.objects.create_superuser("user2")
user1 = UserFactory(username="bank user1")
user2 = UserFactory(username="user2", superuser=True)
group1 = Group.objects.create(name="bank group1")
Group.objects.create(name="group2")
@@ -1925,7 +1922,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
sort_field="",
owner=user2,
)
assign_perm("view_savedview", user1, shared_view)
grant_object(user1, shared_view, "view_savedview")
mail_account1 = MailAccount.objects.create(name="bank mail account 1")
mail_account2 = MailAccount.objects.create(name="mail account 2")
mail_rule1 = MailRule.objects.create(
@@ -2018,12 +2015,9 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(returned.get(root.id), "latest content")
def test_global_search_filters_owned_mail_objects(self) -> None:
user1 = User.objects.create_user("mail-search-user")
user2 = User.objects.create_user("other-mail-search-user")
user1.user_permissions.add(
Permission.objects.get(codename="view_mailaccount"),
Permission.objects.get(codename="view_mailrule"),
)
user1 = UserFactory(username="mail-search-user")
user2 = UserFactory(username="other-mail-search-user")
grant_global(user1, "view_mailaccount", "view_mailrule")
own_account = MailAccount.objects.create(
name="bank owned account",
+7 -11
View File
@@ -5,8 +5,6 @@ from datetime import timedelta
from pathlib import Path
from unittest import mock
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import override_settings
from django.utils import timezone
from rest_framework import status
@@ -14,8 +12,10 @@ from rest_framework.test import APITestCase
from documents.models import PaperlessTask
from documents.permissions import has_system_status_permission
from documents.tests.factories import PaperlessTaskFactory
from paperless import version
from paperless_testing.factories import PaperlessTaskFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
class TestSystemStatus(APITestCase):
@@ -23,9 +23,7 @@ class TestSystemStatus(APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(
username="temp_admin",
)
self.user = UserFactory(username="temp_admin", superuser=True)
self.tmp_dir = Path(tempfile.mkdtemp())
self.override = override_settings(MEDIA_ROOT=self.tmp_dir)
self.override.enable()
@@ -96,7 +94,7 @@ class TestSystemStatus(APITestCase):
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assertEqual(response["WWW-Authenticate"], "Token")
normal_user = User.objects.create_user(username="normal_user")
normal_user = UserFactory(username="normal_user")
self.client.force_login(normal_user)
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@@ -107,10 +105,8 @@ class TestSystemStatus(APITestCase):
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
user = User.objects.create_user(username="status_user")
user.user_permissions.add(
Permission.objects.get(codename="view_system_monitoring"),
)
user = UserFactory(username="status_user")
grant_global(user, "view_system_monitoring")
self.client.force_login(user)
response = self.client.get(self.ENDPOINT)
+20 -38
View File
@@ -11,22 +11,21 @@ from datetime import timedelta
from unittest import mock
import pytest
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.utils import timezone
from guardian.shortcuts import assign_perm
from rest_framework import status
from rest_framework.test import APIClient
from documents.filters import PaperlessTaskFilterSet
from documents.models import PaperlessTask
from documents.tests.factories import DocumentFactory
from documents.tests.factories import PaperlessTaskFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import PaperlessTaskFactory
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
pytestmark = pytest.mark.api
ENDPOINT = "/api/tasks/"
ACCEPT_V10 = "application/json; version=10"
ACCEPT_V9 = "application/json; version=9"
@@ -346,21 +345,16 @@ class TestGetTasksV10:
self,
admin_user: User,
regular_user: User,
user_client: APIClient,
) -> None:
"""Regular users see their own tasks and unowned (system) tasks; other users' tasks are hidden."""
regular_user.user_permissions.add(
Permission.objects.get(codename="view_paperlesstask"),
)
client = APIClient()
client.force_authenticate(user=regular_user)
client.credentials(HTTP_ACCEPT=ACCEPT_V10)
grant_global(regular_user, "view_paperlesstask")
PaperlessTaskFactory(owner=admin_user) # other user — not visible
unowned_task = PaperlessTaskFactory() # unowned (system task) — visible
own_task = PaperlessTaskFactory(owner=regular_user)
response = client.get(ENDPOINT)
response = user_client.get(ENDPOINT)
assert response.status_code == status.HTTP_200_OK
assert response.data["count"] == 2
@@ -590,9 +584,7 @@ class TestGetTasksV9:
regular_user: User,
) -> None:
"""Non-staff users see their own tasks plus unowned tasks via v9 API."""
regular_user.user_permissions.add(
Permission.objects.get(codename="view_paperlesstask"),
)
grant_global(regular_user, "view_paperlesstask")
client = APIClient()
client.force_authenticate(user=regular_user)
@@ -732,19 +724,17 @@ class TestAcknowledge:
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_succeeds_with_change_permission(self, regular_user: User) -> None:
def test_succeeds_with_change_permission(
self,
regular_user: User,
user_client: APIClient,
) -> None:
"""Users granted change_paperlesstask permission can acknowledge tasks."""
regular_user.user_permissions.add(
Permission.objects.get(codename="change_paperlesstask"),
)
grant_global(regular_user, "change_paperlesstask")
regular_user.save()
client = APIClient()
client.force_authenticate(user=regular_user)
client.credentials(HTTP_ACCEPT=ACCEPT_V10)
task = PaperlessTaskFactory()
response = client.post(
response = user_client.post(
ENDPOINT + "acknowledge/",
{"tasks": [task.id]},
format="json",
@@ -807,9 +797,7 @@ class TestSummaryPermissions:
regular_user,
) -> None:
"""A user with view_system_monitoring but no document permissions can access summary/."""
regular_user.user_permissions.add(
Permission.objects.get(codename="view_system_monitoring"),
)
grant_global(regular_user, "view_system_monitoring")
response = user_client.get(ENDPOINT + "summary/")
@@ -822,9 +810,7 @@ class TestSummaryPermissions:
admin_user,
) -> None:
"""Monitoring user sees aggregate data for all tasks, not just unowned ones."""
regular_user.user_permissions.add(
Permission.objects.get(codename="view_system_monitoring"),
)
grant_global(regular_user, "view_system_monitoring")
PaperlessTaskFactory(
owner=admin_user,
task_type=PaperlessTask.TaskType.CONSUME_FILE,
@@ -845,9 +831,7 @@ class TestSummaryPermissions:
) -> None:
"""A regular user with view_paperlesstask but not view_system_monitoring sees only
their own tasks and unowned tasks in the summary, not other users' tasks."""
regular_user.user_permissions.add(
Permission.objects.get(codename="view_paperlesstask"),
)
grant_global(regular_user, "view_paperlesstask")
PaperlessTaskFactory(
owner=regular_user,
@@ -1012,9 +996,7 @@ class TestDuplicateDocumentsPermissions:
@pytest.fixture()
def user_v9_client(self, regular_user: User) -> APIClient:
regular_user.user_permissions.add(
Permission.objects.get(codename="view_paperlesstask"),
)
grant_global(regular_user, "view_paperlesstask")
client = APIClient()
client.force_authenticate(user=regular_user)
client.credentials(HTTP_ACCEPT=ACCEPT_V9)
@@ -1085,7 +1067,7 @@ class TestDuplicateDocumentsPermissions:
) -> None:
"""A user with explicit guardian view_document permission sees the duplicate_of document."""
doc = DocumentFactory(owner=admin_user, title="Granted Doc")
assign_perm("view_document", regular_user, doc)
grant_object(regular_user, doc, "view_document")
PaperlessTaskFactory(
owner=regular_user,
status=PaperlessTask.Status.SUCCESS,
+9 -9
View File
@@ -1,21 +1,21 @@
from datetime import date
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.core.cache import cache
from rest_framework import status
from rest_framework.test import APITestCase
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_all_global
class TestTrashAPI(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_user(username="temp_admin")
self.user.user_permissions.add(*Permission.objects.all())
self.user = UserFactory(username="temp_admin")
grant_all_global(self.user)
self.client.force_authenticate(user=self.user)
cache.clear()
@@ -70,7 +70,7 @@ class TestTrashAPI(DirectoriesMixin, APITestCase):
self.assertEqual(Document.global_objects.count(), 0)
def test_trash_list_requires_global_document_view_permission(self) -> None:
user = User.objects.create_user(username="trash_owner")
user = UserFactory(username="trash_owner")
document = Document.objects.create(title="Owned", owner=user)
document.delete()
self.client.force_authenticate(user)
@@ -140,7 +140,7 @@ class TestTrashAPI(DirectoriesMixin, APITestCase):
created=date(2023, 1, 2),
)
document_not_owned.delete()
user2 = User.objects.create_user(username="user2")
user2 = UserFactory(username="user2")
document_u2 = Document.objects.create(
title="Title3",
content="content3",
@@ -158,7 +158,7 @@ class TestTrashAPI(DirectoriesMixin, APITestCase):
self.assertEqual(resp.data["results"][1]["id"], document_u1.pk)
# superuser sees all documents
superuser = User.objects.create_superuser(username="superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
resp = self.client.get("/api/trash/")
self.assertEqual(resp.status_code, status.HTTP_200_OK)
@@ -174,7 +174,7 @@ class TestTrashAPI(DirectoriesMixin, APITestCase):
- 403 Forbidden
"""
user2 = User.objects.create_user(username="user2")
user2 = UserFactory(username="user2")
document = Document.objects.create(
title="Title",
content="content",
+5 -5
View File
@@ -1,13 +1,13 @@
import json
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import override_settings
from rest_framework import status
from rest_framework.test import APITestCase
from documents.tests.utils import DirectoriesMixin
from paperless.version import __full_version_str__
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
class TestApiUiSettings(DirectoriesMixin, APITestCase):
@@ -15,7 +15,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.test_user = User.objects.create_superuser(username="test")
self.test_user = UserFactory(username="test", superuser=True)
self.test_user.first_name = "Test"
self.test_user.last_name = "User"
self.test_user.save()
@@ -91,7 +91,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
)
def test_api_set_ui_settings_insufficient_global_permissions(self) -> None:
not_superuser = User.objects.create_user(username="test_not_superuser")
not_superuser = UserFactory(username="test_not_superuser")
self.client.force_authenticate(user=not_superuser)
settings = {
@@ -111,7 +111,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_api_set_ui_settings_sufficient_global_permissions(self) -> None:
not_superuser = User.objects.create_user(username="test_not_superuser")
not_superuser = UserFactory(username="test_not_superuser")
not_superuser.user_permissions.add(
*Permission.objects.filter(codename__contains="uisettings"),
)
+3 -2
View File
@@ -14,7 +14,8 @@ from documents.models import Tag
from documents.models import Workflow
from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
class TestApiWorkflows(DirectoriesMixin, APITestCase):
@@ -25,7 +26,7 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
user = User.objects.create_superuser(username="temp_admin")
user = UserFactory(username="temp_admin", superuser=True)
self.client.force_authenticate(user=user)
self.user2 = User.objects.create(username="user2")
self.user3 = User.objects.create(username="user3")
+1 -1
View File
@@ -18,11 +18,11 @@ from documents.models import Document
from documents.models import Tag
from documents.plugins.base import StopConsumeTaskError
from documents.tests.utils import ConsumeTaskMixin
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import DummyProgressManager
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin
from paperless.models import ApplicationConfiguration
from paperless_testing.dirs import DirectoriesMixin
class GetReaderPluginMixin:
+6 -6
View File
@@ -10,7 +10,6 @@ from django.contrib.auth.models import User
from django.db import connection
from django.test import TestCase
from django.test.utils import CaptureQueriesContext
from guardian.shortcuts import assign_perm
from guardian.shortcuts import get_groups_with_perms
from guardian.shortcuts import get_users_with_perms
@@ -23,7 +22,8 @@ from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.permissions import set_permissions_for_objects
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.permissions import grant_object
class TestBulkEdit(DirectoriesMixin, TestCase):
@@ -440,7 +440,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
def test_set_permissions(self, m) -> None:
doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id]
assign_perm("view_document", self.group1, self.doc1)
grant_object(self.group1, self.doc1, "view_document")
permissions = {
"view": {
@@ -482,8 +482,8 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
self.doc1.owner = self.user1
self.doc1.save()
assign_perm("view_document", self.user1, self.doc1)
assign_perm("view_document", self.group1, self.doc1)
grant_object(self.user1, self.doc1, "view_document")
grant_object(self.group1, self.doc1, "view_document")
permissions = {
"view": {
@@ -609,7 +609,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
self.doc1.owner = self.user1
self.doc1.save()
self.user1.groups.add(self.group1)
assign_perm("view_document", self.group1, self.doc1)
grant_object(self.group1, self.doc1, "view_document")
bulk_edit.set_permissions(
[self.doc1.id],
+3 -3
View File
@@ -28,12 +28,12 @@ from documents.models import DocumentType
from documents.models import MatchingModel
from documents.models import StoragePath
from documents.models import Tag
from documents.tests.factories import DocumentFactory
from documents.tests.factories import TagFactory
from documents.tests.utils import DirectoriesMixin
from paperless.settings import CLASSIFIER_LANGUAGES
from paperless.signed_pickle import HMAC_SIZE
from paperless.signed_pickle import signed_pickle_dumps
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import TagFactory
def dummy_preprocess(content: str) -> str:
+3 -2
View File
@@ -30,11 +30,12 @@ from documents.models import Tag
from documents.parsers import ParseError
from documents.plugins.helpers import ProgressStatusOptions
from documents.tasks import sanity_check
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import DummyProgressManager
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import GetConsumerMixin
from paperless_mail.models import MailRule
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
class _BaseNewStyleParser:
@@ -769,7 +770,7 @@ class TestConsumer(
original_modified = timezone.now() - datetime.timedelta(days=1)
Document.objects.filter(pk=root_doc.pk).update(modified=original_modified)
actor = User.objects.create_user(
actor = UserFactory(
username="actor",
email="actor@example.com",
password="password",
@@ -9,11 +9,11 @@ from django.test.utils import CaptureQueriesContext
from rest_framework import status
from documents.models import Document
from documents.tests.factories import DocumentFactory
from documents.versioning import LATEST_VERSION_CONTENT_PREFETCH_ATTR
from documents.versioning import has_prefetched_effective_content
from documents.versioning import latest_version_content_prefetch
from documents.views import DocumentViewSet
from paperless_testing.factories import DocumentFactory
if TYPE_CHECKING:
from rest_framework.test import APIClient
+32 -22
View File
@@ -15,9 +15,9 @@ from documents.data_models import ConsumableDocument
from documents.data_models import DocumentSource
from documents.double_sided import STAGING_FILE_NAME
from documents.double_sided import TIMEOUT_MINUTES
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import DummyProgressManager
from documents.tests.utils import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
@override_settings(
@@ -29,8 +29,8 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def setUp(self) -> None:
super().setUp()
self.dirs.double_sided_dir = self.dirs.consumption_dir / "double-sided"
self.dirs.double_sided_dir.mkdir()
self.double_sided_dir = self.dirs.consumption_dir / "double-sided"
self.double_sided_dir.mkdir()
self.staging_file = self.dirs.scratch_dir / STAGING_FILE_NAME
def consume_file(self, srcname, dstname: str | Path = "foo.pdf"):
@@ -39,7 +39,7 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
destination file does not exist afterwards
"""
src = self.SAMPLE_DIR / srcname
dst = self.dirs.double_sided_dir / dstname
dst = self.double_sided_dir / dstname
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst)
with mock.patch(
@@ -214,31 +214,41 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertIsNotFile(self.staging_file)
self.assertIsInstance(msg.get("document_id"), int)
def test_subdirectory_upload(self) -> None:
def test_consume_double_sided_in_nested_dir(self) -> None:
"""
GIVEN:
- A staging file exists
WHEN:
- A file gets uploaded into foo/bar/double-sided
or double-sided/foo/bar
- A file is uploaded into foo/bar/double-sided
THEN:
- The collated file gets put into foo/bar
- The collated file is placed into foo/bar
"""
# TODO: parameterize this instead
for path in [
Path("foo") / "bar" / "double-sided",
Path("double-sided") / "foo" / "bar",
]:
with self.subTest(path=str(path)):
# Ensure we get fresh directories for each run
self.tearDown()
self.setUp()
self.create_staging_file()
self.consume_file(
"double-sided-odd.pdf",
Path("foo") / "bar" / "double-sided" / "foo.pdf",
)
self.assertIsFile(
self.dirs.consumption_dir / "foo" / "bar" / "foo-collated.pdf",
)
self.create_staging_file()
self.consume_file("double-sided-odd.pdf", Path(path) / "foo.pdf")
self.assertIsFile(
self.dirs.consumption_dir / "foo" / "bar" / "foo-collated.pdf",
)
def test_consume_double_sided_with_nested_subdir(self) -> None:
"""
GIVEN:
- A staging file exists
WHEN:
- A file is uploaded into double-sided/foo/bar
THEN:
- The collated file is placed into foo/bar
"""
self.create_staging_file()
self.consume_file(
"double-sided-odd.pdf",
Path("double-sided") / "foo" / "bar" / "foo.pdf",
)
self.assertIsFile(
self.dirs.consumption_dir / "foo" / "bar" / "foo-collated.pdf",
)
@override_settings(CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=False)
def test_disabled_double_sided_dir_upload(self) -> None:
+4 -4
View File
@@ -8,7 +8,6 @@ from unittest import mock
import pytest
from auditlog.context import disable_auditlog
from django.conf import settings
from django.contrib.auth.models import User
from django.db import DatabaseError
from django.db import connection
from django.test import TestCase
@@ -30,9 +29,10 @@ from documents.models import DocumentType
from documents.models import StoragePath
from documents.serialisers import DocumentSerializer
from documents.tasks import empty_trash
from documents.tests.factories import DocumentFactory
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import UserFactory
class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
@@ -1323,7 +1323,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
- Document without owner returns "none"
"""
u1 = User.objects.create_user("user1")
u1 = UserFactory(username="user1")
owned_doc = Document.objects.create(
title="The Title",
+4 -4
View File
@@ -13,10 +13,10 @@ import pytest
from documents.file_handling import generate_filename
from documents.models import CustomField
from documents.models import CustomFieldInstance
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentFactory
from documents.tests.factories import StoragePathFactory
from documents.tests.factories import TagFactory
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import StoragePathFactory
from paperless_testing.factories import TagFactory
@pytest.mark.django_db
+1 -1
View File
@@ -20,8 +20,8 @@ if TYPE_CHECKING:
from documents.file_handling import generate_filename
from documents.models import Document
from documents.tasks import update_document_content_maybe_archive_file
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
sample_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
@@ -26,7 +26,6 @@ from django.test import override_settings
from django.utils import timezone
from guardian.models import GroupObjectPermission
from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm
from documents.management.commands import document_exporter
from documents.models import Correspondent
@@ -46,11 +45,12 @@ from documents.models import WorkflowTrigger
from documents.sanity_checker import check_sanity
from documents.settings import EXPORTER_FILE_NAME
from documents.settings import EXPORTER_SHARE_LINK_BUNDLE_NAME
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin
from documents.tests.utils import paperless_environment
from paperless_mail.models import MailAccount
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.dirs import paperless_environment
from paperless_testing.permissions import grant_object
@pytest.mark.management
@@ -105,8 +105,8 @@ class TestExportImport(
user=self.user,
)
assign_perm("view_document", self.user2, self.d2)
assign_perm("view_document", self.group1, self.d3)
grant_object(self.user2, self.d2, "view_document")
grant_object(self.group1, self.d3, "view_document")
self.t1 = Tag.objects.create(name="t")
self.dt1 = DocumentType.objects.create(name="dt")
+1 -1
View File
@@ -8,7 +8,7 @@ from django.core.management import call_command
from django.test import TestCase
from documents.models import Document
from documents.tests.factories import DocumentFactory
from paperless_testing.factories import DocumentFactory
@pytest.mark.management
@@ -15,9 +15,9 @@ from documents.management.commands.document_importer import _deserialize_record
from documents.models import Document
from documents.settings import EXPORTER_ARCHIVE_NAME
from documents.settings import EXPORTER_FILE_NAME
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin
from paperless_testing.dirs import DirectoriesMixin
@pytest.mark.management
@@ -14,12 +14,12 @@ from documents.models import DocumentType
from documents.models import MatchingModel
from documents.models import StoragePath
from documents.models import Tag
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentFactory
from documents.tests.factories import DocumentTypeFactory
from documents.tests.factories import StoragePathFactory
from documents.tests.factories import TagFactory
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import DocumentTypeFactory
from paperless_testing.factories import StoragePathFactory
from paperless_testing.factories import TagFactory
# ---------------------------------------------------------------------------
# Module-level type aliases
@@ -7,7 +7,7 @@ from django.contrib.auth.models import User
from django.core.management import call_command
from django.test import TestCase
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
@pytest.mark.management
@@ -9,8 +9,8 @@ from django.test import TestCase
from documents.management.commands.document_thumbnails import _process_document
from documents.models import Document
from documents.parsers import get_default_thumbnail
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
@pytest.mark.management
+4 -4
View File
@@ -7,10 +7,10 @@ from documents import matching
from documents.models import Document
from documents.models import MatchingModel
from documents.signals import document_consumption_finished
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentFactory
from documents.tests.factories import DocumentTypeFactory
from documents.tests.factories import TagFactory
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import DocumentTypeFactory
from paperless_testing.factories import TagFactory
@pytest.fixture(
@@ -2,8 +2,6 @@ import json
from unittest import mock
from auditlog.models import LogEntry
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from rest_framework import status
@@ -12,6 +10,8 @@ from rest_framework.test import APITestCase
from documents.bulk_edit import merge_as_versions
from documents.models import Document
from documents.serialisers import MergeDocumentsAsVersionsSerializer
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
class TestMergeDocumentsAsVersionsSerializer(TestCase):
@@ -294,7 +294,7 @@ class TestMergeDocumentsAsVersions(TestCase):
@mock.patch("documents.bulk_edit.bulk_update_documents.apply_async")
@mock.patch("documents.search.get_backend")
def test_writes_audit_log_entry(self, *_mocks) -> None:
user = User.objects.create_user(username="merger")
user = UserFactory(username="merger")
root = Document.objects.create(checksum="A", title="Root")
source = Document.objects.create(checksum="B", title="Source")
LogEntry.objects.all().delete()
@@ -335,12 +335,8 @@ class TestMergeDocumentsAsVersions(TestCase):
class TestMergeDocumentsAsVersionsAPI(APITestCase):
def setUp(self) -> None:
self.user = User.objects.create_user(username="user")
self.user.user_permissions.add(
Permission.objects.get(codename="change_document"),
Permission.objects.get(codename="view_document"),
Permission.objects.get(codename="delete_document"),
)
self.user = UserFactory(username="user")
grant_global(self.user, "change_document", "view_document", "delete_document")
self.doc1 = Document.objects.create(
checksum="A",
title="A",
@@ -382,7 +378,7 @@ class TestMergeDocumentsAsVersionsAPI(APITestCase):
@mock.patch("documents.views.bulk_edit.merge_as_versions")
def test_requires_change_permission(self, merge_mock) -> None:
merge_mock.__name__ = "merge_as_versions"
user = User.objects.create_user(username="no-change")
user = UserFactory(username="no-change")
self.doc1.owner = user
self.doc1.save()
self.doc2.owner = user
@@ -405,11 +401,8 @@ class TestMergeDocumentsAsVersionsAPI(APITestCase):
def test_requires_delete_permission(self, merge_mock) -> None:
merge_mock.__name__ = "merge_as_versions"
# Owns them and may change them, but may not make them stop being documents
user = User.objects.create_user(username="no-delete")
user.user_permissions.add(
Permission.objects.get(codename="change_document"),
Permission.objects.get(codename="view_document"),
)
user = UserFactory(username="no-delete")
grant_global(user, "change_document", "view_document")
for doc in (self.doc1, self.doc2):
doc.owner = user
doc.save()
@@ -1,5 +1,4 @@
import hashlib
import shutil
import tempfile
from pathlib import Path
@@ -19,19 +18,22 @@ class TestSha256ChecksumDataMigration(TestMigrations):
migrate_from = "0015_document_version_index_and_more"
migrate_to = "0016_sha256_checksums"
migrate_once = True
reset_sequences = True
ORIGINAL_CONTENT = b"original file content for sha256 migration test"
ARCHIVE_CONTENT = b"archive file content for sha256 migration test"
def setUpBeforeMigration(self, apps) -> None:
self._originals_dir = Path(tempfile.mkdtemp())
self._archive_dir = Path(tempfile.mkdtemp())
self._settings_override = override_settings(
ORIGINALS_DIR=self._originals_dir,
ARCHIVE_DIR=self._archive_dir,
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
originals_dir = Path(cls.enterClassContext(tempfile.TemporaryDirectory()))
archive_dir = Path(cls.enterClassContext(tempfile.TemporaryDirectory()))
cls.enterClassContext(
override_settings(ORIGINALS_DIR=originals_dir, ARCHIVE_DIR=archive_dir),
)
self._settings_override.enable()
def setUpBeforeMigration(self, apps) -> None:
Document = apps.get_model("documents", "Document")
# doc1: original file present, no archive
@@ -85,8 +87,9 @@ class TestSha256ChecksumDataMigration(TestMigrations):
archive_checksum=None,
).pk
def _fixture_teardown(self) -> None:
super()._fixture_teardown()
@classmethod
def tearDownClass(cls) -> None:
super().tearDownClass()
# Django's SQLite backend returns [] from sequence_reset_sql(), so
# reset_sequences=True flushes rows but never clears sqlite_sequence.
# Explicitly delete the entry so subsequent tests start from pk=1.
@@ -96,12 +99,6 @@ class TestSha256ChecksumDataMigration(TestMigrations):
"DELETE FROM sqlite_sequence WHERE name='documents_document'",
)
def tearDown(self) -> None:
super().tearDown()
self._settings_override.disable()
shutil.rmtree(self._originals_dir, ignore_errors=True)
shutil.rmtree(self._archive_dir, ignore_errors=True)
def test_original_checksum_updated_to_sha256_when_file_exists(self) -> None:
Document = self.apps.get_model("documents", "Document")
doc = Document.objects.get(pk=self.doc1_id)
+2 -2
View File
@@ -2,8 +2,8 @@ import pytest
from documents.models import Correspondent
from documents.models import Document
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentFactory
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import DocumentFactory
@pytest.mark.django_db
@@ -1,15 +1,13 @@
from __future__ import annotations
from http import HTTPStatus
from typing import TYPE_CHECKING
from unittest.mock import patch
import pytest
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import override_settings
from guardian.shortcuts import assign_perm
from rest_framework.test import APIClient
from documents.matching import match_correspondents
@@ -24,11 +22,17 @@ from documents.permissions import permitted_document_ids
from documents.permissions import permitted_object_ids
from documents.permissions import restrict_queryset_to_visible
from documents.serialisers import _get_viewable_duplicates
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentFactory
from documents.tests.factories import DocumentTypeFactory
from documents.tests.factories import StoragePathFactory
from documents.tests.factories import TagFactory
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import DocumentTypeFactory
from paperless_testing.factories import StoragePathFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
if TYPE_CHECKING:
from paperless_testing.dirs import PaperlessDirs
def assert_visible_document_ids(actual_ids, *, expected_visible, expected_hidden):
@@ -47,8 +51,8 @@ def assert_visible_document_ids(actual_ids, *, expected_visible, expected_hidden
@pytest.mark.django_db
class TestPermittedDocumentIdsSecurity:
def test_owner_sees_own_document(self):
user = User.objects.create_user(username="alice")
stranger = User.objects.create_user(username="mallory")
user = UserFactory(username="alice")
stranger = UserFactory(username="mallory")
owned = DocumentFactory(owner=user)
strangers_doc = DocumentFactory(owner=stranger)
@@ -61,7 +65,7 @@ class TestPermittedDocumentIdsSecurity:
)
def test_unowned_document_visible_to_everyone(self):
user = User.objects.create_user(username="alice")
user = UserFactory(username="alice")
unowned = DocumentFactory(owner=None)
assert_visible_document_ids(
@@ -71,12 +75,12 @@ class TestPermittedDocumentIdsSecurity:
)
def test_explicit_user_permission_grants_visibility(self):
grantee = User.objects.create_user(username="alice")
stranger = User.objects.create_user(username="mallory")
owner = User.objects.create_user(username="owner")
grantee = UserFactory(username="alice")
stranger = UserFactory(username="mallory")
owner = UserFactory(username="owner")
shared = DocumentFactory(owner=owner)
not_shared = DocumentFactory(owner=owner)
assign_perm("view_document", grantee, shared)
grant_object(grantee, shared, "view_document")
assert_visible_document_ids(
permitted_document_ids(grantee),
@@ -90,13 +94,13 @@ class TestPermittedDocumentIdsSecurity:
)
def test_explicit_group_permission_grants_visibility_to_members_only(self):
owner = User.objects.create_user(username="owner")
member = User.objects.create_user(username="member")
non_member = User.objects.create_user(username="non_member")
owner = UserFactory(username="owner")
member = UserFactory(username="member")
non_member = UserFactory(username="non_member")
group = Group.objects.create(name="finance")
member.groups.add(group)
shared = DocumentFactory(owner=owner)
assign_perm("view_document", group, shared)
grant_object(group, shared, "view_document")
assert_visible_document_ids(
permitted_document_ids(member),
@@ -110,7 +114,7 @@ class TestPermittedDocumentIdsSecurity:
)
def test_soft_deleted_document_excluded_by_default(self):
owner = User.objects.create_user(username="owner")
owner = UserFactory(username="owner")
doc = DocumentFactory(owner=owner)
doc.delete() # soft delete
doc.refresh_from_db()
@@ -126,8 +130,8 @@ class TestPermittedDocumentIdsSecurity:
)
def test_superuser_sees_everything_including_no_perm_documents(self):
superuser = User.objects.create_superuser(username="root")
owner = User.objects.create_user(username="owner")
superuser = UserFactory(username="root", superuser=True)
owner = UserFactory(username="owner")
doc = DocumentFactory(owner=owner)
assert_visible_document_ids(
@@ -137,7 +141,7 @@ class TestPermittedDocumentIdsSecurity:
)
def test_anonymous_user_sees_only_unowned_documents(self):
owner = User.objects.create_user(username="owner")
owner = UserFactory(username="owner")
owned = DocumentFactory(owner=owner)
unowned = DocumentFactory(owner=None)
@@ -151,7 +155,7 @@ class TestPermittedDocumentIdsSecurity:
@pytest.mark.django_db
class TestPermittedDocumentIdsIncludeDeleted:
def test_include_deleted_true_reveals_soft_deleted_owned_document(self):
owner = User.objects.create_user(username="owner")
owner = UserFactory(username="owner")
doc = DocumentFactory(owner=owner)
doc.delete()
@@ -162,8 +166,8 @@ class TestPermittedDocumentIdsIncludeDeleted:
)
def test_include_deleted_true_still_respects_permission_boundary(self):
owner = User.objects.create_user(username="owner")
stranger = User.objects.create_user(username="mallory")
owner = UserFactory(username="owner")
stranger = UserFactory(username="mallory")
doc = DocumentFactory(owner=owner)
doc.delete()
@@ -191,14 +195,12 @@ class TestAiChatAllDocumentsPermissionBoundary:
def test_chat_all_documents_excludes_unshared_document(self, mock_stream_chat):
mock_stream_chat.return_value = iter([b"data"])
owner = User.objects.create_user(username="owner")
asker = User.objects.create_user(username="asker")
asker.user_permissions.add(
*Permission.objects.filter(codename="view_document"),
)
owner = UserFactory(username="owner")
asker = UserFactory(username="asker")
grant_global(asker, "view_document")
shared = DocumentFactory(owner=owner)
not_shared = DocumentFactory(owner=owner)
assign_perm("view_document", asker, shared)
grant_object(asker, shared, "view_document")
client = APIClient()
client.force_authenticate(user=asker)
@@ -219,13 +221,13 @@ class TestAiChatAllDocumentsPermissionBoundary:
@pytest.mark.django_db
class TestDuplicateDocumentsPermissionBoundary:
def test_get_viewable_duplicates_includes_soft_deleted_but_respects_perms(self):
owner = User.objects.create_user(username="owner")
stranger = User.objects.create_user(username="mallory")
owner = UserFactory(username="owner")
stranger = UserFactory(username="mallory")
original = DocumentFactory(owner=owner, checksum="dupe-checksum")
dup_visible = DocumentFactory(owner=owner, checksum="dupe-checksum")
dup_hidden = DocumentFactory(owner=owner, checksum="dupe-checksum")
dup_hidden.delete() # soft delete, should still be found (include_deleted=True)
assign_perm("view_document", stranger, dup_visible)
grant_object(stranger, dup_visible, "view_document")
result_owner = _get_viewable_duplicates(original, owner)
assert {d.pk for d in result_owner} == {dup_visible.pk, dup_hidden.pk}
@@ -237,13 +239,13 @@ class TestDuplicateDocumentsPermissionBoundary:
@pytest.mark.django_db
class TestPermittedDocumentIdsArbitraryPermission:
def test_change_document_permission_is_distinct_from_view(self):
owner = User.objects.create_user(username="owner")
viewer_only = User.objects.create_user(username="viewer")
editor = User.objects.create_user(username="editor")
owner = UserFactory(username="owner")
viewer_only = UserFactory(username="viewer")
editor = UserFactory(username="editor")
doc = DocumentFactory(owner=owner)
assign_perm("view_document", viewer_only, doc)
assign_perm("change_document", editor, doc)
assign_perm("view_document", editor, doc)
grant_object(viewer_only, doc, "view_document")
grant_object(editor, doc, "change_document")
grant_object(editor, doc, "view_document")
assert_visible_document_ids(
permitted_document_ids(editor, perm="change_document"),
@@ -257,10 +259,10 @@ class TestPermittedDocumentIdsArbitraryPermission:
)
def test_qualified_permission_string_is_normalized_to_codename(self):
owner = User.objects.create_user(username="owner")
editor = User.objects.create_user(username="editor")
owner = UserFactory(username="owner")
editor = UserFactory(username="editor")
doc = DocumentFactory(owner=owner)
assign_perm("change_document", editor, doc)
grant_object(editor, doc, "change_document")
assert_visible_document_ids(
permitted_document_ids(editor, perm="documents.change_document"),
@@ -269,11 +271,11 @@ class TestPermittedDocumentIdsArbitraryPermission:
)
def test_delete_permission_with_include_deleted_for_trash_restore(self):
owner = User.objects.create_user(username="owner")
stranger = User.objects.create_user(username="mallory")
view_only = User.objects.create_user(username="viewer")
owner = UserFactory(username="owner")
stranger = UserFactory(username="mallory")
view_only = UserFactory(username="viewer")
doc = DocumentFactory(owner=owner)
assign_perm("view_document", view_only, doc)
grant_object(view_only, doc, "view_document")
doc.delete()
assert_visible_document_ids(
@@ -307,11 +309,9 @@ class TestEmailDocumentPermissionBoundary:
self,
rest_api_client,
):
owner = User.objects.create_user(username="owner")
requester = User.objects.create_user(username="requester")
requester.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
owner = UserFactory(username="owner")
requester = UserFactory(username="requester")
grant_global(requester, "view_document")
rest_api_client.force_authenticate(user=requester)
hidden = DocumentFactory(owner=owner)
@@ -339,19 +339,17 @@ class TestBulkEditChangePermissionBoundary:
# permitted document must not be partially applied just because it
# was bundled with a forbidden one, proving the endpoint checks
# every document in the batch rather than only the first/last.
owner = User.objects.create_user(username="owner")
requester = User.objects.create_user(username="requester")
owner = UserFactory(username="owner")
requester = UserFactory(username="requester")
# grant the global change_document permission so the object-level
# check (not the global has_perm check) is what's under test
requester.user_permissions.add(
Permission.objects.get(codename="change_document"),
)
grant_global(requester, "change_document")
rest_api_client.force_authenticate(user=requester)
changeable = DocumentFactory(owner=owner)
assign_perm("view_document", requester, changeable)
assign_perm("change_document", requester, changeable) # fully permitted
grant_object(requester, changeable, "view_document")
grant_object(requester, changeable, "change_document") # fully permitted
target = DocumentFactory(owner=owner)
assign_perm("view_document", requester, target) # view only, NOT change
grant_object(requester, target, "view_document") # view only, NOT change
response = rest_api_client.post(
"/api/documents/bulk_edit/",
@@ -369,15 +367,14 @@ class TestBulkEditChangePermissionBoundary:
class TestBulkDownloadPermissionChecksRootDocument:
def test_download_requires_global_view_permission(
self,
rest_api_client,
paperless_dirs,
_media_settings,
):
owner = User.objects.create_user(username="owner")
requester = User.objects.create_user(username="requester")
rest_api_client: APIClient,
paperless_dirs: PaperlessDirs,
) -> None:
owner = UserFactory(username="owner")
requester = UserFactory(username="requester")
root = DocumentFactory(owner=owner)
root.source_path.write_bytes(b"%PDF-1.4 test")
assign_perm("view_document", requester, root)
grant_object(requester, root, "view_document")
rest_api_client.force_authenticate(user=requester)
response = rest_api_client.post(
@@ -390,21 +387,18 @@ class TestBulkDownloadPermissionChecksRootDocument:
def test_permission_checked_on_root_not_on_version(
self,
rest_api_client,
paperless_dirs,
_media_settings,
):
owner = User.objects.create_user(username="owner")
requester = User.objects.create_user(username="requester")
requester.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
rest_api_client: APIClient,
paperless_dirs: PaperlessDirs,
) -> None:
owner = UserFactory(username="owner")
requester = UserFactory(username="requester")
grant_global(requester, "view_document")
rest_api_client.force_authenticate(user=requester)
root = DocumentFactory(owner=owner)
# a version of root that the requester has NOT been individually granted
version = DocumentFactory(owner=owner, root_document=root, version_index=1)
version.source_path.write_bytes(b"%PDF-1.4 test")
assign_perm("view_document", requester, root) # granted on ROOT only
grant_object(requester, root, "view_document") # granted on ROOT only
response = rest_api_client.post(
"/api/documents/bulk_download/",
@@ -422,11 +416,9 @@ class TestBulkDownloadPermissionChecksRootDocument:
# root-or-version bug; a user with no grant at all (the old
# `stranger` case) can't tell the two apart, since they're denied
# either way.
version_only_grantee = User.objects.create_user(username="version_only_grantee")
version_only_grantee.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
assign_perm("view_document", version_only_grantee, version)
version_only_grantee = UserFactory(username="version_only_grantee")
grant_global(version_only_grantee, "view_document")
grant_object(version_only_grantee, version, "view_document")
rest_api_client.force_authenticate(user=version_only_grantee)
response = rest_api_client.post(
"/api/documents/bulk_download/",
@@ -445,14 +437,12 @@ class TestTrashRestorePermissionBoundary:
self,
rest_api_client,
):
owner = User.objects.create_user(username="owner")
requester = User.objects.create_user(username="requester")
requester.user_permissions.add(
Permission.objects.get(codename="delete_document"),
)
owner = UserFactory(username="owner")
requester = UserFactory(username="requester")
grant_global(requester, "delete_document")
rest_api_client.force_authenticate(user=requester)
doc = DocumentFactory(owner=owner)
assign_perm("view_document", requester, doc) # view only, NOT delete
grant_object(requester, doc, "view_document") # view only, NOT delete
doc.delete()
response = rest_api_client.post(
@@ -466,14 +456,12 @@ class TestTrashRestorePermissionBoundary:
self,
rest_api_client,
):
owner = User.objects.create_user(username="owner")
requester = User.objects.create_user(username="requester")
requester.user_permissions.add(
Permission.objects.get(codename="delete_document"),
)
owner = UserFactory(username="owner")
requester = UserFactory(username="requester")
grant_global(requester, "delete_document")
rest_api_client.force_authenticate(user=requester)
doc = DocumentFactory(owner=owner)
assign_perm("delete_document", requester, doc)
grant_object(requester, doc, "delete_document")
doc.delete()
response = rest_api_client.post(
@@ -484,11 +472,11 @@ class TestTrashRestorePermissionBoundary:
assert response.status_code == HTTPStatus.OK
def test_restore_requires_global_delete_permission(self, rest_api_client):
owner = User.objects.create_user(username="owner")
requester = User.objects.create_user(username="requester")
owner = UserFactory(username="owner")
requester = UserFactory(username="requester")
rest_api_client.force_authenticate(user=requester)
doc = DocumentFactory(owner=owner)
assign_perm("delete_document", requester, doc)
grant_object(requester, doc, "delete_document")
doc.delete()
response = rest_api_client.post(
@@ -513,14 +501,12 @@ class TestTrashViewExcludesExplicitlyGrantedDocuments:
"""
def test_explicit_grant_does_not_leak_trashed_document(self, rest_api_client):
owner = User.objects.create_user(username="trash_owner")
grantee = User.objects.create_user(username="trash_grantee")
grantee.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
owner = UserFactory(username="trash_owner")
grantee = UserFactory(username="trash_grantee")
grant_global(grantee, "view_document")
doc = DocumentFactory(owner=owner)
doc.delete() # soft delete
assign_perm("view_document", grantee, doc)
grant_object(grantee, doc, "view_document")
rest_api_client.force_authenticate(user=grantee)
response = rest_api_client.get("/api/trash/")
@@ -542,8 +528,8 @@ class TestTrashViewExcludesExplicitlyGrantedDocuments:
)
class TestPermittedObjectIdsGenericModels:
def test_owner_sees_own_object(self, model, factory, perm):
owner = User.objects.create_user(username=f"owner_{model.__name__}")
stranger = User.objects.create_user(username=f"stranger_{model.__name__}")
owner = UserFactory(username=f"owner_{model.__name__}")
stranger = UserFactory(username=f"stranger_{model.__name__}")
owned = factory(owner=owner)
strangers = factory(owner=stranger)
@@ -556,14 +542,14 @@ class TestPermittedObjectIdsGenericModels:
@pytest.mark.parametrize("is_superuser", [False, True])
def test_inactive_user_sees_nothing(self, model, factory, perm, is_superuser):
suffix = f"{model.__name__}_{is_superuser}"
user = User.objects.create_user(
user = UserFactory(
username=f"inactive_{suffix}",
is_active=False,
is_superuser=is_superuser,
)
other = User.objects.create_user(username=f"other_{suffix}")
other = UserFactory(username=f"other_{suffix}")
granted = factory(owner=other)
assign_perm(perm, user, granted)
grant_object(user, granted, perm)
assert_visible_document_ids(
permitted_object_ids(user, model, perm),
@@ -576,7 +562,7 @@ class TestPermittedObjectIdsGenericModels:
)
def test_unowned_object_visible_to_everyone(self, model, factory, perm):
user = User.objects.create_user(username=f"user_{model.__name__}")
user = UserFactory(username=f"user_{model.__name__}")
unowned = factory(owner=None)
assert_visible_document_ids(
@@ -586,12 +572,12 @@ class TestPermittedObjectIdsGenericModels:
)
def test_explicit_permission_grants_visibility(self, model, factory, perm):
owner = User.objects.create_user(username=f"owner2_{model.__name__}")
grantee = User.objects.create_user(username=f"grantee_{model.__name__}")
stranger = User.objects.create_user(username=f"stranger2_{model.__name__}")
owner = UserFactory(username=f"owner2_{model.__name__}")
grantee = UserFactory(username=f"grantee_{model.__name__}")
stranger = UserFactory(username=f"stranger2_{model.__name__}")
shared = factory(owner=owner)
not_shared = factory(owner=owner)
assign_perm(perm, grantee, shared)
grant_object(grantee, shared, perm)
assert_visible_document_ids(
permitted_object_ids(grantee, model, perm),
@@ -610,13 +596,13 @@ class TestPermittedObjectIdsGenericModels:
factory,
perm,
):
owner = User.objects.create_user(username=f"owner3_{model.__name__}")
member = User.objects.create_user(username=f"member_{model.__name__}")
non_member = User.objects.create_user(username=f"nonmember_{model.__name__}")
owner = UserFactory(username=f"owner3_{model.__name__}")
member = UserFactory(username=f"member_{model.__name__}")
non_member = UserFactory(username=f"nonmember_{model.__name__}")
group = Group.objects.create(name=f"group_{model.__name__}")
member.groups.add(group)
shared = factory(owner=owner)
assign_perm(perm, group, shared)
grant_object(group, shared, perm)
assert_visible_document_ids(
permitted_object_ids(member, model, perm),
@@ -630,8 +616,8 @@ class TestPermittedObjectIdsGenericModels:
)
def test_superuser_sees_everything(self, model, factory, perm):
superuser = User.objects.create_superuser(username=f"root_{model.__name__}")
owner = User.objects.create_user(username=f"owner4_{model.__name__}")
superuser = UserFactory(username=f"root_{model.__name__}", superuser=True)
owner = UserFactory(username=f"owner4_{model.__name__}")
obj = factory(owner=owner)
assert_visible_document_ids(
@@ -644,8 +630,8 @@ class TestPermittedObjectIdsGenericModels:
@pytest.mark.django_db
class TestMatchingRespectsObjectPermissions:
def test_match_tags_only_considers_tags_visible_to_user(self):
owner = User.objects.create_user(username="tag_owner")
classifying_user = User.objects.create_user(username="classifier_user")
owner = UserFactory(username="tag_owner")
classifying_user = UserFactory(username="classifier_user")
visible_tag = TagFactory(
owner=owner,
match="invoice",
@@ -656,7 +642,7 @@ class TestMatchingRespectsObjectPermissions:
match="invoice",
matching_algorithm=Tag.MATCH_LITERAL,
)
assign_perm("view_tag", classifying_user, visible_tag)
grant_object(classifying_user, visible_tag, "view_tag")
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
matched = match_tags(doc, classifier=None, user=classifying_user)
@@ -665,8 +651,8 @@ class TestMatchingRespectsObjectPermissions:
assert hidden_tag.pk not in matched_ids
def test_match_correspondents_only_considers_correspondents_visible_to_user(self):
owner = User.objects.create_user(username="correspondent_owner")
classifying_user = User.objects.create_user(username="classifier_user2")
owner = UserFactory(username="correspondent_owner")
classifying_user = UserFactory(username="classifier_user2")
visible_correspondent = CorrespondentFactory(
owner=owner,
match="invoice",
@@ -677,7 +663,7 @@ class TestMatchingRespectsObjectPermissions:
match="invoice",
matching_algorithm=Correspondent.MATCH_LITERAL,
)
assign_perm("view_correspondent", classifying_user, visible_correspondent)
grant_object(classifying_user, visible_correspondent, "view_correspondent")
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
matched = match_correspondents(doc, classifier=None, user=classifying_user)
@@ -686,8 +672,8 @@ class TestMatchingRespectsObjectPermissions:
assert hidden_correspondent.pk not in matched_ids
def test_match_document_types_only_considers_document_types_visible_to_user(self):
owner = User.objects.create_user(username="document_type_owner")
classifying_user = User.objects.create_user(username="classifier_user3")
owner = UserFactory(username="document_type_owner")
classifying_user = UserFactory(username="classifier_user3")
visible_document_type = DocumentTypeFactory(
owner=owner,
match="invoice",
@@ -698,7 +684,7 @@ class TestMatchingRespectsObjectPermissions:
match="invoice",
matching_algorithm=DocumentType.MATCH_LITERAL,
)
assign_perm("view_documenttype", classifying_user, visible_document_type)
grant_object(classifying_user, visible_document_type, "view_documenttype")
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
matched = match_document_types(doc, classifier=None, user=classifying_user)
@@ -707,8 +693,8 @@ class TestMatchingRespectsObjectPermissions:
assert hidden_document_type.pk not in matched_ids
def test_match_storage_paths_only_considers_storage_paths_visible_to_user(self):
owner = User.objects.create_user(username="storage_path_owner")
classifying_user = User.objects.create_user(username="classifier_user4")
owner = UserFactory(username="storage_path_owner")
classifying_user = UserFactory(username="classifier_user4")
visible_storage_path = StoragePathFactory(
owner=owner,
match="invoice",
@@ -719,7 +705,7 @@ class TestMatchingRespectsObjectPermissions:
match="invoice",
matching_algorithm=StoragePath.MATCH_LITERAL,
)
assign_perm("view_storagepath", classifying_user, visible_storage_path)
grant_object(classifying_user, visible_storage_path, "view_storagepath")
doc = DocumentFactory(owner=classifying_user, content="an invoice document")
matched = match_storage_paths(doc, classifier=None, user=classifying_user)
@@ -731,14 +717,12 @@ class TestMatchingRespectsObjectPermissions:
@pytest.mark.django_db
class TestBulkEditObjectsApplyToAllPermissionBoundary:
def test_apply_to_all_tags_excludes_unpermitted_tag(self, rest_api_client):
owner = User.objects.create_user(username="tags_owner")
requester = User.objects.create_user(username="tags_requester")
new_owner = User.objects.create_user(username="tags_new_owner")
owner = UserFactory(username="tags_owner")
requester = UserFactory(username="tags_requester")
new_owner = UserFactory(username="tags_new_owner")
# grant the global change_tag permission so the object-level
# filtering (not the global has_perm check) is what's under test
requester.user_permissions.add(
Permission.objects.get(codename="change_tag"),
)
grant_global(requester, "change_tag")
rest_api_client.force_authenticate(user=requester)
visible = TagFactory(owner=requester)
hidden = TagFactory(owner=owner)
@@ -771,16 +755,14 @@ class TestBulkEditObjectsApplyToAllPermissionBoundary:
request rather than being silently skipped. Editing permissions is
limited to the owner, same as documents.
"""
owner = User.objects.create_user(username="shared_tags_owner")
requester = User.objects.create_user(username="shared_tags_requester")
requester.user_permissions.add(
Permission.objects.get(codename="change_tag"),
)
owner = UserFactory(username="shared_tags_owner")
requester = UserFactory(username="shared_tags_requester")
grant_global(requester, "change_tag")
rest_api_client.force_authenticate(user=requester)
owned = TagFactory(owner=requester)
shared = TagFactory(owner=owner)
assign_perm("view_tag", requester, shared)
assign_perm("change_tag", requester, shared)
grant_object(requester, shared, "view_tag")
grant_object(requester, shared, "change_tag")
response = rest_api_client.post(
"/api/bulk_edit_objects/",
@@ -831,14 +813,12 @@ class TestBulkEditObjectsTagDescendantPartialPermission:
would pass/fail based on FK cascade behavior, not on whether the
descendant-expansion logic itself respected per-object permissions.
"""
owner = User.objects.create_user(username="tag_hierarchy_owner")
requester = User.objects.create_user(username="tag_hierarchy_requester")
new_owner = User.objects.create_user(username="tag_hierarchy_new_owner")
owner = UserFactory(username="tag_hierarchy_owner")
requester = UserFactory(username="tag_hierarchy_requester")
new_owner = UserFactory(username="tag_hierarchy_new_owner")
# global change_tag permission so the has_perm() gate passes and the
# object-level permitted_object_ids filtering is what's under test
requester.user_permissions.add(
Permission.objects.get(codename="change_tag"),
)
grant_global(requester, "change_tag")
rest_api_client.force_authenticate(user=requester)
parent = TagFactory(owner=requester, name="parent-tag")
@@ -890,7 +870,7 @@ class TestRestrictQuerysetToVisible:
- The queryset is returned unfiltered, rather than
permitted_object_ids(None, ...)'s narrower "unowned rows only"
"""
owner = User.objects.create_user(username="vis_none_owner")
owner = UserFactory(username="vis_none_owner")
tag = TagFactory(owner=owner)
visible = restrict_queryset_to_visible(Tag.objects.all(), None, "view_tag")
@@ -907,8 +887,8 @@ class TestRestrictQuerysetToVisible:
- The queryset is returned unfiltered, skipping the permission
lookup entirely
"""
superuser = User.objects.create_superuser(username="vis_active_super")
owner = User.objects.create_user(username="vis_active_super_owner")
superuser = UserFactory(username="vis_active_super", superuser=True)
owner = UserFactory(username="vis_active_super_owner")
tag = TagFactory(owner=owner)
visible = restrict_queryset_to_visible(
@@ -930,7 +910,7 @@ class TestRestrictQuerysetToVisible:
deactivation has to win over the superuser shortcut, matching
permitted_object_ids's own ordering
"""
user = User.objects.create_user(
user = UserFactory(
username="vis_inactive_super",
is_active=False,
is_superuser=True,
@@ -951,8 +931,8 @@ class TestRestrictQuerysetToVisible:
THEN:
- Only the rows permitted_object_ids() reports are visible
"""
user = User.objects.create_user(username="vis_regular")
other = User.objects.create_user(username="vis_regular_other")
user = UserFactory(username="vis_regular")
other = UserFactory(username="vis_regular_other")
own = TagFactory(owner=user)
hidden = TagFactory(owner=other)
@@ -1,11 +1,11 @@
import pytest
from django.contrib.auth.models import User
from guardian.shortcuts import assign_perm
from rest_framework.test import APIRequestFactory
from documents.filters import PermittedObjectsFilter
from documents.models import Tag
from documents.tests.factories import TagFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_object
class _DummyView:
@@ -15,8 +15,8 @@ class _DummyView:
@pytest.mark.django_db
class TestPermittedObjectsFilter:
def test_superuser_bypasses_filtering_entirely(self):
superuser = User.objects.create_superuser(username="root")
owner = User.objects.create_user(username="owner")
superuser = UserFactory(username="root", superuser=True)
owner = UserFactory(username="owner")
TagFactory(owner=owner)
request = APIRequestFactory().get("/")
request.user = superuser
@@ -29,13 +29,13 @@ class TestPermittedObjectsFilter:
assert result.count() == Tag.objects.count()
def test_non_superuser_sees_only_owned_unowned_and_granted(self):
owner = User.objects.create_user(username="owner")
grantee = User.objects.create_user(username="grantee")
owner = UserFactory(username="owner")
grantee = UserFactory(username="grantee")
owned = TagFactory(owner=grantee)
unowned = TagFactory(owner=None)
granted = TagFactory(owner=owner)
hidden = TagFactory(owner=owner)
assign_perm("view_tag", grantee, granted)
grant_object(grantee, granted, "view_tag")
request = APIRequestFactory().get("/")
request.user = grantee
@@ -49,11 +49,11 @@ class TestPermittedObjectsFilter:
assert hidden.pk not in visible_ids
def test_include_granted_false_excludes_explicitly_shared_objects(self):
owner = User.objects.create_user(username="owner2")
grantee = User.objects.create_user(username="grantee2")
owner = UserFactory(username="owner2")
grantee = UserFactory(username="grantee2")
owned = TagFactory(owner=grantee)
granted = TagFactory(owner=owner)
assign_perm("view_tag", grantee, granted)
grant_object(grantee, granted, "view_tag")
request = APIRequestFactory().get("/")
request.user = grantee
@@ -74,15 +74,15 @@ class TestPermittedObjectsFilter:
[("inactive", False), ("inactive_super", True)],
)
def test_inactive_user_sees_nothing(self, username: str, *, is_superuser: bool):
user = User.objects.create_user(
user = UserFactory(
username=username,
is_active=False,
is_superuser=is_superuser,
)
TagFactory(owner=None)
TagFactory(owner=user)
granted = TagFactory(owner=User.objects.create_user(username=f"o_{username}"))
assign_perm("view_tag", user, granted)
granted = TagFactory(owner=UserFactory(username=f"o_{username}"))
grant_object(user, granted, "view_tag")
request = APIRequestFactory().get("/")
request.user = user
@@ -94,7 +94,7 @@ class TestPermittedObjectsFilter:
assert result.count() == 0
def test_inactive_user_sees_nothing_with_include_granted_false(self):
user = User.objects.create_user(username="inactive_owner", is_active=False)
user = UserFactory(username="inactive_owner", is_active=False)
TagFactory(owner=user)
TagFactory(owner=None)
request = APIRequestFactory().get("/")
+8 -9
View File
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
from collections.abc import Iterable
from documents.models import Document
from documents.tests.conftest import PaperlessDirs
from paperless_testing.dirs import PaperlessDirs
class TestSanityCheckMessages:
@@ -46,14 +46,14 @@ class TestSanityCheckMessages:
class TestCheckSanityNoDocuments:
"""Sanity checks against an empty archive."""
@pytest.mark.usefixtures("_media_settings")
@pytest.mark.usefixtures("paperless_dirs")
def test_no_documents(self) -> None:
messages = check_sanity()
assert not messages.has_error
assert not messages.has_warning
assert messages.total_issue_count == 0
@pytest.mark.usefixtures("_media_settings")
@pytest.mark.usefixtures("paperless_dirs")
def test_no_issues_logs_clean(self, caplog: pytest.LogCaptureFixture) -> None:
messages = check_sanity()
with caplog.at_level(logging.INFO, logger="paperless.sanity_checker"):
@@ -214,18 +214,17 @@ class TestCheckSanityOrphans:
sample_doc: Document,
paperless_dirs: PaperlessDirs,
) -> None:
(paperless_dirs.originals / "orphan.pdf").touch()
(paperless_dirs.originals_dir / "orphan.pdf").touch()
messages = check_sanity()
assert messages.has_warning
assert any("Orphaned file" in m["message"] for m in messages[None])
@pytest.mark.usefixtures("_media_settings")
def test_ignorable_files_not_flagged(
self,
paperless_dirs: PaperlessDirs,
) -> None:
(paperless_dirs.media / ".DS_Store").touch()
(paperless_dirs.media / "desktop.ini").touch()
(paperless_dirs.media_dir / ".DS_Store").touch()
(paperless_dirs.media_dir / "desktop.ini").touch()
messages = check_sanity()
assert not messages.has_warning
@@ -269,13 +268,13 @@ class TestCheckSanityLogMessages:
paperless_dirs: PaperlessDirs,
caplog: pytest.LogCaptureFixture,
) -> None:
(paperless_dirs.originals / "orphan.pdf").touch()
(paperless_dirs.originals_dir / "orphan.pdf").touch()
messages = check_sanity()
with caplog.at_level(logging.WARNING, logger="paperless.sanity_checker"):
messages.log_messages()
assert "Orphaned file" in caplog.text
@pytest.mark.usefixtures("_media_settings")
@pytest.mark.usefixtures("paperless_dirs")
def test_logs_unknown_doc_pk(self, caplog: pytest.LogCaptureFixture) -> None:
"""A doc PK not in the DB logs 'Unknown' as the title."""
messages = check_sanity()
+11 -14
View File
@@ -6,10 +6,8 @@ from pathlib import Path
from unittest import mock
from django.conf import settings
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.utils import timezone
from guardian.shortcuts import assign_perm
from rest_framework import serializers
from rest_framework import status
from rest_framework.test import APITestCase
@@ -20,8 +18,11 @@ from documents.models import ShareLinkBundle
from documents.serialisers import ShareLinkBundleSerializer
from documents.tasks import build_share_link_bundle
from documents.tasks import cleanup_expired_share_link_bundles
from documents.tests.factories import DocumentFactory
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
@@ -29,7 +30,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="bundle_admin")
self.user = UserFactory(username="bundle_admin", superuser=True)
self.client.force_authenticate(self.user)
self.document = DocumentFactory.create()
@@ -55,13 +56,11 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
self,
delay_mock,
) -> None:
owner = User.objects.create_user(username="document_owner")
requester = User.objects.create_user(username="bundle_creator")
requester.user_permissions.add(
Permission.objects.get(codename="add_sharelinkbundle"),
)
owner = UserFactory(username="document_owner")
requester = UserFactory(username="bundle_creator")
grant_global(requester, "add_sharelinkbundle")
document = DocumentFactory.create(owner=owner)
assign_perm("view_document", requester, document)
grant_object(requester, document, "view_document")
self.client.force_authenticate(requester)
payload = {
"document_ids": [document.pk],
@@ -72,9 +71,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
response = self.client.post(self.ENDPOINT, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
requester.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_global(requester, "view_document")
requester = User.objects.get(pk=requester.pk)
self.client.force_authenticate(requester)
response = self.client.post(self.ENDPOINT, payload, format="json")
+7 -9
View File
@@ -1,7 +1,5 @@
from unittest import mock
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
@@ -13,16 +11,16 @@ from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from documents.serialisers import TagSerializer
from documents.signals.handlers import run_workflows
from documents.tests.utils import DirectoriesMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
class TestTagHierarchyPermissions(APITestCase):
def test_children_only_include_visible_tags(self) -> None:
owner = User.objects.create_user(username="owner")
requester = User.objects.create_user(username="requester")
requester.user_permissions.add(
Permission.objects.get(codename="view_tag"),
)
owner = UserFactory(username="owner")
requester = UserFactory(username="requester")
grant_global(requester, "view_tag")
parent = Tag.objects.create(name="Visible parent", owner=requester)
hidden_child = Tag.objects.create(
name="Hidden child",
@@ -49,7 +47,7 @@ class TestTagHierarchyPermissions(APITestCase):
class TestTagHierarchy(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(username="admin")
self.user = UserFactory(username="admin", superuser=True)
self.client.force_authenticate(user=self.user)
self.parent = Tag.objects.create(name="Parent")
+4 -3
View File
@@ -17,7 +17,8 @@ from documents.signals.handlers import task_failure_handler
from documents.signals.handlers import task_postrun_handler
from documents.signals.handlers import task_prerun_handler
from documents.signals.handlers import task_revoked_handler
from documents.tests.factories import PaperlessTaskFactory
from paperless_testing.factories import PaperlessTaskFactory
from paperless_testing.factories import UserFactory
@pytest.fixture
@@ -34,8 +35,8 @@ def consume_input_doc():
@pytest.fixture
def consume_overrides(django_user_model):
user = django_user_model.objects.create_user(username="testuser")
def consume_overrides():
user = UserFactory(username="testuser")
overrides = mock.MagicMock(spec=DocumentMetadataOverrides)
overrides.owner_id = user.id
return overrides
+1 -1
View File
@@ -18,8 +18,8 @@ from documents.models import WorkflowAction
from documents.sanity_checker import SanityCheckFailedException
from documents.sanity_checker import SanityCheckMessages
from documents.tests.test_classifier import dummy_preprocess
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
@pytest.mark.django_db
@@ -14,8 +14,8 @@ from documents.conditionals import preview_etag
from documents.conditionals import thumbnail_etag
from documents.conditionals import thumbnail_last_modified
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
from documents.versioning import resolve_effective_document_by_pk
from paperless_testing.dirs import DirectoriesMixin
if TYPE_CHECKING:
from rest_framework.request import Request
+27 -39
View File
@@ -15,7 +15,6 @@ from django.test import TestCase
from django.test import override_settings
from django.test.utils import CaptureQueriesContext
from django.utils import timezone
from guardian.shortcuts import assign_perm
from rest_framework import status
from documents.caching import get_llm_suggestion_cache
@@ -29,11 +28,14 @@ from documents.models import StoragePath
from documents.models import Tag
from documents.models import UiSettings
from documents.signals.handlers import update_llm_suggestions_cache
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import read_streaming_response
from paperless.models import ApplicationConfiguration
from paperless_ai.exceptions import LLMProviderError
from paperless_ai.exceptions import LLMTimeoutError
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
class TestViews(DirectoriesMixin, TestCase):
@@ -43,7 +45,7 @@ class TestViews(DirectoriesMixin, TestCase):
ApplicationConfiguration.objects.get_or_create()
def setUp(self) -> None:
self.user = User.objects.create_user("testuser")
self.user = UserFactory(username="testuser")
super().setUp()
def test_login_redirect(self) -> None:
@@ -141,9 +143,7 @@ class TestViews(DirectoriesMixin, TestCase):
codename__contains="sharelink",
)
self.user.user_permissions.add(*sharelink_permissions)
self.user.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_global(self.user, "view_document")
self.user.save()
self.client.force_login(self.user)
@@ -205,9 +205,7 @@ class TestViews(DirectoriesMixin, TestCase):
codename__contains="sharelink",
)
self.user.user_permissions.add(*sharelink_permissions)
self.user.user_permissions.add(
Permission.objects.get(codename="view_document"),
)
grant_global(self.user, "view_document")
self.client.force_login(self.user)
create_response = self.client.post(
@@ -240,16 +238,16 @@ class TestViews(DirectoriesMixin, TestCase):
group2 = Group.objects.create(name="group2")
group3 = Group.objects.create(name="group3")
t1 = Tag.objects.create(name="invoice", pk=1)
assign_perm("view_tag", self.user, t1)
assign_perm("view_tag", user2, t1)
assign_perm("view_tag", user3, t1)
assign_perm("view_tag", group1, t1)
assign_perm("view_tag", group2, t1)
assign_perm("view_tag", group3, t1)
assign_perm("change_tag", self.user, t1)
assign_perm("change_tag", user2, t1)
assign_perm("change_tag", group1, t1)
assign_perm("change_tag", group2, t1)
grant_object(self.user, t1, "view_tag")
grant_object(user2, t1, "view_tag")
grant_object(user3, t1, "view_tag")
grant_object(group1, t1, "view_tag")
grant_object(group2, t1, "view_tag")
grant_object(group3, t1, "view_tag")
grant_object(self.user, t1, "change_tag")
grant_object(user2, t1, "change_tag")
grant_object(group1, t1, "change_tag")
grant_object(group2, t1, "change_tag")
Tag.objects.create(name="bank statement", pk=2)
d1 = Document.objects.create(
@@ -338,7 +336,7 @@ class TestViews(DirectoriesMixin, TestCase):
class TestAISuggestions(DirectoriesMixin, TestCase):
def setUp(self) -> None:
self.user = User.objects.create_superuser(username="testuser")
self.user = UserFactory(username="testuser", superuser=True)
self.document = Document.objects.create(
title="Test Document",
filename="test.pdf",
@@ -425,14 +423,10 @@ class TestAISuggestions(DirectoriesMixin, TestCase):
requester; the invisible tag id does not leak into either the
matched or suggested tags
"""
tag_owner = User.objects.create_user(username="cache_tag_owner")
tag_owner = UserFactory(username="cache_tag_owner")
invisible_tag = Tag.objects.create(name="cache_restricted", owner=tag_owner)
requester = User.objects.create_user(username="cache_requester")
requester.user_permissions.add(
*Permission.objects.filter(
codename__in=["view_document", "change_document", "view_tag"],
),
)
requester = UserFactory(username="cache_requester")
grant_global(requester, "view_document", "change_document", "view_tag")
mock_get_cache.return_value = MagicMock(
suggestions={
"title": "Untitled",
@@ -637,7 +631,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase):
- The classification runs with the second user's visibility
context without evicting the first user's result
"""
second_user = User.objects.create_superuser(username="second_user")
second_user = UserFactory(username="second_user", superuser=True)
empty_choices = {
"tags": {"existing_ids": [], "new_names": []},
"correspondents": {"existing_ids": [], "new_names": []},
@@ -875,14 +869,10 @@ class TestAISuggestions(DirectoriesMixin, TestCase):
permission filtering survives the full request path
- it does not appear in either the matched or suggested tags
"""
tag_owner = User.objects.create_user(username="tagowner")
tag_owner = UserFactory(username="tagowner")
invisible_tag = Tag.objects.create(name="restricted", owner=tag_owner)
requester = User.objects.create_user(username="requester")
requester.user_permissions.add(
*Permission.objects.filter(
codename__in=["view_document", "change_document", "view_tag"],
),
)
requester = UserFactory(username="requester")
grant_global(requester, "view_document", "change_document", "view_tag")
mock_get_ai_classification.return_value = {
"title": "Untitled",
@@ -956,7 +946,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase):
ENDPOINT = "/api/documents/chat/"
def setUp(self) -> None:
self.user = User.objects.create_user(username="testuser", password="pass")
self.user = UserFactory(username="testuser", password="pass")
self.client.force_login(user=self.user)
self.document = Document.objects.create(
title="Test Document",
@@ -966,9 +956,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase):
super().setUp()
def grant_view_document_permission(self) -> None:
self.user.user_permissions.add(
*Permission.objects.filter(codename="view_document"),
)
grant_global(self.user, "view_document")
@override_settings(AI_ENABLED=False)
def test_post_ai_disabled(self) -> None:
+26 -25
View File
@@ -17,7 +17,6 @@ from django.contrib.auth.models import User
from django.core import mail
from django.test import override_settings
from django.utils import timezone
from guardian.shortcuts import assign_perm
from guardian.shortcuts import get_groups_with_perms
from guardian.shortcuts import get_users_with_perms
from httpx import ConnectError
@@ -64,13 +63,15 @@ from documents.models import WorkflowTrigger
from documents.plugins.base import StopConsumeTaskError
from documents.serialisers import WorkflowTriggerSerializer
from documents.signals import document_consumption_finished
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import DummyProgressManager
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin
from documents.workflows.actions import execute_password_removal_action
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_object
class TestWorkflows(
@@ -1952,7 +1953,7 @@ class TestWorkflows(
original_filename="sample.pdf",
)
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
self.client.patch(
@@ -1982,7 +1983,7 @@ class TestWorkflows(
w.actions.add(action)
w.save()
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
test_file = shutil.copy(
self.SAMPLE_DIR / "simple.pdf",
@@ -2040,7 +2041,7 @@ class TestWorkflows(
value_text="existing value",
)
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
self.client.patch(
@@ -2084,7 +2085,7 @@ class TestWorkflows(
)
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
self.client.patch(
@@ -2129,12 +2130,12 @@ class TestWorkflows(
original_filename="sample.pdf",
)
assign_perm("documents.view_document", self.user2, doc)
assign_perm("documents.change_document", self.user2, doc)
assign_perm("documents.view_document", self.group1, doc)
assign_perm("documents.change_document", self.group1, doc)
grant_object(self.user2, doc, "documents.view_document")
grant_object(self.user2, doc, "documents.change_document")
grant_object(self.group1, doc, "documents.view_document")
grant_object(self.group1, doc, "documents.change_document")
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
self.client.patch(
@@ -2900,12 +2901,12 @@ class TestWorkflows(
doc.tags.set([self.t1, self.t2])
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
doc.save()
assign_perm("documents.view_document", self.user3, doc)
assign_perm("documents.change_document", self.user3, doc)
assign_perm("documents.view_document", self.group1, doc)
assign_perm("documents.change_document", self.group1, doc)
grant_object(self.user3, doc, "documents.view_document")
grant_object(self.user3, doc, "documents.change_document")
grant_object(self.group1, doc, "documents.view_document")
grant_object(self.group1, doc, "documents.change_document")
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
self.client.patch(
@@ -2977,7 +2978,7 @@ class TestWorkflows(
doc.refresh_from_db()
doc.tags.set([self.t1, self.t2])
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
self.client.patch(
f"/api/documents/{doc.id}/",
@@ -3041,7 +3042,7 @@ class TestWorkflows(
doc.refresh_from_db()
doc.tags.set([self.t1])
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
self.client.patch(
f"/api/documents/{doc.id}/",
@@ -3097,12 +3098,12 @@ class TestWorkflows(
doc.tags.set([self.t1, self.t2])
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
doc.save()
assign_perm("documents.view_document", self.user3, doc)
assign_perm("documents.change_document", self.user3, doc)
assign_perm("documents.view_document", self.group1, doc)
assign_perm("documents.change_document", self.group1, doc)
grant_object(self.user3, doc, "documents.view_document")
grant_object(self.user3, doc, "documents.change_document")
grant_object(self.group1, doc, "documents.view_document")
grant_object(self.group1, doc, "documents.change_document")
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
self.client.patch(
@@ -3395,7 +3396,7 @@ class TestWorkflows(
original_filename="sample.pdf",
)
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
self.client.patch(
@@ -5357,7 +5358,7 @@ class TestDateWorkflowLocalization(
c = Correspondent.objects.create(name="Correspondent Name")
client = APIClient()
superuser = User.objects.create_superuser("superuser")
superuser = UserFactory(username="superuser", superuser=True)
client.force_authenticate(user=superuser)
trigger = WorkflowTrigger.objects.create(
+68 -125
View File
@@ -1,8 +1,5 @@
import shutil
import tempfile
import time
import warnings
from collections import namedtuple
from collections.abc import Callable
from collections.abc import Generator
from collections.abc import Iterator
@@ -19,7 +16,6 @@ from django.db import connection
from django.db.migrations.executor import MigrationExecutor
from django.http import StreamingHttpResponse
from django.test import TransactionTestCase
from django.test import override_settings
from documents.consumer import AsnCheckPlugin
from documents.consumer import ConsumerPlugin
@@ -31,65 +27,6 @@ from documents.parsers import ParseError
from documents.plugins.helpers import ProgressStatusOptions
def setup_directories():
dirs = namedtuple("Dirs", ())
dirs.data_dir = Path(tempfile.mkdtemp()).resolve()
dirs.scratch_dir = Path(tempfile.mkdtemp()).resolve()
dirs.media_dir = Path(tempfile.mkdtemp()).resolve()
dirs.consumption_dir = Path(tempfile.mkdtemp()).resolve()
dirs.static_dir = Path(tempfile.mkdtemp()).resolve()
dirs.index_dir = dirs.data_dir / "index"
dirs.originals_dir = dirs.media_dir / "documents" / "originals"
dirs.thumbnail_dir = dirs.media_dir / "documents" / "thumbnails"
dirs.archive_dir = dirs.media_dir / "documents" / "archive"
dirs.logging_dir = dirs.data_dir / "log"
dirs.index_dir.mkdir(parents=True, exist_ok=True)
dirs.originals_dir.mkdir(parents=True, exist_ok=True)
dirs.thumbnail_dir.mkdir(parents=True, exist_ok=True)
dirs.archive_dir.mkdir(parents=True, exist_ok=True)
dirs.logging_dir.mkdir(parents=True, exist_ok=True)
dirs.settings_override = override_settings(
DATA_DIR=dirs.data_dir,
SCRATCH_DIR=dirs.scratch_dir,
MEDIA_ROOT=dirs.media_dir,
ORIGINALS_DIR=dirs.originals_dir,
THUMBNAIL_DIR=dirs.thumbnail_dir,
ARCHIVE_DIR=dirs.archive_dir,
CONSUMPTION_DIR=dirs.consumption_dir,
LOGGING_DIR=dirs.logging_dir,
INDEX_DIR=dirs.index_dir,
STATIC_ROOT=dirs.static_dir,
MODEL_FILE=dirs.data_dir / "classification_model.pickle",
MEDIA_LOCK=dirs.media_dir / "media.lock",
)
dirs.settings_override.enable()
return dirs
def remove_dirs(dirs) -> None:
shutil.rmtree(dirs.media_dir, ignore_errors=True)
shutil.rmtree(dirs.data_dir, ignore_errors=True)
shutil.rmtree(dirs.scratch_dir, ignore_errors=True)
shutil.rmtree(dirs.consumption_dir, ignore_errors=True)
shutil.rmtree(dirs.static_dir, ignore_errors=True)
dirs.settings_override.disable()
@contextmanager
def paperless_environment():
dirs = None
try:
dirs = setup_directories()
yield dirs
finally:
if dirs:
remove_dirs(dirs)
def util_call_with_backoff(
method_or_callable: Callable,
args: list | tuple,
@@ -158,27 +95,6 @@ def read_streaming_response(response: StreamingHttpResponse) -> bytes:
return content
class DirectoriesMixin:
"""
Creates and overrides settings for all folders and paths, then ensures
they are cleaned up on exit
"""
def setUp(self) -> None:
from documents.search import reset_backend
reset_backend()
self.dirs = setup_directories()
super().setUp()
def tearDown(self) -> None:
from documents.search import reset_backend
super().tearDown()
reset_backend()
remove_dirs(self.dirs)
class FileSystemAssertsMixin:
"""
Utilities for checks various state information of the file system
@@ -196,20 +112,6 @@ class FileSystemAssertsMixin:
def assertIsNotDir(self, path: PathLike[str] | str) -> None:
self.assertFalse(Path(path).resolve().is_dir(), f"Dir does exist: {path}")
def assertFilesEqual(
self,
path1: PathLike[str] | str,
path2: PathLike[str] | str,
) -> None:
path1 = Path(path1)
path2 = Path(path2)
import hashlib
hash1 = hashlib.sha256(path1.read_bytes()).hexdigest()
hash2 = hashlib.sha256(path2.read_bytes()).hexdigest()
self.assertEqual(hash1, hash2, "File SHA256 mismatch")
def assertFileCountInDir(self, path: PathLike[str] | str, count: int) -> None:
path = Path(path).resolve()
self.assertTrue(path.is_dir(), f"Path {path} is not a directory")
@@ -221,24 +123,6 @@ class FileSystemAssertsMixin:
)
class ConsumerProgressMixin:
"""
Mocks the Consumer _send_progress, preventing attempts to connect to Redis
and allowing access to its calls for verification
"""
def setUp(self) -> None:
self.send_progress_patcher = mock.patch(
"documents.consumer.Consumer._send_progress",
)
self.send_progress_mock = self.send_progress_patcher.start()
super().setUp()
def tearDown(self) -> None:
super().tearDown()
self.send_progress_patcher.stop()
class ConsumeTaskMixin:
"""
Provides mocking of the consume_file asynchronous task and useful utilities
@@ -275,6 +159,16 @@ class ConsumeTaskMixin:
class TestMigrations(TransactionTestCase):
"""Run a migration on seeded data, then let the tests inspect the result.
By default every test migrates back, seeds, migrates forward and returns to
the latest migration, which costs several seconds. A class whose tests only
read the migrated data can set ``migrate_once`` to pay that once per class:
the migration runs for the first test, the database is left alone between
tests, and it is restored and flushed when the class finishes. Such tests
must not write to the database.
"""
@property
def app(self):
return apps.get_containing_app_config(type(self).__module__).name
@@ -282,11 +176,38 @@ class TestMigrations(TransactionTestCase):
migrate_from = None
dependencies = None
migrate_to = None
auto_migrate = True
migrate_once = False
_once_owner: "TestMigrations | None" = None
_once_state: dict[str, Any] | None = None
_once_finishing = False
def setUp(self) -> None:
super().setUp()
cls = type(self)
if self.migrate_once:
if cls._once_state is not None:
vars(self).update(cls._once_state)
return
if cls._once_owner is not None:
raise RuntimeError(
f"The migration in '{cls.__name__}' failed for an earlier test",
)
# Recorded before migrating so a failed migration is still restored
cls._once_owner = self
before = dict(vars(self))
self._migrate()
if self.migrate_once:
cls._once_state = {
name: value
for name, value in vars(self).items()
if name not in before or before[name] is not value
}
def _migrate(self) -> None:
assert self.migrate_from and self.migrate_to, (
f"TestCase '{type(self).__name__}' must define migrate_from and migrate_to properties"
)
@@ -304,10 +225,6 @@ class TestMigrations(TransactionTestCase):
self.apps = old_apps
if self.auto_migrate:
self.performMigration()
def performMigration(self) -> None:
# Run the migration to test
executor = MigrationExecutor(connection)
executor.loader.build_graph() # reload.
@@ -318,19 +235,45 @@ class TestMigrations(TransactionTestCase):
def setUpBeforeMigration(self, apps) -> None:
pass
def _migrate_to_latest(self) -> None:
executor = MigrationExecutor(connection)
executor.loader.build_graph()
targets = executor.loader.graph.leaf_nodes()
executor.migrate(targets)
def tearDown(self) -> None:
"""
Ensure the database schema is restored to the latest migration after
each migration test, so subsequent tests run against HEAD.
"""
if self.migrate_once and not self._once_finishing:
return
try:
executor = MigrationExecutor(connection)
executor.loader.build_graph()
targets = executor.loader.graph.leaf_nodes()
executor.migrate(targets)
self._migrate_to_latest()
finally:
super().tearDown()
def _fixture_teardown(self) -> None:
# Django flushes every table after each test, which would discard the
# data the remaining tests of a migrate_once class still need
if self.migrate_once and not self._once_finishing:
return
super()._fixture_teardown()
@classmethod
def tearDownClass(cls) -> None:
owner = cls._once_owner
try:
if owner is not None:
cls._once_finishing = True
owner.tearDown()
owner._fixture_teardown()
finally:
cls._once_owner = None
cls._once_state = None
cls._once_finishing = False
super().tearDownClass()
class SampleDirMixin:
SAMPLE_DIR = Path(__file__).parent / "samples"
+5 -1
View File
@@ -394,7 +394,11 @@ class RasterisedDocumentParser:
plain_pdf_path = Path(self.tempdir) / "image_plain.pdf"
try:
convert_kwargs: dict = {}
convert_kwargs: dict = {
# Ignore invalid EXIF orientation values (e.g. 0) instead of
# aborting the conversion; valid values are still applied
"rotation": img2pdf.Rotation.ifvalid,
}
if self.settings.image_dpi is not None:
convert_kwargs["layout_fun"] = img2pdf.get_fixed_dpi_layout_fun(
(self.settings.image_dpi, self.settings.image_dpi),
@@ -18,6 +18,7 @@ import img2pdf
import magic
import pikepdf
import pytest
from PIL import Image
from documents.parsers import ParseError
@@ -139,3 +140,24 @@ class TestConvertImageToPdfa:
tesseract_parser._convert_image_to_pdfa(simple_png_file)
spy.assert_not_called()
def test_invalid_exif_orientation_is_ignored(
self,
tesseract_parser: RasterisedDocumentParser,
tmp_path: Path,
) -> None:
"""
GIVEN: a JPEG with an invalid EXIF orientation value (0)
WHEN: _convert_image_to_pdfa is called
THEN: the invalid value is ignored and a valid PDF is produced
"""
image_path = tmp_path / "invalid_orientation.jpg"
with Image.new("RGB", (120, 80), "white") as image:
exif = image.getexif()
exif[274] = 0 # EXIF tag 274: Orientation
image.save(image_path, exif=exif)
result = tesseract_parser._convert_image_to_pdfa(image_path)
assert result.exists()
assert magic.from_file(str(result), mime=True) == "application/pdf"
@@ -3,7 +3,6 @@ import json
from django.test import TestCase
from django.test import override_settings
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from paperless.models import ApplicationConfiguration
from paperless.models import CleanChoices
@@ -11,6 +10,7 @@ from paperless.models import ColorConvertChoices
from paperless.models import ModeChoices
from paperless.models import OutputTypeChoices
from paperless.parsers.tesseract import RasterisedDocumentParser
from paperless_testing.dirs import DirectoriesMixin
class TestParserSettingsFromDb(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
@@ -2,22 +2,20 @@ import os
from unittest import mock
from django.conf import settings
from django.contrib.auth.models import User
from django.test import override_settings
from rest_framework import status
from rest_framework.test import APITestCase
from documents.tests.utils import DirectoriesMixin
from paperless.settings import _parse_remote_user_settings
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
class TestRemoteUser(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_superuser(
username="temp_admin",
)
self.user = UserFactory(username="temp_admin", superuser=True)
def test_remote_user(self) -> None:
"""
+6 -5
View File
@@ -15,6 +15,7 @@ from pytest_mock import MockerFixture
from rest_framework.authtoken.models import Token
from paperless.adapter import DrfTokenStrategy
from paperless_testing.factories import UserFactory
@pytest.mark.django_db
@@ -25,7 +26,7 @@ class TestCustomAccountAdapter:
# With no accounts, signups should be allowed
assert adapter.is_open_for_signup(None)
User.objects.create_user("testuser")
UserFactory(username="testuser")
settings.ACCOUNT_ALLOW_SIGNUPS = True
assert adapter.is_open_for_signup(None)
@@ -92,7 +93,7 @@ class TestCustomAccountAdapter:
) -> None:
settings.ACCOUNT_DEFAULT_GROUPS = ["group1", "group2"]
Group.objects.create(name="group1")
user = User.objects.create_user("testuser")
user = UserFactory(username="testuser")
adapter = get_adapter()
form = mocker.MagicMock(
cleaned_data={
@@ -152,7 +153,7 @@ class TestCustomSocialAccountAdapter:
settings.SOCIAL_ACCOUNT_DEFAULT_GROUPS = ["group1", "group2"]
Group.objects.create(name="group1")
adapter = get_social_adapter()
user = User.objects.create_user("testuser")
user = UserFactory(username="testuser")
sociallogin = mocker.MagicMock(user=user)
user = adapter.save_user(HttpRequest(), sociallogin, None)
@@ -187,7 +188,7 @@ class TestDrfTokenStrategy:
THEN:
- A new token is created and its key is returned
"""
user = User.objects.create_user("testuser")
user = UserFactory(username="testuser")
request = HttpRequest()
request.user = user
@@ -207,7 +208,7 @@ class TestDrfTokenStrategy:
THEN:
- The same token key is returned (no new token created)
"""
user = User.objects.create_user("testuser")
user = UserFactory(username="testuser")
existing_token = Token.objects.create(user=user)
request = HttpRequest()
+3 -5
View File
@@ -1,12 +1,13 @@
import uuid
from django.contrib.auth.models import User
from django.test import TestCase
from django.test import override_settings
from django.urls import resolve
from django.urls import reverse
from rest_framework import status
from paperless_testing.factories import UserFactory
class TestApiAuthViews(TestCase):
def test_api_auth_login_uses_allauth_login_view(self):
@@ -24,10 +25,7 @@ class TestApiAuthViews(TestCase):
@override_settings(DISABLE_REGULAR_LOGIN=True)
def test_api_auth_login_respects_disable_regular_login(self):
username = f"testuser-{uuid.uuid4().hex}"
User.objects.create_user(
username=username,
password="testpassword",
)
UserFactory(username=username, password="testpassword")
response = self.client.post(
reverse("rest_framework:login"),
+3 -3
View File
@@ -1,10 +1,10 @@
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import User
from django.test import RequestFactory
from django.test import TestCase
from django.test import override_settings
from paperless.auth import AutoLoginMiddleware
from paperless_testing.factories import UserFactory
@override_settings(AUTO_LOGIN_USERNAME="autologin")
@@ -29,7 +29,7 @@ class TestAutoLoginMiddleware(TestCase):
THEN:
- That user is attached to the request
"""
user = User.objects.create_user(username="autologin")
user = UserFactory(username="autologin")
request = self._process(self.factory.get("/"))
@@ -44,7 +44,7 @@ class TestAutoLoginMiddleware(TestCase):
THEN:
- The request is left anonymous rather than authenticated as them
"""
User.objects.create_user(username="autologin", is_active=False)
UserFactory(username="autologin", is_active=False)
request = self.factory.get("/")
request.user = AnonymousUser()
+9 -38
View File
@@ -1,6 +1,5 @@
import os
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from unittest import mock
@@ -19,35 +18,7 @@ from paperless.checks import check_v3_minimum_upgrade_version
from paperless.checks import debug_mode_check
from paperless.checks import paths_check
from paperless.checks import settings_values_check
@dataclass(frozen=True, slots=True)
class PaperlessTestDirs:
data_dir: Path
media_dir: Path
consumption_dir: Path
# TODO: consolidate with documents/tests/conftest.py PaperlessDirs/paperless_dirs
# once the paperless and documents test suites are ready to share fixtures.
@pytest.fixture()
def directories(tmp_path: Path, settings: Settings) -> PaperlessTestDirs:
data_dir = tmp_path / "data"
media_dir = tmp_path / "media"
consumption_dir = tmp_path / "consumption"
for d in (data_dir, media_dir, consumption_dir):
d.mkdir()
settings.DATA_DIR = data_dir
settings.MEDIA_ROOT = media_dir
settings.CONSUMPTION_DIR = consumption_dir
return PaperlessTestDirs(
data_dir=data_dir,
media_dir=media_dir,
consumption_dir=consumption_dir,
)
from paperless_testing.dirs import PaperlessDirs
class TestChecks:
@@ -58,7 +29,7 @@ class TestChecks:
settings.CONVERT_BINARY = "uuuhh"
assert len(binaries_check(None)) == 1
@pytest.mark.usefixtures("directories")
@pytest.mark.usefixtures("paperless_dirs")
def test_paths_check(self) -> None:
assert paths_check(None) == []
@@ -73,17 +44,17 @@ class TestChecks:
for msg in msgs:
assert msg.msg.endswith("is set but doesn't exist.")
def test_paths_check_no_access(self, directories: PaperlessTestDirs) -> None:
directories.data_dir.chmod(0o000)
directories.media_dir.chmod(0o000)
directories.consumption_dir.chmod(0o000)
def test_paths_check_no_access(self, paperless_dirs: PaperlessDirs) -> None:
paperless_dirs.data_dir.chmod(0o000)
paperless_dirs.media_dir.chmod(0o000)
paperless_dirs.consumption_dir.chmod(0o000)
try:
msgs = paths_check(None)
finally:
directories.data_dir.chmod(0o777)
directories.media_dir.chmod(0o777)
directories.consumption_dir.chmod(0o777)
paperless_dirs.data_dir.chmod(0o777)
paperless_dirs.media_dir.chmod(0o777)
paperless_dirs.consumption_dir.chmod(0o777)
assert len(msgs) == 3
for msg in msgs:
@@ -4,6 +4,7 @@ from documents.tests.utils import TestMigrations
class TestMigrateSkipArchiveFile(TestMigrations):
migrate_from = "0007_optimize_integer_field_sizes"
migrate_to = "0008_replace_skip_archive_file"
migrate_once = True
def setUpBeforeMigration(self, apps):
ApplicationConfiguration = apps.get_model(
+23 -63
View File
@@ -1,7 +1,6 @@
from unittest.mock import Mock
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.http import HttpRequest
from django.test import TestCase
from django.test import override_settings
@@ -9,6 +8,7 @@ from django.test import override_settings
from documents.models import UiSettings
from paperless.signals import handle_failed_login
from paperless.signals import handle_social_account_updated
from paperless_testing.factories import UserFactory
class TestFailedLoginLogging(TestCase):
@@ -120,7 +120,7 @@ class TestSyncSocialLoginGroups(TestCase):
- The user's groups are updated to match the social login's groups
"""
group = Group.objects.create(name="group1")
user = User.objects.create_user(username="testuser")
user = UserFactory(username="testuser")
sociallogin = Mock(
user=user,
account=Mock(
@@ -147,7 +147,7 @@ class TestSyncSocialLoginGroups(TestCase):
- The user's groups are not updated
"""
Group.objects.create(name="group1")
user = User.objects.create_user(username="testuser")
user = UserFactory(username="testuser")
sociallogin = Mock(
user=user,
account=Mock(
@@ -180,7 +180,7 @@ class TestSyncSocialLoginGroups(TestCase):
would be rejected for a deactivated user anyway
"""
Group.objects.create(name="admin-group")
user = User.objects.create_user(
user = UserFactory(
username="inactive_user",
is_active=False,
is_superuser=False,
@@ -215,7 +215,7 @@ class TestSyncSocialLoginGroups(TestCase):
- The user's groups are cleared to match the social login's groups
"""
group = Group.objects.create(name="group1")
user = User.objects.create_user(username="testuser")
user = UserFactory(username="testuser")
user.groups.add(group)
user.save()
sociallogin = Mock(
@@ -244,7 +244,7 @@ class TestSyncSocialLoginGroups(TestCase):
- The user's groups are updated using `userinfo.groups`
"""
group = Group.objects.create(name="group1")
user = User.objects.create_user(username="testuser")
user = UserFactory(username="testuser")
sociallogin = Mock(
user=user,
account=Mock(
@@ -275,7 +275,7 @@ class TestSyncSocialLoginGroups(TestCase):
- The user's groups are updated using `id_token.groups`
"""
group = Group.objects.create(name="group1")
user = User.objects.create_user(username="testuser")
user = UserFactory(username="testuser")
sociallogin = Mock(
user=user,
account=Mock(
@@ -310,11 +310,7 @@ class TestSyncSocialLoginGroups(TestCase):
THEN:
- User is not promoted, since only an exact group match counts
"""
user = User.objects.create_user(
username="testuser",
is_superuser=False,
is_staff=False,
)
user = UserFactory(username="testuser", is_superuser=False, is_staff=False)
sociallogin = Mock(
user=user,
account=Mock(
@@ -345,11 +341,7 @@ class TestSyncSocialLoginGroups(TestCase):
THEN:
- User becomes superuser and staff
"""
user = User.objects.create_user(
username="testuser_s_e",
is_superuser=False,
is_staff=False,
)
user = UserFactory(username="testuser_s_e", is_superuser=False, is_staff=False)
sociallogin = Mock(
user=user,
account=Mock(
@@ -380,11 +372,7 @@ class TestSyncSocialLoginGroups(TestCase):
THEN:
- User loses superuser status but preserves staff status if they had it
"""
user = User.objects.create_user(
username="testuser_s_d",
is_superuser=True,
is_staff=True,
)
user = UserFactory(username="testuser_s_d", is_superuser=True, is_staff=True)
sociallogin = Mock(
user=user,
account=Mock(
@@ -415,11 +403,7 @@ class TestSyncSocialLoginGroups(TestCase):
THEN:
- User becomes staff
"""
user = User.objects.create_user(
username="testuser_st_e",
is_superuser=False,
is_staff=False,
)
user = UserFactory(username="testuser_st_e", is_superuser=False, is_staff=False)
sociallogin = Mock(
user=user,
account=Mock(
@@ -450,11 +434,7 @@ class TestSyncSocialLoginGroups(TestCase):
THEN:
- User loses staff status
"""
user = User.objects.create_user(
username="testuser_st_d",
is_superuser=False,
is_staff=True,
)
user = UserFactory(username="testuser_st_d", is_superuser=False, is_staff=True)
sociallogin = Mock(
user=user,
account=Mock(
@@ -485,11 +465,7 @@ class TestSyncSocialLoginGroups(TestCase):
- Roles are correctly assigned/revoked according to groups
"""
# Case 1: has both
user = User.objects.create_user(
username="testuser_b_1",
is_superuser=False,
is_staff=False,
)
user = UserFactory(username="testuser_b_1", is_superuser=False, is_staff=False)
sociallogin = Mock(
user=user,
account=Mock(extra_data={"groups": ["admin-group", "staff-group"]}),
@@ -504,11 +480,7 @@ class TestSyncSocialLoginGroups(TestCase):
self.assertTrue(user.is_staff)
# Case 2: has only staff
user2 = User.objects.create_user(
username="testuser_b_2",
is_superuser=True,
is_staff=True,
)
user2 = UserFactory(username="testuser_b_2", is_superuser=True, is_staff=True)
sociallogin2 = Mock(
user=user2,
account=Mock(extra_data={"groups": ["staff-group"]}),
@@ -523,11 +495,7 @@ class TestSyncSocialLoginGroups(TestCase):
self.assertTrue(user2.is_staff)
# Case 3: has neither
user3 = User.objects.create_user(
username="testuser_b_3",
is_superuser=True,
is_staff=True,
)
user3 = UserFactory(username="testuser_b_3", is_superuser=True, is_staff=True)
sociallogin3 = Mock(
user=user3,
account=Mock(extra_data={"groups": ["other-group"]}),
@@ -554,11 +522,7 @@ class TestSyncSocialLoginGroups(TestCase):
THEN:
- Existing roles are not modified
"""
user = User.objects.create_user(
username="testuser_n_s",
is_superuser=True,
is_staff=True,
)
user = UserFactory(username="testuser_n_s", is_superuser=True, is_staff=True)
sociallogin = Mock(
user=user,
account=Mock(extra_data={"groups": ["admin-group", "staff-group"]}),
@@ -586,7 +550,7 @@ class TestSyncSocialLoginGroups(TestCase):
THEN:
- User's superuser status is demoted, matching the group claim exactly
"""
user = User.objects.create_user(
user = UserFactory(
username="local_admin",
password="password123",
is_superuser=True,
@@ -618,11 +582,7 @@ class TestSyncSocialLoginGroups(TestCase):
THEN:
- User's superuser status is demoted, even though they are the last admin
"""
user = User.objects.create_user(
username="last_admin",
is_superuser=True,
is_staff=True,
)
user = UserFactory(username="last_admin", is_superuser=True, is_staff=True)
user.set_unusable_password()
user.save()
@@ -652,7 +612,7 @@ class TestSyncSocialLoginGroups(TestCase):
THEN:
- User's staff status is demoted, matching the group claim exactly
"""
user = User.objects.create_user(
user = UserFactory(
username="local_staff",
password="password123",
is_superuser=False,
@@ -688,8 +648,8 @@ class TestUserGroupDeletionCleanup(TestCase):
THEN:
- References in ui_settings are cleaned up
"""
user = User.objects.create_user(username="testuser")
user2 = User.objects.create_user(username="testuser2")
user = UserFactory(username="testuser")
user2 = UserFactory(username="testuser2")
group = Group.objects.create(name="testgroup")
ui_settings = UiSettings.objects.create(
@@ -727,8 +687,8 @@ class TestUserGroupDeletionCleanup(TestCase):
THEN:
- Error is logged and the system remains stable
"""
user = User.objects.create_user(username="testuser")
user2 = User.objects.create_user(username="testuser2")
user = UserFactory(username="testuser")
user2 = UserFactory(username="testuser2")
user2_id = user2.id
Group.objects.create(name="testgroup")
+6 -6
View File
@@ -7,14 +7,10 @@ from unittest.mock import patch
import pytest
import pytest_mock
from django.test import override_settings
from guardian.shortcuts import assign_perm
from guardian.shortcuts import remove_perm
from documents.models import Document
from documents.search import TantivyBackend
from documents.tests.factories import DocumentFactory
from documents.tests.factories import TagFactory
from documents.tests.factories import UserFactory
from paperless.config import AIConfig
from paperless_ai.ai_classifier import TAXONOMY_CANDIDATE_TOP_K
from paperless_ai.ai_classifier import _fulltext_similar_documents
@@ -27,6 +23,10 @@ from paperless_ai.ai_classifier import get_taxonomy_context
from paperless_ai.taxonomy import SimilarDocument
from paperless_ai.taxonomy import TaxonomyCandidate
from paperless_ai.taxonomy import TaxonomyCandidates
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_object
@pytest.fixture
@@ -815,8 +815,8 @@ class TestFulltextSimilarDocuments:
content="shared content phrase",
owner=owner,
)
assign_perm("view_document", viewer, permitted)
assign_perm("view_document", viewer, now_private)
grant_object(viewer, permitted, "view_document")
grant_object(viewer, now_private, "view_document")
fulltext_backend.add_or_update(source)
fulltext_backend.add_or_update(permitted)
fulltext_backend.add_or_update(now_private)
+2 -2
View File
@@ -21,12 +21,12 @@ from documents.models import Note
from documents.models import PaperlessTask
from documents.signals import document_consumption_finished
from documents.signals import document_updated
from documents.tests.factories import DocumentFactory
from documents.tests.factories import PaperlessTaskFactory
from paperless.models import ApplicationConfiguration
from paperless_ai import indexing
from paperless_ai.tests.conftest import FakeEmbedding
from paperless_ai.vector_store import PaperlessSqliteVecVectorStore
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import PaperlessTaskFactory
@pytest.fixture
+1 -1
View File
@@ -14,7 +14,6 @@ from llama_index.core.embeddings.mock_embed_model import MockEmbedding
from llama_index.core.schema import TextNode
from documents.models import Document
from documents.tests.factories import DocumentFactory
from paperless_ai import chat
from paperless_ai import indexing
from paperless_ai.chat import CHAT_ERROR_MESSAGE
@@ -22,6 +21,7 @@ from paperless_ai.chat import CHAT_METADATA_DELIMITER
from paperless_ai.chat import _build_chat_prompt
from paperless_ai.chat import _build_refine_prompt
from paperless_ai.chat import stream_chat_with_documents
from paperless_testing.factories import DocumentFactory
if TYPE_CHECKING:
from pathlib import Path
+5 -5
View File
@@ -10,11 +10,6 @@ from documents.models import Correspondent
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentTypeFactory
from documents.tests.factories import StoragePathFactory
from documents.tests.factories import TagFactory
from documents.tests.factories import UserFactory
from paperless_ai.matching import extract_unmatched_names
from paperless_ai.matching import match_correspondents_by_name
from paperless_ai.matching import match_document_types_by_name
@@ -24,6 +19,11 @@ from paperless_ai.matching import resolve_correspondent_ids
from paperless_ai.matching import resolve_document_type_ids
from paperless_ai.matching import resolve_storage_path_ids
from paperless_ai.matching import resolve_tag_ids
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import DocumentTypeFactory
from paperless_testing.factories import StoragePathFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
class TestAIMatching(TestCase):
+6 -6
View File
@@ -3,16 +3,16 @@ import json
import pytest
import pytest_mock
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentFactory
from documents.tests.factories import DocumentTypeFactory
from documents.tests.factories import StoragePathFactory
from documents.tests.factories import TagFactory
from documents.tests.factories import UserFactory
from paperless_ai.taxonomy import SimilarDocument
from paperless_ai.taxonomy import TaxonomyCandidates
from paperless_ai.taxonomy import build_taxonomy_candidates
from paperless_ai.taxonomy import format_taxonomy_for_prompt
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import DocumentTypeFactory
from paperless_testing.factories import StoragePathFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
def make_similar(document_id: int, weight: float) -> SimilarDocument:
+36 -38
View File
@@ -1,16 +1,10 @@
import json
from unittest import mock
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from guardian.shortcuts import assign_perm
from rest_framework import status
from rest_framework.test import APITestCase
from documents.tests.factories import CorrespondentFactory
from documents.tests.factories import DocumentTypeFactory
from documents.tests.factories import TagFactory
from documents.tests.utils import DirectoriesMixin
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
from paperless_mail.models import ProcessedMail
@@ -18,6 +12,14 @@ from paperless_mail.tests.factories import MailAccountFactory
from paperless_mail.tests.factories import MailRuleFactory
from paperless_mail.tests.factories import ProcessedMailFactory
from paperless_mail.tests.test_mail import BogusMailBox
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import DocumentTypeFactory
from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_all_global
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
class TestAPIMailAccounts(DirectoriesMixin, APITestCase):
@@ -33,8 +35,8 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase):
super().setUp()
self.user = User.objects.create_user(username="temp_admin")
self.user.user_permissions.add(*Permission.objects.all())
self.user = UserFactory(username="temp_admin")
grant_all_global(self.user)
self.user.save()
self.client.force_authenticate(user=self.user)
@@ -291,7 +293,7 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase):
imap_port=443,
owner=None,
)
user = User.objects.create_user(username="no_perms")
user = UserFactory(username="no_perms")
self.client.force_authenticate(user=user)
response = self.client.post(
@@ -321,7 +323,7 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase):
THEN:
- API returns forbidden
"""
owner = User.objects.create_user(username="account_owner")
owner = UserFactory(username="account_owner")
account = MailAccountFactory(
username="admin",
password="secret",
@@ -329,8 +331,8 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase):
imap_port=443,
owner=owner,
)
user = User.objects.create_user(username="object_perms_only")
assign_perm("change_mailaccount", user, account)
user = UserFactory(username="object_perms_only")
grant_object(user, account, "change_mailaccount")
self.client.force_authenticate(user=user)
response = self.client.post(
@@ -377,13 +379,13 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase):
- Only unowned, owned by user or granted accounts are provided
"""
user2 = User.objects.create_user(username="temp_admin2")
user2 = UserFactory(username="temp_admin2")
account1 = MailAccountFactory(name="Email1")
account2 = MailAccountFactory(name="Email2", owner=self.user)
_account3 = MailAccountFactory(name="Email3", owner=user2)
account4 = MailAccountFactory(name="Email4", owner=user2)
assign_perm("view_mailaccount", self.user, account4)
grant_object(self.user, account4, "view_mailaccount")
response = self.client.get(self.ENDPOINT)
@@ -400,8 +402,8 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_user(username="temp_admin")
self.user.user_permissions.add(*Permission.objects.all())
self.user = UserFactory(username="temp_admin")
grant_all_global(self.user)
self.user.save()
self.client.force_authenticate(user=self.user)
@@ -590,7 +592,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
self.assertEqual(returned_rule1.action, MailRule.MailAction.DELETE)
def test_create_mail_rule_scopes_accounts(self) -> None:
other_user = User.objects.create_user(username="mail-owner")
other_user = UserFactory(username="mail-owner")
foreign_account = MailAccountFactory(name="ForeignEmail", owner=other_user)
response = self.client.post(
@@ -633,9 +635,9 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
def test_create_mail_rule_allowed_for_granted_account_change_permission(
self,
) -> None:
other_user = User.objects.create_user(username="mail-owner")
other_user = UserFactory(username="mail-owner")
foreign_account = MailAccountFactory(name="ForeignEmail", owner=other_user)
assign_perm("change_mailaccount", self.user, foreign_account)
grant_object(self.user, foreign_account, "change_mailaccount")
response = self.client.post(
self.ENDPOINT,
@@ -658,7 +660,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
def test_update_mail_rule_forbidden_for_unpermitted_account(self) -> None:
own_account = MailAccountFactory()
other_user = User.objects.create_user(username="mail-owner")
other_user = UserFactory(username="mail-owner")
foreign_account = MailAccountFactory(owner=other_user)
rule1 = MailRuleFactory(account=own_account)
@@ -681,13 +683,13 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
- Only unowned, owned by user or granted mail rules are provided
"""
user2 = User.objects.create_user(username="temp_admin2")
user2 = UserFactory(username="temp_admin2")
account1 = MailAccountFactory()
rule1 = MailRuleFactory(account=account1, order=0)
rule2 = MailRuleFactory(account=account1, order=1, owner=self.user)
MailRuleFactory(account=account1, order=2, owner=user2)
rule4 = MailRuleFactory(account=account1, order=3, owner=user2)
assign_perm("view_mailrule", self.user, rule4)
grant_object(self.user, rule4, "view_mailrule")
response = self.client.get(self.ENDPOINT)
@@ -737,8 +739,8 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create_user(username="temp_admin")
self.user.user_permissions.add(*Permission.objects.all())
self.user = UserFactory(username="temp_admin")
grant_all_global(self.user)
self.user.save()
self.client.force_authenticate(user=self.user)
@@ -751,7 +753,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase):
THEN:
- Only unowned, owned by user or granted processed mails are provided
"""
user2 = User.objects.create_user(username="temp_admin2")
user2 = UserFactory(username="temp_admin2")
rule = MailRuleFactory()
pm1 = ProcessedMailFactory(rule=rule)
pm2 = ProcessedMailFactory(
@@ -762,7 +764,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase):
)
ProcessedMailFactory(rule=rule, owner=user2)
pm4 = ProcessedMailFactory(rule=rule, owner=user2)
assign_perm("view_processedmail", self.user, pm4)
grant_object(self.user, pm4, "view_processedmail")
response = self.client.get(self.ENDPOINT)
@@ -802,7 +804,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase):
THEN:
- Only the specified processed mails are deleted, respecting ownership and permissions
"""
user2 = User.objects.create_user(username="temp_admin2")
user2 = UserFactory(username="temp_admin2")
rule = MailRuleFactory()
# unowned, owned by self, and one with explicit object perm
pm_unowned = ProcessedMailFactory(rule=rule)
@@ -813,7 +815,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase):
owner=self.user,
)
pm_granted = ProcessedMailFactory(rule=rule, owner=user2)
assign_perm("delete_processedmail", self.user, pm_granted)
grant_object(self.user, pm_granted, "delete_processedmail")
pm_forbidden = ProcessedMailFactory(rule=rule, owner=user2)
# Success for allowed items
@@ -855,13 +857,11 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_bulk_delete_requires_global_delete_permission(self) -> None:
owner = User.objects.create_user(username="mail_owner")
requester = User.objects.create_user(username="mail_deleter")
requester.user_permissions.add(
Permission.objects.get(codename="add_processedmail"),
)
owner = UserFactory(username="mail_owner")
requester = UserFactory(username="mail_deleter")
grant_global(requester, "add_processedmail")
mail = ProcessedMailFactory(owner=owner)
assign_perm("delete_processedmail", requester, mail)
grant_object(requester, mail, "delete_processedmail")
self.client.force_authenticate(requester)
response = self.client.post(
@@ -871,9 +871,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase):
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
requester.user_permissions.add(
Permission.objects.get(codename="delete_processedmail"),
)
grant_global(requester, "delete_processedmail")
requester = User.objects.get(pk=requester.pk)
self.client.force_authenticate(requester)
response = self.client.post(
@@ -893,7 +891,7 @@ class TestAPIProcessedMails(DirectoriesMixin, APITestCase):
THEN:
- The request is rejected and neither mail is deleted
"""
user2 = User.objects.create_user(username="temp_admin2")
user2 = UserFactory(username="temp_admin2")
rule = MailRuleFactory()
# Created first so it sorts ahead of the forbidden mail, i.e. the
# permission check has to cover the whole batch before deleting rather
+11 -18
View File
@@ -9,7 +9,6 @@ from unittest import mock
import pytest
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.core.management import call_command
from django.db import DatabaseError
from django.test import TestCase
@@ -28,8 +27,6 @@ from rest_framework.test import APITestCase
from documents.models import Correspondent
from documents.models import MatchingModel
from documents.tests.factories import CorrespondentFactory
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from paperless_mail import tasks
from paperless_mail.mail import MailAccountHandler
@@ -43,6 +40,10 @@ from paperless_mail.models import MailRule
from paperless_mail.models import ProcessedMail
from paperless_mail.tests.factories import MailAccountFactory
from paperless_mail.tests.factories import MailRuleFactory
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import CorrespondentFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
@dataclasses.dataclass
@@ -2263,15 +2264,8 @@ class TestMailAccountTestView(APITestCase):
self.mailMocker = MailMocker()
self.mailMocker.setUp()
self.addCleanup(self.mailMocker.doCleanups)
self.user = User.objects.create_user(
username="testuser",
password="testpassword",
)
self.user.user_permissions.add(
*Permission.objects.filter(
codename__in=["add_mailaccount", "change_mailaccount"],
),
)
self.user = UserFactory(username="testuser", password="testpassword")
grant_global(self.user, "add_mailaccount", "change_mailaccount")
self.user.save()
self.client.force_authenticate(user=self.user)
self.url = "/api/mail_accounts/test/"
@@ -2409,10 +2403,7 @@ class TestMailAccountTestView(APITestCase):
self.assertIn(expected_str, error_str)
def test_mail_account_test_view_existing_forbidden_for_other_owner(self) -> None:
other_user = User.objects.create_user(
username="otheruser",
password="testpassword",
)
other_user = UserFactory(username="otheruser", password="testpassword")
existing_account = MailAccount.objects.create(
name="Owned account",
imap_server="imap.example.com",
@@ -2535,9 +2526,10 @@ class TestMailAccountProcess(APITestCase):
self.mailMocker = MailMocker()
self.mailMocker.setUp()
self.addCleanup(self.mailMocker.doCleanups)
self.user = User.objects.create_superuser(
self.user = UserFactory(
username="testuser",
password="testpassword",
superuser=True,
)
self.client.force_authenticate(user=self.user)
self.account = MailAccount.objects.create(
@@ -2560,9 +2552,10 @@ class TestMailAccountProcess(APITestCase):
class TestMailRuleAPI(APITestCase):
def setUp(self) -> None:
self.user = User.objects.create_superuser(
self.user = UserFactory(
username="testuser",
password="testpassword",
superuser=True,
)
self.client.force_authenticate(user=self.user)
self.account = MailAccountFactory(owner=self.user)
+3 -12
View File
@@ -13,8 +13,6 @@ from unittest import mock
import pytest
from documents.tests.utils import remove_dirs
from documents.tests.utils import setup_directories
from paperless_mail.models import MailRule
from paperless_mail.tests.factories import MailAccountFactory
from paperless_mail.tests.test_mail import MessageBuilder
@@ -22,13 +20,6 @@ from paperless_mail.tests.test_mail import _AttachmentDef
from paperless_mail.tests.test_mail import fake_magic_from_buffer
@pytest.fixture()
def directories(settings):
dirs = setup_directories()
yield dirs
remove_dirs(dirs)
@pytest.fixture()
def queue_consumption_tasks_mock():
with mock.patch("paperless_mail.mail.queue_consumption_tasks") as m:
@@ -78,7 +69,7 @@ class TestMailNFCNormalization:
def test_attachment_nfd_filename_normalized_to_nfc(
self,
directories,
paperless_dirs,
queue_consumption_tasks_mock,
attachment_rule,
mail_account_handler,
@@ -123,7 +114,7 @@ class TestMailNFCNormalization:
def test_eml_subject_filename_nfc(
self,
directories,
paperless_dirs,
queue_consumption_tasks_mock,
eml_rule,
mail_account_handler,
@@ -156,7 +147,7 @@ class TestMailNFCNormalization:
def test_already_nfc_attachment_filename_unchanged(
self,
directories,
paperless_dirs,
queue_consumption_tasks_mock,
attachment_rule,
mail_account_handler,
+4 -9
View File
@@ -2,7 +2,6 @@ from datetime import timedelta
from unittest import mock
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import TestCase
from django.test import override_settings
from django.utils import timezone
@@ -14,6 +13,8 @@ from paperless_mail.mail import MailAccountHandler
from paperless_mail.models import MailAccount
from paperless_mail.oauth import PaperlessMailOAuth2Manager
from paperless_mail.tests.factories import MailAccountFactory
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
@override_settings(
@@ -27,14 +28,8 @@ class TestMailOAuth(
TestCase,
):
def setUp(self) -> None:
self.user = User.objects.create_user("testuser")
self.user.user_permissions.add(
*Permission.objects.filter(
codename__in=[
"add_mailaccount",
],
),
)
self.user = UserFactory(username="testuser")
grant_global(self.user, "add_mailaccount")
self.user.save()
self.client.force_login(self.user)
self.mail_account_handler = MailAccountHandler()
+5
View File
@@ -0,0 +1,5 @@
"""Shared test support for every Paperless-ngx app.
Fixtures live in ``src/conftest.py``; anything a test imports by name lives
here. App-specific helpers stay in that app's ``tests`` package.
"""
+146
View File
@@ -0,0 +1,146 @@
"""The Paperless-ngx temp directory layout, stated once.
``build_paperless_dirs`` owns where things go and creates them.
``dirs_settings`` owns the mapping onto Django setting names and is pure.
Everything else in the test suite is a caller of these two.
"""
from __future__ import annotations
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING
from typing import TypedDict
import pytest
from django.test import override_settings
if TYPE_CHECKING:
from collections.abc import Iterator
@dataclass(frozen=True, slots=True)
class PaperlessDirs:
"""Standard Paperless-ngx directory layout for tests."""
data_dir: Path
scratch_dir: Path
media_dir: Path
consumption_dir: Path
static_dir: Path
index_dir: Path
originals_dir: Path
thumbnail_dir: Path
archive_dir: Path
logging_dir: Path
model_file: Path
media_lock: Path
class DirSettings(TypedDict):
"""The Django settings the layout above maps onto."""
DATA_DIR: Path
SCRATCH_DIR: Path
MEDIA_ROOT: Path
ORIGINALS_DIR: Path
THUMBNAIL_DIR: Path
ARCHIVE_DIR: Path
CONSUMPTION_DIR: Path
LOGGING_DIR: Path
INDEX_DIR: Path
STATIC_ROOT: Path
MODEL_FILE: Path
MEDIA_LOCK: Path
def build_paperless_dirs(root: Path) -> PaperlessDirs:
"""Compute the layout under root and create the directories."""
data_dir = root / "data"
media_dir = root / "media"
documents_dir = media_dir / "documents"
dirs = PaperlessDirs(
data_dir=data_dir,
scratch_dir=root / "scratch",
media_dir=media_dir,
consumption_dir=root / "consume",
static_dir=root / "static",
index_dir=data_dir / "index",
originals_dir=documents_dir / "originals",
thumbnail_dir=documents_dir / "thumbnails",
archive_dir=documents_dir / "archive",
logging_dir=data_dir / "log",
model_file=data_dir / "classification_model.pickle",
media_lock=media_dir / "media.lock",
)
for directory in (
dirs.data_dir,
dirs.scratch_dir,
dirs.media_dir,
dirs.consumption_dir,
dirs.static_dir,
dirs.index_dir,
dirs.originals_dir,
dirs.thumbnail_dir,
dirs.archive_dir,
dirs.logging_dir,
):
directory.mkdir(parents=True, exist_ok=True)
return dirs
def dirs_settings(dirs: PaperlessDirs) -> DirSettings:
"""Map the layout onto Django setting names."""
return DirSettings(
DATA_DIR=dirs.data_dir,
SCRATCH_DIR=dirs.scratch_dir,
MEDIA_ROOT=dirs.media_dir,
ORIGINALS_DIR=dirs.originals_dir,
THUMBNAIL_DIR=dirs.thumbnail_dir,
ARCHIVE_DIR=dirs.archive_dir,
CONSUMPTION_DIR=dirs.consumption_dir,
LOGGING_DIR=dirs.logging_dir,
INDEX_DIR=dirs.index_dir,
STATIC_ROOT=dirs.static_dir,
MODEL_FILE=dirs.model_file,
MEDIA_LOCK=dirs.media_lock,
)
@contextmanager
def paperless_environment() -> Iterator[PaperlessDirs]:
"""A second, isolated environment for the duration of the block.
Only for tests needing a fresh environment part way through a test body,
which a fixture cannot provide. Everything else uses the paperless_dirs
fixture.
"""
from documents.search import reset_backend
with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
dirs = build_paperless_dirs(Path(tmp))
with override_settings(**dirs_settings(dirs)):
reset_backend()
try:
yield dirs
finally:
reset_backend()
class DirectoriesMixin:
"""Bridges the directory fixture to unittest TestCase subclasses and to
plain pytest test classes that want `self.dirs`.
Delete once no class depends on it.
"""
dirs: PaperlessDirs
@pytest.fixture(autouse=True)
def _paperless_dirs(self, paperless_dirs: PaperlessDirs) -> None:
self.dirs = paperless_dirs
@@ -78,7 +78,10 @@ class UserFactory(DjangoModelFactory[UserModelT]):
username = factory.Sequence(lambda n: f"user{n}")
is_staff = False
is_superuser = False
password = factory.django.Password("test")
# Hashing a real password costs about half a second per user, and no test
# authenticates with its password. None gives the unusable password
# create_user() gives.
password = factory.django.Password(None)
class Params:
superuser = factory.Trait(is_staff=True, is_superuser=True)
+43
View File
@@ -0,0 +1,43 @@
"""Helpers that grant permissions to users and groups in tests.
Two mechanisms, two names. A global permission is a Django model permission held
by a user. An object permission is a guardian permission held on one object by a
user or a group. They are not interchangeable.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from django.contrib.auth.models import Permission
from guardian.shortcuts import assign_perm
if TYPE_CHECKING:
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.db.models import Model
def grant_global(user: User, *perms: str) -> None:
"""Grant model-level permissions.
Each of perms is a codename or "app_label.codename". An unknown name raises
Permission.DoesNotExist.
"""
for perm in perms:
app_label, _, codename = perm.rpartition(".")
lookup = {"codename": codename}
if app_label:
lookup["content_type__app_label"] = app_label
user.user_permissions.add(Permission.objects.get(**lookup))
def grant_all_global(user: User) -> None:
"""Grant every existing model-level permission without making a superuser."""
user.user_permissions.add(*Permission.objects.all())
def grant_object(target: User | Group, obj: Model, *perms: str) -> None:
"""Grant object-level permissions on obj to a user or a group."""
for perm in perms:
assign_perm(perm, target, obj)
Generated
+16
View File
@@ -2969,6 +2969,7 @@ dev = [
{ name = "pytest-env" },
{ name = "pytest-httpx" },
{ name = "pytest-mock" },
{ name = "pytest-randomly" },
{ name = "pytest-rerunfailures" },
{ name = "pytest-sugar" },
{ name = "pytest-xdist" },
@@ -2994,6 +2995,7 @@ testing = [
{ name = "pytest-env" },
{ name = "pytest-httpx" },
{ name = "pytest-mock" },
{ name = "pytest-randomly" },
{ name = "pytest-rerunfailures" },
{ name = "pytest-sugar" },
{ name = "pytest-xdist" },
@@ -3111,6 +3113,7 @@ dev = [
{ name = "pytest-env", specifier = "~=1.7.0" },
{ name = "pytest-httpx" },
{ name = "pytest-mock", specifier = "~=3.15.1" },
{ name = "pytest-randomly", specifier = "~=5.0.0" },
{ name = "pytest-rerunfailures", specifier = "~=16.4" },
{ name = "pytest-sugar" },
{ name = "pytest-xdist", specifier = "~=3.8.0" },
@@ -3134,6 +3137,7 @@ testing = [
{ name = "pytest-env", specifier = "~=1.7.0" },
{ name = "pytest-httpx" },
{ name = "pytest-mock", specifier = "~=3.15.1" },
{ name = "pytest-randomly", specifier = "~=5.0.0" },
{ name = "pytest-rerunfailures", specifier = "~=16.4" },
{ name = "pytest-sugar" },
{ name = "pytest-xdist", specifier = "~=3.8.0" },
@@ -3911,6 +3915,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
]
[[package]]
name = "pytest-randomly"
version = "5.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/3b/6a40e1b9d925651e601e056a97f60d8a1daeddeac03d5609be60cb4362ce/pytest_randomly-5.0.0.tar.gz", hash = "sha256:e9c575a5873ef168ddbe340ed9e97ce9edb4492ccc821e4b2ac6bb1f0ed515d2", size = 8542, upload-time = "2026-09-01T22:34:20.441Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/b4/47e939285caad9a623d021512912ac08dc92a467ad075d179f43729d2934/pytest_randomly-5.0.0-py3-none-any.whl", hash = "sha256:8a0d4703115c0c25b38b6e129fc16b1947b9643ff26a41bc1d185d7e5a7689c1", size = 8920, upload-time = "2026-09-01T22:34:19.227Z" },
]
[[package]]
name = "pytest-rerunfailures"
version = "16.6.1"