Files
ACORE/apps/nextcloud_connector/tests.py
T

818 lines
35 KiB
Python

from django.test import TestCase
from apps.nextcloud_connector.nextcloud_service import (
NextcloudService, NextcloudConfigError,
)
from apps.nextcloud_connector.services import sync_service as ss
class _FakeDept:
def __init__(self, code, name):
self.code, self.name = code, name
self.parent = None
self.pk = None
class _FakePharmacy:
def __init__(self, number, address=''):
self.pharmacy_number, self.address = number, address
class MountpointTests(TestCase):
def test_department_mountpoint_and_group(self):
d = _FakeDept('hr', 'HR')
self.assertEqual(ss.dept_mountpoint(d), '03_Отделы/HR')
self.assertEqual(ss.dept_group_name(d), 'nc-dept-hr')
def test_department_mountpoint_hierarchical(self):
parent = _FakeDept('admin', 'Административный')
parent.pk = 1
child = _FakeDept('legal', 'Юридическая служба')
child.pk = 2
child.parent = parent
self.assertEqual(
ss.dept_mountpoint(child),
'03_Отделы/Административный/Юридическая служба',
)
def test_pharmacy_mountpoint_zero_padded_with_address(self):
p = _FakePharmacy(17, 'Одесса, ул. Дерибасовская, 1')
self.assertEqual(ss.pharmacy_mountpoint(p),
'02_Аптеки/A-017 · Одесса, ул. Дерибасовская, 1')
def test_pharmacy_mountpoint_without_address(self):
self.assertEqual(ss.pharmacy_mountpoint(_FakePharmacy(3)), '02_Аптеки/A-003')
class ConfigGateTests(TestCase):
def test_service_raises_without_config(self):
# ConnectorConfig(nextcloud) не заведён — сервис должен упасть предсказуемо
with self.assertRaises(NextcloudConfigError):
NextcloudService()
class EnsureFolderTests(TestCase):
"""Идемпотентность ensure_folder без сети — низкоуровневые вызовы замоканы."""
def _service(self):
svc = object.__new__(NextcloudService) # без __init__ (не лезем в конфиг/сеть)
svc.calls = []
svc.create_folder = lambda mp: (svc.calls.append(('create', mp)) or 42)
svc.add_group = lambda fid, g: svc.calls.append(('add_group', fid, g))
svc.set_group_permissions = lambda fid, g, p: svc.calls.append(('perm', fid, g, p))
svc.set_quota = lambda fid, q: svc.calls.append(('quota', fid, q))
return svc
def test_creates_when_absent(self):
svc = self._service()
cache = {}
fid, created = svc.ensure_folder('03_Отделы/HR',
{'nc-dept-hr': NextcloudService.PERM_RW},
folder_cache=cache)
self.assertTrue(created)
self.assertEqual(fid, 42)
self.assertIn(('create', '03_Отделы/HR'), svc.calls)
self.assertIn(('perm', 42, 'nc-dept-hr', 15), svc.calls)
self.assertEqual(cache['03_Отделы/HR'], 42) # кэш обновлён
def test_skips_create_when_present(self):
svc = self._service()
fid, created = svc.ensure_folder('03_Отделы/HR',
{'nc-dept-hr': NextcloudService.PERM_RW},
folder_cache={'03_Отделы/HR': 7})
self.assertFalse(created)
self.assertEqual(fid, 7)
self.assertNotIn(('create', '03_Отделы/HR'), svc.calls)
self.assertIn(('perm', 7, 'nc-dept-hr', 15), svc.calls)
import datetime
import json
from django.test import Client, TestCase
from unittest.mock import patch
from core.models import ConnectorConfig, Directory, DirectoryType
from apps.nextcloud_connector.onlyoffice import (
can_edit_file, decode_file_token, document_key, encode_ds_jwt,
encode_file_token, is_allowed_ds_download_url, is_office_file,
is_previewable_file, office_content_type,
)
from apps.locations.models import Pharmacy
from apps.employees.models import Employee
from apps.nextcloud_connector.access import (
can_access_pharmacy_docs, can_edit_pharmacy_docs, safe_filename,
)
from core.test_utils import login_2fa
class SafeFilenameTests(TestCase):
def test_accepts_plain_names(self):
for n in ('licence.pdf', 'Договор 2026.docx', 'фото-1.jpg'):
self.assertEqual(safe_filename(n), n)
def test_rejects_traversal_and_separators(self):
for n in ('../secret', 'a/b.txt', 'a\\b.txt', '..', '.', '', None,
'.hidden', 'x\x00y'):
self.assertIsNone(safe_filename(n), f'должно отклонить: {n!r}')
class AccessControlTests(TestCase):
@classmethod
def setUpTestData(cls):
org_type = DirectoryType.objects.create(code='organization', name='Орг')
region_type = DirectoryType.objects.create(code='region', name='Регион')
region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
org = Directory.objects.create(directory_type=org_type, code='akc', name='АКС')
cls.ph_a = Pharmacy.objects.create(organization=org, location_region=region,
pharmacy_number=17, location_type='pharmacy')
cls.ph_b = Pharmacy.objects.create(organization=org, location_region=region,
pharmacy_number=18, location_type='pharmacy')
common = dict(mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1))
cls.emp_a = Employee.objects.create_user(
username='ph_a_emp', user_location_type='pharmacy', user_pharmacy=cls.ph_a, **common)
cls.emp_b = Employee.objects.create_user(
username='ph_b_emp', user_location_type='pharmacy', user_pharmacy=cls.ph_b, **common)
cls.staff = Employee.objects.create_user(username='office', is_staff=True, **common)
def test_pharmacy_employee_sees_only_own(self):
self.assertTrue(can_access_pharmacy_docs(self.emp_a, self.ph_a))
self.assertFalse(can_access_pharmacy_docs(self.emp_a, self.ph_b))
def test_pharmacy_employee_view_only_by_default(self):
# show_documents=own_or_staff, edit_pharmacy=staff
self.assertTrue(can_access_pharmacy_docs(self.emp_a, self.ph_a))
self.assertFalse(can_edit_pharmacy_docs(self.emp_a, self.ph_a))
def test_staff_needs_explicit_network_access(self):
from core.test_utils import grant_all_locations
self.assertFalse(can_access_pharmacy_docs(self.staff, self.ph_a))
grant_all_locations(self.staff)
self.assertTrue(can_access_pharmacy_docs(self.staff, self.ph_a))
self.assertTrue(can_access_pharmacy_docs(self.staff, self.ph_b))
self.assertTrue(can_edit_pharmacy_docs(self.staff, self.ph_a))
class DocumentsViewTests(TestCase):
@classmethod
def setUpTestData(cls):
org_type = DirectoryType.objects.create(code='organization', name='Орг')
region_type = DirectoryType.objects.create(code='region', name='Регион')
region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
org = Directory.objects.create(directory_type=org_type, code='akc', name='АКС')
cls.ph_a = Pharmacy.objects.create(organization=org, location_region=region,
pharmacy_number=17, location_type='pharmacy')
cls.ph_b = Pharmacy.objects.create(organization=org, location_region=region,
pharmacy_number=18, location_type='pharmacy')
common = dict(mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1))
cls.emp_a = Employee.objects.create_user(
username='ph_a_emp', user_location_type='pharmacy', user_pharmacy=cls.ph_a, **common)
def setUp(self):
self.client = Client()
def test_own_pharmacy_ok_but_storage_not_configured(self):
self.client.force_login(self.emp_a)
r = self.client.get(f'/pharmacies/{self.ph_a.pk}/documents/')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'не настроен') # коннектор не сконфигурирован в тестах
def test_other_pharmacy_forbidden(self):
self.client.force_login(self.emp_a)
r = self.client.get(f'/pharmacies/{self.ph_b.pk}/documents/')
self.assertEqual(r.status_code, 404)
def test_anonymous_redirected(self):
r = self.client.get(f'/pharmacies/{self.ph_a.pk}/documents/')
self.assertEqual(r.status_code, 302)
def test_download_rejects_bad_filename(self):
self.client.force_login(self.emp_a)
r = self.client.get(f'/pharmacies/{self.ph_a.pk}/documents/download/?name=../etc/passwd')
self.assertEqual(r.status_code, 400)
def test_download_other_pharmacy_forbidden(self):
self.client.force_login(self.emp_a)
r = self.client.get(f'/pharmacies/{self.ph_b.pk}/documents/download/?name=x.pdf')
self.assertEqual(r.status_code, 404)
def test_upload_forbidden_without_edit(self):
self.client.force_login(self.emp_a)
from django.core.files.uploadedfile import SimpleUploadedFile
r = self.client.post(
f'/pharmacies/{self.ph_a.pk}/documents/upload/',
{'file': SimpleUploadedFile('a.txt', b'x')},
)
self.assertEqual(r.status_code, 403)
from apps.nextcloud_connector.access import (
accessible_sections, get_section, safe_relpath,
)
class SafeRelpathTests(TestCase):
def test_empty_is_root(self):
self.assertEqual(safe_relpath(''), '')
self.assertEqual(safe_relpath(None), '')
def test_valid_nested(self):
self.assertEqual(safe_relpath('Приказы/2026'), 'Приказы/2026')
self.assertEqual(safe_relpath('/Приказы/'), 'Приказы')
def test_rejects_traversal(self):
for p in ('../etc', 'a/../b', 'a/./b', 'a//b', 'a/.hidden'):
self.assertIsNone(safe_relpath(p), f'должно отклонить: {p!r}')
class SectionsTests(TestCase):
@classmethod
def setUpTestData(cls):
common = dict(mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1))
cls.plain = Employee.objects.create_user(username='plain', **common)
cls.staff = Employee.objects.create_user(username='st', is_staff=True, **common)
cls.root = Employee.objects.create_user(
username='root', is_staff=True, is_superuser=True, **common)
def _keys(self, user):
return {s['key'] for s in accessible_sections(user)}
def test_plain_user_sections(self):
# без отдела — вкладки company/org/training без «Мой отдел»
self.assertEqual(self._keys(self.plain), {'company', 'org', 'training'})
def test_mine_tab_when_user_has_department(self):
from core.models import Directory, DirectoryType
dt = DirectoryType.objects.create(code='department', name='Відділ')
dept = Directory.objects.create(directory_type=dt, code='it', name='IT')
self.plain.department = dept
self.plain.save(update_fields=['department'])
keys = accessible_sections(self.plain)
self.assertEqual(keys[0]['key'], 'mine')
self.assertIn('mine', self._keys(self.plain))
def test_company_writable_only_for_staff(self):
company_plain = get_section(self.plain, 'company')
company_staff = get_section(self.staff, 'company')
self.assertFalse(company_plain['writable'])
self.assertTrue(company_staff['writable'])
def test_superuser_gets_hr(self):
self.assertIn('hr', self._keys(self.root))
self.assertNotIn('hr', self._keys(self.staff))
class DocumentsBrowserViewTests(TestCase):
@classmethod
def setUpTestData(cls):
from django.contrib.auth.models import Group
cls.user = Employee.objects.create_user(
username='browser', mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1))
# Любая группа = доступ к открытым разделам (в т.ч. /documents/).
cls.user.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
def setUp(self):
self.client = Client()
def test_browse_valid_section_ok(self):
self.client.force_login(self.user)
r = self.client.get('/documents/browse/?section=company')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'не настроен')
def test_browse_inaccessible_section_forbidden(self):
self.client.force_login(self.user)
r = self.client.get('/documents/browse/?section=hr') # не суперюзер
self.assertEqual(r.status_code, 403)
def test_browse_traversal_path_bad_request(self):
self.client.force_login(self.user)
r = self.client.get('/documents/browse/?section=company&path=../04_Кадровые дела')
self.assertEqual(r.status_code, 400)
def test_upload_to_readonly_section_forbidden(self):
self.client.force_login(self.user) # обычный юзер: company только чтение
r = self.client.post('/documents/upload/', {'section': 'company', 'path': ''})
self.assertEqual(r.status_code, 403)
def test_anonymous_redirected(self):
r = self.client.get('/documents/browse/?section=company')
self.assertEqual(r.status_code, 302)
def test_mkdir_rename_delete_readonly_forbidden(self):
self.client.force_login(self.user)
for url in ('/documents/mkdir/', '/documents/rename/', '/documents/delete/'):
r = self.client.post(url, {'section': 'company', 'path': '', 'name': 'x'})
self.assertEqual(r.status_code, 403, url)
def test_staff_mkdir_rename_delete(self):
from unittest.mock import patch
from django.contrib.auth.models import Group
staff = Employee.objects.create_user(
username='docs_staff', is_staff=True,
mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1),
)
staff.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
login_2fa(self.client, staff)
dav = patch('apps.nextcloud_connector.views.NextcloudWebDAVService').start()
self.addCleanup(patch.stopall)
inst = dav.return_value
inst.list_dir.return_value = []
r = self.client.post('/documents/mkdir/', {'section': 'company', 'path': '', 'name': 'Папка'})
self.assertEqual(r.status_code, 200)
inst.mkdir.assert_called_once()
r = self.client.post(
'/documents/rename/',
{'section': 'company', 'path': '', 'name': 'a.txt', 'new_name': 'b.txt'},
)
self.assertEqual(r.status_code, 200)
inst.move.assert_called_once()
r = self.client.post('/documents/delete/', {'section': 'company', 'path': '', 'name': 'b.txt'})
self.assertEqual(r.status_code, 200)
inst.delete.assert_called_once()
r = self.client.post('/documents/mkdir/', {'section': 'company', 'path': '', 'name': '../x'})
self.assertContains(r, 'Некорректное имя', status_code=200)
inst.mkdir.assert_called_once() # второй раз не вызван
r = self.client.post(
'/documents/create/',
{'section': 'company', 'path': '', 'type': 'docx', 'name': 'Протокол'},
)
self.assertEqual(r.status_code, 200)
inst.upload.assert_called_once()
uploaded_name = inst.upload.call_args[0][0]
self.assertTrue(str(uploaded_name).endswith('Протокол.docx'))
def test_rename_via_hx_prompt_header(self):
from unittest.mock import patch
from django.contrib.auth.models import Group
staff = Employee.objects.create_user(
username='docs_prompt', is_staff=True,
mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1),
)
staff.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
login_2fa(self.client, staff)
with patch('apps.nextcloud_connector.views.NextcloudWebDAVService') as dav:
dav.return_value.list_dir.return_value = []
r = self.client.post(
'/documents/rename/',
{'section': 'company', 'path': '', 'name': 'old.pdf'},
headers={'HX-Prompt': 'new.pdf'},
)
self.assertEqual(r.status_code, 200)
dav.return_value.move.assert_called_once()
from apps.nextcloud_connector.doc_acl import (
grant_owner_edit, resolve_access, user_can_create_in,
)
from apps.nextcloud_connector.services.sync_service import (
safe_component, dept_mountpoint,
)
class DocumentOwnerAclTests(TestCase):
"""Автор — edit; колеги — view; керівник відділу — edit."""
@classmethod
def setUpTestData(cls):
dtype = DirectoryType.objects.create(code='department', name='Відділ')
cls.dept = Directory.objects.create(directory_type=dtype, code='it', name='IT')
common = dict(
mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1),
user_location_type='office',
department=cls.dept,
)
cls.head = Employee.objects.create_user(username='acl_head', **common)
cls.author = Employee.objects.create_user(
username='acl_author', manager=cls.head, **common,
)
cls.peer = Employee.objects.create_user(
username='acl_peer', manager=cls.head, **common,
)
def test_owner_gets_edit_peer_view_head_edit(self):
grant_owner_edit(
self.author,
section_key='org',
path='doc.docx',
department_id=self.dept.pk,
is_folder=False,
)
owner = resolve_access(
self.author, section_key='org', path='doc.docx',
department_id=self.dept.pk,
)
peer = resolve_access(
self.peer, section_key='org', path='doc.docx',
department_id=self.dept.pk,
)
head = resolve_access(
self.head, section_key='org', path='doc.docx',
department_id=self.dept.pk,
)
self.assertTrue(owner.can_edit)
self.assertTrue(peer.can_view)
self.assertFalse(peer.can_edit)
self.assertTrue(head.can_edit)
def test_own_dept_can_create_without_folder_edit(self):
self.assertTrue(user_can_create_in(
self.author, section_key='org', path='', department_id=self.dept.pk,
))
folder = resolve_access(
self.author, section_key='org', path='', department_id=self.dept.pk,
)
self.assertTrue(folder.can_view)
self.assertFalse(folder.can_edit)
def test_upload_creates_owner_grant(self):
from django.contrib.auth.models import Group
from django.core.files.uploadedfile import SimpleUploadedFile
from unittest.mock import patch
self.author.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
client = Client()
client.force_login(self.author)
with patch('apps.nextcloud_connector.views.NextcloudWebDAVService') as dav:
dav.return_value.list_dir.return_value = []
r = client.post(
'/documents/upload/',
{
'section': 'mine',
'path': '',
'file': SimpleUploadedFile('mine.docx', b'PK'),
},
)
self.assertEqual(r.status_code, 200)
from apps.nextcloud_connector.models import DocumentAccessGrant
g = DocumentAccessGrant.objects.filter(
employee=self.author, section_key='org', path='mine.docx',
department_id=self.dept.pk, is_active=True,
).first()
self.assertIsNotNone(g)
self.assertEqual(g.level, 'edit')
class PathHardeningTests(TestCase):
def test_safe_component_strips_separators(self):
self.assertEqual(safe_component('a/b'), 'a-b')
self.assertEqual(safe_component('a\\b'), 'a-b')
self.assertEqual(safe_component('..'), '_')
self.assertNotIn('/', safe_component('../secret'))
def test_dept_mountpoint_no_traversal(self):
class _D:
name = '../04_Кадровые дела'
parent = None
pk = 1
mp = dept_mountpoint(_D())
self.assertTrue(mp.startswith('03_Отделы/'))
self.assertNotIn('/../', mp)
self.assertNotIn('/04_', mp) # имя схлопнуто в один сегмент
class ExchangeWritableTests(TestCase):
@classmethod
def setUpTestData(cls):
common = dict(mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1))
cls.plain = Employee.objects.create_user(username='ex_plain', **common)
cls.staff = Employee.objects.create_user(username='ex_staff', is_staff=True, **common)
def test_training_writable_only_for_staff(self):
self.assertFalse(get_section(self.plain, 'training')['writable'])
self.assertTrue(get_section(self.staff, 'training')['writable'])
class OnlyOfficeHelpersTests(TestCase):
def test_office_extensions(self):
self.assertTrue(is_office_file('Договор.docx'))
self.assertTrue(is_office_file('table.XLSX'))
self.assertFalse(is_office_file('scan.pdf'))
self.assertFalse(is_office_file('photo.jpg'))
self.assertTrue(can_edit_file('a.docx'))
self.assertFalse(can_edit_file('a.pdf'))
self.assertTrue(is_previewable_file('scan.pdf'))
self.assertTrue(is_previewable_file('photo.JPG'))
self.assertFalse(is_previewable_file('a.docx'))
def test_file_token_roundtrip(self):
payload = {'kind': 'section', 'section': 'company', 'path': '', 'name': 'a.docx', 'uid': 1}
token = encode_file_token(payload)
self.assertEqual(decode_file_token(token)['name'], 'a.docx')
def test_document_key_changes_with_mtime(self):
path = '01_Компания/a.docx'
k1 = document_key(path, size=10, modified='Wed, 01 Jan 2026 00:00:00 GMT')
k2 = document_key(path, size=11, modified='Thu, 02 Jan 2026 00:00:00 GMT')
self.assertNotEqual(k1, k2)
self.assertEqual(document_key(path, size=10, modified='Wed, 01 Jan 2026 00:00:00 GMT'), k1)
class OnlyOfficeViewTests(TestCase):
JWT_SECRET = 'test-jwt-secret-32-bytes-long!!'
@classmethod
def setUpTestData(cls):
from django.contrib.auth.models import Group
cls.user = Employee.objects.create_user(
username='oo_user', mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1), is_staff=True,
)
cls.user.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
ConnectorConfig.objects.create(
connector_type='onlyoffice',
is_enabled=True,
host='docs.test.local',
use_ssl=True,
token=cls.JWT_SECRET,
extra_config={'callback_base': 'https://portal.test.local'},
)
def setUp(self):
self.client = Client()
login_2fa(self.client, self.user)
def _file_token(self):
return encode_file_token({
'kind': 'section',
'section': 'company',
'path': '',
'name': 'a.docx',
'uid': self.user.pk,
'mode': 'edit',
})
def test_editor_rejects_non_office(self):
r = self.client.get('/documents/edit/?section=company&name=photo.jpg')
self.assertEqual(r.status_code, 400)
def test_editor_page_for_docx(self):
r = self.client.get('/documents/edit/?section=company&name=Договор.docx')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'DocsAPI')
self.assertContains(r, 'docs.test.local')
def test_content_rejects_garbage_token(self):
anon = Client()
jwt_hdr = f'Bearer {encode_ds_jwt({"payload": {"url": "https://portal.test.local/x"}}, self.JWT_SECRET)}'
r = anon.get(
'/documents/onlyoffice/content/not-a-token/',
HTTP_AUTHORIZATION=jwt_hdr,
)
self.assertEqual(r.status_code, 403)
def test_content_rejects_missing_jwt(self):
anon = Client()
r = anon.get(f'/documents/onlyoffice/content/{self._file_token()}/')
self.assertEqual(r.status_code, 403)
def test_callback_rejects_unsigned_body(self):
anon = Client()
with patch('apps.nextcloud_connector.views.fetch_saved_file') as fetch:
r = anon.post(
f'/documents/onlyoffice/callback/{self._file_token()}/',
data=json.dumps({
'status': 2,
'url': 'https://evil.example/payload',
}),
content_type='application/json',
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {'error': 1})
fetch.assert_not_called()
def test_callback_rejects_wrong_jwt_secret(self):
anon = Client()
bad = encode_ds_jwt(
{'status': 2, 'url': 'https://docs.test.local/cache/a.docx'},
'wrong-secret-32-bytes-long!!!!!',
)
with patch('apps.nextcloud_connector.views.fetch_saved_file') as fetch:
r = anon.post(
f'/documents/onlyoffice/callback/{self._file_token()}/',
data=json.dumps({'status': 2, 'url': 'https://docs.test.local/cache/a.docx'}),
content_type='application/json',
HTTP_AUTHORIZATION=f'Bearer {bad}',
)
self.assertEqual(r.json(), {'error': 1})
fetch.assert_not_called()
def test_callback_save_uploads_to_nextcloud(self):
ds_jwt = encode_ds_jwt(
{'status': 2, 'url': 'https://docs.test.local/cache/a.docx'},
self.JWT_SECRET,
)
anon = Client()
with patch('apps.nextcloud_connector.views.fetch_saved_file', return_value=b'DOCX') as fetch, \
patch('apps.nextcloud_connector.views.NextcloudWebDAVService') as dav_cls:
dav_cls.return_value.upload.return_value = None
r = anon.post(
f'/documents/onlyoffice/callback/{self._file_token()}/',
data=json.dumps({'status': 2, 'url': 'https://evil.example/ignore'}),
content_type='application/json',
HTTP_AUTHORIZATION=f'Bearer {ds_jwt}',
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {'error': 0})
fetch.assert_called_once()
self.assertEqual(fetch.call_args[0][0], 'https://docs.test.local/cache/a.docx')
dav_cls.return_value.upload.assert_called_once()
def test_ssrf_url_not_from_ds_host(self):
settings = {
'public_url': 'https://docs.test.local',
'download_hosts': [],
}
self.assertFalse(is_allowed_ds_download_url('https://evil.example/x', settings))
self.assertTrue(is_allowed_ds_download_url('https://docs.test.local/cache/a.docx', settings))
def test_content_accepts_jwt_query_param(self):
ds_jwt = encode_ds_jwt({'url': 'https://portal.test.local/x'}, self.JWT_SECRET)
anon = Client()
with patch('apps.nextcloud_connector.views.NextcloudWebDAVService') as dav:
dav.return_value.download.return_value = (b'PKDOCX', 'application/octet-stream')
r = anon.get(
f'/documents/onlyoffice/content/{self._file_token()}/',
{'token': ds_jwt},
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content, b'PKDOCX')
self.assertIn(
'officedocument.wordprocessingml.document',
r['Content-Type'],
)
class DocumentPreviewTests(TestCase):
@classmethod
def setUpTestData(cls):
from django.contrib.auth.models import Group
org_type = DirectoryType.objects.create(code='organization', name='Орг')
region_type = DirectoryType.objects.create(code='region', name='Регион')
region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
org = Directory.objects.create(directory_type=org_type, code='akc', name='АКС')
cls.ph = Pharmacy.objects.create(
organization=org, location_region=region,
pharmacy_number=33, location_type='pharmacy',
)
common = dict(
mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1),
)
cls.emp = Employee.objects.create_user(
username='prev_emp',
user_location_type='pharmacy',
user_pharmacy=cls.ph,
**common,
)
cls.staff = Employee.objects.create_user(
username='prev_staff', is_staff=True, **common,
)
# раздел /documents/ требует любую группу (perm=None)
cls.staff.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
def setUp(self):
self.client = Client()
def test_pharmacy_preview_page_for_pdf(self):
self.client.force_login(self.emp)
r = self.client.get(
f'/pharmacies/{self.ph.pk}/documents/preview/?name=scan.pdf',
)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'doc-preview-frame')
self.assertContains(r, 'inline=1')
def test_pharmacy_preview_page_for_image(self):
self.client.force_login(self.emp)
r = self.client.get(
f'/pharmacies/{self.ph.pk}/documents/preview/?name=фото.png',
)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'doc-preview-image')
def test_preview_rejects_office_file(self):
self.client.force_login(self.emp)
r = self.client.get(
f'/pharmacies/{self.ph.pk}/documents/preview/?name=a.docx',
)
self.assertEqual(r.status_code, 400)
def test_inline_download_serves_pdf(self):
self.client.force_login(self.emp)
with patch('apps.nextcloud_connector.views.NextcloudWebDAVService') as dav:
dav.return_value.download.return_value = (b'%PDF-1.4', 'application/octet-stream')
r = self.client.get(
f'/pharmacies/{self.ph.pk}/documents/download/?name=scan.pdf&inline=1',
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r['Content-Type'], 'application/pdf')
self.assertIn('inline', r['Content-Disposition'])
self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN')
def test_section_preview_for_staff(self):
login_2fa(self.client, self.staff)
r = self.client.get('/documents/preview/?section=company&name=scan.pdf')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'doc-preview-frame')
class PharmacyOnlyOfficeAclTests(TestCase):
JWT_SECRET = 'test-jwt-secret-32-bytes-long!!'
@classmethod
def setUpTestData(cls):
from django.contrib.auth.models import Group
from core.test_utils import grant_all_locations
org_type = DirectoryType.objects.create(code='organization', name='Орг')
region_type = DirectoryType.objects.create(code='region', name='Регион')
region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
org = Directory.objects.create(directory_type=org_type, code='akc', name='АКС')
cls.ph = Pharmacy.objects.create(
organization=org, location_region=region,
pharmacy_number=21, location_type='pharmacy',
)
common = dict(
mattermost_sync_enabled=False, ad_sync_enabled=False,
hire_date=datetime.date(2020, 1, 1),
)
cls.viewer = Employee.objects.create_user(
username='ph_viewer', user_location_type='pharmacy',
user_pharmacy=cls.ph, **common,
)
cls.editor = Employee.objects.create_user(
username='ph_editor', is_staff=True, **common,
)
grant_all_locations(cls.editor)
cls.viewer.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
cls.editor.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
ConnectorConfig.objects.create(
connector_type='onlyoffice',
is_enabled=True,
host='docs.test.local',
use_ssl=True,
token=cls.JWT_SECRET,
extra_config={'callback_base': 'https://portal.test.local'},
)
def setUp(self):
self.client = Client()
def test_viewer_opens_readonly(self):
login_2fa(self.client, self.viewer)
with patch('apps.nextcloud_connector.views.NextcloudWebDAVService') as dav:
dav.return_value.stat.return_value = {'size': 10, 'modified': None}
r = self.client.get(
f'/documents/edit/?pharmacy={self.ph.pk}&name=Договор.docx',
)
self.assertEqual(r.status_code, 200)
self.assertContains(r, '"mode": "view"')
def test_editor_opens_writable(self):
login_2fa(self.client, self.editor)
with patch('apps.nextcloud_connector.views.NextcloudWebDAVService') as dav:
dav.return_value.stat.return_value = {'size': 10, 'modified': None}
r = self.client.get(
f'/documents/edit/?pharmacy={self.ph.pk}&name=Договор.docx',
)
self.assertEqual(r.status_code, 200)
self.assertContains(r, '"mode": "edit"')
def test_callback_rejects_save_for_viewer(self):
token = encode_file_token({
'kind': 'pharmacy',
'pharmacy_id': self.ph.pk,
'name': 'a.docx',
'uid': self.viewer.pk,
'mode': 'view',
})
ds_jwt = encode_ds_jwt(
{'status': 2, 'url': 'https://docs.test.local/cache/a.docx'},
self.JWT_SECRET,
)
with patch('apps.nextcloud_connector.views.fetch_saved_file') as fetch, \
patch('apps.nextcloud_connector.views.NextcloudWebDAVService') as dav_cls:
r = Client().post(
f'/documents/onlyoffice/callback/{token}/',
data=json.dumps({'status': 2, 'url': 'https://docs.test.local/cache/a.docx'}),
content_type='application/json',
HTTP_AUTHORIZATION=f'Bearer {ds_jwt}',
)
self.assertEqual(r.json(), {'error': 1})
fetch.assert_not_called()
dav_cls.return_value.upload.assert_not_called()
class BlankOfficeCreateTests(TestCase):
def test_office_mime(self):
self.assertIn('wordprocessingml', office_content_type('a.docx'))
self.assertIn('spreadsheetml', office_content_type('a.xlsx'))