odoo18/addons_extensions/documents/tests/test_documents_document.py

556 lines
26 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from datetime import datetime, timedelta
from unittest import skip
from odoo import Command, http
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests.common import new_test_user
from odoo.tests import users
from .test_documents_common import TransactionCaseDocuments, GIF, TEXT
DATA = "data:application/zip;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs="
file_a = {'name': 'doc.zip', 'data': 'data:application/zip;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs='}
file_b = {'name': 'icon.zip', 'data': 'data:application/zip;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs='}
class TestCaseDocuments(TransactionCaseDocuments):
@skip("TODO: move to controller")
@users('documents@example.com')
def test_documents_action_log_access_archived(self):
access = self.env['documents.access'].search([
('document_id', '=', self.document_txt.id),
('partner_id', '=', self.env.user.partner_id.id),
])
self.assertFalse(access)
self.document_txt.action_archive()
self.env['documents.document'].action_log_access(
self.document_txt.access_token)
access = self.env['documents.access'].search([
('document_id', '=', self.document_txt.id),
('partner_id', '=', self.env.user.partner_id.id),
])
self.assertTrue(access)
def test_documents_create_from_attachment(self):
"""
Tests a documents.document create method when created from an already existing ir.attachment.
"""
attachment = self.env['ir.attachment'].create({
'datas': GIF,
'name': 'attachmentGif.gif',
'res_model': 'documents.document',
'res_id': 0,
})
document_a = self.env['documents.document'].create({
'folder_id': self.folder_b.id,
'name': 'new name',
'attachment_id': attachment.id,
})
self.assertEqual(document_a.attachment_id.id, attachment.id,
'the attachment should be the attachment given in the create values')
self.assertEqual(document_a.name, 'new name',
'the name given should be used')
self.assertEqual(document_a.res_model, 'documents.document',
'the res_model should be set as document by default')
self.assertEqual(document_a.res_id, document_a.id,
'the res_id should be set as its own id by default to allow access right inheritance')
@users('documents@example.com')
def test_documents_create_write(self):
"""
Tests a documents.document create and write method,
documents should automatically create a new ir.attachments in relevant cases.
"""
document_a = self.env['documents.document'].create({
'name': 'Test mimetype gif',
'datas': GIF,
'folder_id': self.folder_b.id,
})
self.assertEqual(document_a.res_model, 'documents.document',
'the res_model should be set as document by default')
self.assertEqual(document_a.res_id, document_a.id,
'the res_id should be set as its own id by default to allow access right inheritance')
self.assertEqual(document_a.attachment_id.datas, GIF, 'the document should have a GIF data')
document_no_attachment = self.env['documents.document'].create({
'name': 'Test mimetype gif',
'folder_id': self.folder_b.id,
})
self.assertFalse(document_no_attachment.attachment_id, 'the new document shouldnt have any attachment_id')
document_no_attachment.write({'datas': TEXT})
self.assertEqual(document_no_attachment.attachment_id.datas, TEXT, 'the document should have an attachment')
def test_documents_create_performance(self):
folders = self.env['documents.document'].create([
{'type': 'folder', 'name': f'Folder {i}', 'access_internal': 'view'}
for i in range(50)
])
folders.flush_recordset()
folders.invalidate_recordset()
with self.assertQueryCount(162):
self.env['documents.document'].create([{
'folder_id': folder.id,
'type': 'binary',
} for folder in folders])
def test_documents_share_links(self):
"""
Tests document share links
"""
# todo: transform into testing sharing a shortcut document with expiration
# by Folder
pass
def test_documents_share_popup(self):
shared_folder = self.env['documents.document'].create({
'type': 'folder',
'name': 'share folder',
'children_ids': [
Command.create({'type': 'binary', 'datas': GIF, 'name': 'file.gif', 'mimetype': 'image/gif'}),
Command.create({'type': 'url', 'url': 'https://ftprotech.in'}),
],
})
share_tag = self.env['documents.tag'].create({
'name': "share category > share tag",
})
shared_folder.children_ids[0].tag_ids = [Command.set(share_tag.ids)]
# todo
# self.assertEqual(shared_folder.links_count, 0, "There should be no links counted in this share")
def test_default_res_id_model(self):
"""
Test default res_id and res_model from context are used for linking attachment to document.
"""
document = self.env['documents.document'].create({'folder_id': self.folder_b.id})
attachment = self.env['ir.attachment'].with_context(
default_res_id=document.id,
default_res_model=document._name,
).create({
'name': 'attachmentGif.gif',
'datas': GIF,
})
self.assertEqual(attachment.res_id, document.id, "It should be linked to the default res_id")
self.assertEqual(attachment.res_model, document._name, "It should be linked to the default res_model")
self.assertEqual(document.attachment_id, attachment, "Document should be linked to the created attachment")
@users('documents@example.com')
def test_versioning(self):
"""
Tests the versioning/history of documents
"""
document = self.env["documents.document"].create(
{
"datas": GIF,
"folder_id": self.folder_b.id,
"res_model": "res.users",
"res_id": self.doc_user.id,
}
)
def check_attachment_res_fields(
attachment, expected_res_model, expected_res_id
):
self.assertEqual(
attachment.res_model,
expected_res_model,
"The attachment should be linked to the right model",
)
self.assertEqual(
attachment.res_id,
expected_res_id,
"The attachment should be linked to the right record",
)
self.assertEqual(len(document.previous_attachment_ids.ids), 0, "The history should be empty")
original_attachment = document.attachment_id
check_attachment_res_fields(original_attachment, "res.users", self.doc_user.id)
document.write({'datas': TEXT})
new_attachment = document.previous_attachment_ids
check_attachment_res_fields(original_attachment, "res.users", self.doc_user.id)
check_attachment_res_fields(new_attachment, "documents.document", document.id)
self.assertEqual(len(document.previous_attachment_ids), 1)
self.assertNotEqual(document.previous_attachment_ids, original_attachment)
self.assertEqual(document.previous_attachment_ids[0].datas, GIF, "The history should have the right content")
self.assertEqual(document.attachment_id.datas, TEXT, "The document should have the right content")
old_attachment = document.attachment_id
document.write({'attachment_id': new_attachment.id})
check_attachment_res_fields(new_attachment, "res.users", self.doc_user.id)
check_attachment_res_fields(old_attachment, "documents.document", document.id)
self.assertEqual(document.attachment_id.id, new_attachment.id, "the document should contain the new attachment")
self.assertEqual(document.previous_attachment_ids, original_attachment, "the history should contain the original attachment")
document.write({"attachment_id": document.attachment_id.id})
check_attachment_res_fields(new_attachment, "res.users", self.doc_user.id)
self.assertEqual(
document.attachment_id.id,
new_attachment.id,
"the document attachment should not have changed",
)
self.assertTrue(
new_attachment not in document.previous_attachment_ids,
"the history should not contain the new attachment",
)
document.write({'datas': DATA})
self.assertEqual(document.attachment_id, new_attachment)
def test_write_mimetype(self):
"""
Tests the consistency of documents' mimetypes
"""
document = self.env['documents.document'].with_user(self.doc_user.id).create({'datas': GIF, 'folder_id': self.folder_b.id})
document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'text/plain'})
self.assertEqual(document.mimetype, 'text/plain', "the new mimetype should be the one given on write")
document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'})
self.assertEqual(document.mimetype, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', "should preserve office mime type")
def test_cascade_delete(self):
"""
Makes sure that documents are unlinked when their attachment is unlinked.
"""
document = self.env['documents.document'].create({'datas': GIF, 'folder_id': self.folder_b.id})
self.assertTrue(document.exists(), 'the document should exist')
document.attachment_id.unlink()
self.assertFalse(document.exists(), 'the document should not exist')
def test_is_favorited(self):
user = new_test_user(self.env, "test user", groups='documents.group_documents_user')
document = self.env['documents.document'].create({'datas': GIF, 'folder_id': self.folder_b.id})
document.favorited_ids = user
self.assertFalse(document.is_favorited)
self.assertTrue(document.with_user(user).is_favorited)
def test_neuter_mimetype(self):
"""
Tests that potentially harmful mimetypes (XML mimetypes that can lead to XSS attacks) are converted to text
In fact this logic is implemented in the base `IrAttachment` model but was originally duplicated.
The test stays duplicated here to ensure the de-duplicated logic still catches our use cases.
"""
self.folder_b.action_update_access_rights(partners={self.doc_user.partner_id: ('edit', False)})
document = self.env['documents.document'].create({'datas': GIF, 'folder_id': self.folder_b.id})
document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'text/xml'})
self.assertEqual(document.mimetype, 'text/plain', "XML mimetype should be forced to text")
document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'image/svg+xml'})
self.assertEqual(document.mimetype, 'text/plain', "SVG mimetype should be forced to text")
document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'text/html'})
self.assertEqual(document.mimetype, 'text/plain', "HTML mimetype should be forced to text")
document.with_user(self.doc_user.id).write({'datas': TEXT, 'mimetype': 'application/xhtml+xml'})
self.assertEqual(document.mimetype, 'text/plain', "XHTML mimetype should be forced to text")
def test_create_from_message_invalid_tags(self):
"""
Create a new document from message with a deleted tag, it should keep only existing tags.
"""
message = self.env['documents.document'].message_new({
'subject': 'Test',
}, {
'tag_ids': [(6, 0, [self.tag_b.id, -1])],
'folder_id': self.folder_a.id,
})
self.assertEqual(message.tag_ids.ids, [self.tag_b.id], "Should only keep the existing tag")
def test_file_extension(self):
""" Test the detection of the file extension and its edition. """
sanitized_extension = 'txt'
for extension in ('.txt', ' .txt', '..txt', '.txt ', ' .txt ', ' .txt '):
document = self.env['documents.document'].create({
'datas': base64.b64encode(b"Test"),
'name': f'name{extension}',
'mimetype': 'text/plain',
'folder_id': self.folder_b.id,
})
self.assertEqual(document.file_extension, sanitized_extension,
f'"{extension}" must be sanitized to "{sanitized_extension}" at creation')
for extension in ('txt', ' txt', ' txt ', '.txt', ' .txt', ' .txt ', '..txt', ' ..txt '):
document.file_extension = extension
self.assertEqual(document.file_extension, sanitized_extension,
f'"{extension}" must be sanitized to "{sanitized_extension}" at edition')
# test extension when filename is changed (i.e. name is edited or file is replaced)
document.name = 'test.png'
self.assertEqual(document.file_extension, 'png', "extension must be updated on change in filename")
def test_restricted_folder_multi_company(self):
"""
Tests the behavior of a restricted folder in a multi-company environment
"""
company_a = self.env.company
company_b = self.env['res.company'].create({'name': 'Company B'})
user_b = self.env['res.users'].create({
'name': 'User of company B',
'login': 'user_b',
'groups_id': [(6, 0, [self.ref('documents.group_documents_manager')])],
'company_id': company_b.id,
'company_ids': [(6, 0, [company_b.id])]
})
self.folder_a.company_id = company_a
self.assertEqual(self.folder_a.display_name, 'folder A',
"The parent folder's name should not be hidden")
self.assertEqual(self.folder_a.with_user(user_b).display_name, 'Restricted',
"The parent folder's name should be hidden")
self.assertEqual(self.folder_a_a.display_name, "folder A - A",
"The parent folder name should not be included in the name")
def test_unlink_attachments_with_documents(self):
"""
Tests a documents.document unlink method.
Attachments should be deleted when related documents are deleted,
for which res_model is not 'documents.document'.
Test case description:
Case 1:
- upload a document with res_model 'res.partner'.
- check if attachment exists.
- unlink the document.
- check if attachment exists or not.
Case 2:
- ensure the existing flow for res_model 'documents.document'
does not break.
"""
document = self.env['documents.document'].create({
'datas': GIF,
'folder_id': self.folder_b.id,
'res_model': 'res.partner',
})
self.assertTrue(document.attachment_id.exists(), 'the attachment should exist')
attachment = document.attachment_id
document.unlink()
self.assertFalse(attachment.exists(), 'the attachment should not exist')
self.assertTrue(self.document_txt.attachment_id.exists(), 'the attachment should exist')
attachment_a = self.document_txt.attachment_id
self.document_txt.unlink()
self.assertFalse(attachment_a.exists(), 'the attachment should not exist')
def test_archive_and_unarchive_document(self):
self.document_txt.action_archive()
self.assertFalse(self.document_txt.active, 'the document should be inactive')
self.document_txt.action_unarchive()
self.assertTrue(self.document_txt.active, 'the document should be active')
def test_unarchive_document_with_archived_parent(self):
"""Unarchive a document whose parent folder is archived should send an error."""
document = self.document_txt
def check_error_message(document):
with self.assertRaises(UserError) as err:
document.action_unarchive()
self.assertEqual(
err.exception.args[0],
"Item(s) you wish to restore are included in archived folders. "
"To restore these items, you must restore the following including folders instead:"
"\n"
"- folder B"
)
self.folder_b.folder_id = self.folder_a # when the parent has folder_id
self.folder_b.action_archive()
check_error_message(document)
self.folder_b.folder_id = False # when the parent has folder_id False
check_error_message(document)
def test_delete_document(self):
self.document_txt.action_archive()
self.assertFalse(self.document_txt.active, 'the document should be inactive')
self.document_txt.unlink()
self.assertFalse(self.document_txt.exists(), 'the document should not exist')
def test_copy_document(self):
copy = self.document_txt.copy()
self.assertEqual(copy.name, "file.txt (copy)")
self.assertNotEqual(
copy.attachment_id.ensure_one().id,
self.document_txt.attachment_id.id,
"There must be a new attachment"
)
self.assertEqual(copy.raw, self.document_txt.raw)
copy_with_default = self.document_txt.copy({"name": "test"})
self.assertEqual(copy_with_default.name, "test")
self.assertNotEqual(
copy.attachment_id.ensure_one().id,
self.document_txt.attachment_id.id,
"There must be a new attachment"
)
self.assertEqual(copy.raw, self.document_txt.raw)
# check that we can copy in a folder inside the company folder
self.assertFalse(self.folder_a.folder_id)
self.folder_a.owner_id = self.env.ref("base.user_root")
self.folder_a.access_internal = 'edit'
# Special case where we can not write, but `user_permission == edit` because
# the folder is in the company root
with self.assertRaises(AccessError):
self.folder_a.with_user(self.internal_user).check_access('write')
self.assertEqual(self.folder_a.with_user(self.internal_user).user_permission, 'edit')
self.document_txt.folder_id = self.folder_a
self.document_txt.with_user(self.internal_user).copy()
def test_document_thumbnail_status(self):
for mimetype in ['application/pdf', 'application/pdf;base64']:
with self.subTest(mimetype=mimetype):
pdf_document = self.env['documents.document'].create({
'name': 'Test PDF doc',
'mimetype': mimetype,
'datas': "JVBERi0gRmFrZSBQREYgY29udGVudA==",
'folder_id': self.folder_b.id,
})
self.assertEqual(pdf_document.thumbnail, False)
self.assertEqual(pdf_document.thumbnail_status, 'client_generated')
word_document = self.env['documents.document'].create({
'name': 'Test DOC',
'mimetype': 'application/msword',
'folder_id': self.folder_b.id,
})
self.assertEqual(word_document.thumbnail, False)
self.assertEqual(word_document.thumbnail_status, False)
for mimetype in ['image/bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/tiff', 'image/x-icon', 'image/webp']:
with self.subTest(mimetype=mimetype):
image_document = self.env['documents.document'].create({
'name': 'Test image doc',
'mimetype': mimetype,
'datas': GIF,
'folder_id': self.folder_b.id,
})
self.assertEqual(image_document.thumbnail, GIF)
self.assertEqual(image_document.thumbnail_status, 'present')
def test_document_max_upload_limit(self):
Doc = self.env['documents.document']
ICP = self.env['ir.config_parameter']
key_doc = 'document.max_fileupload_size'
key_web = 'web.max_file_upload_size'
ICP.set_param(key_doc, 20)
ICP.set_param(key_web, 10)
self.assertEqual(Doc.get_document_max_upload_limit(), 20)
ICP.set_param(key_doc, 0)
self.assertEqual(Doc.get_document_max_upload_limit(), None)
ICP.search([('key', '=', key_doc)]).unlink()
self.assertEqual(Doc.get_document_max_upload_limit(), 10)
ICP.search([('key', '=', key_web)]).unlink()
self.assertEqual(
Doc.get_document_max_upload_limit(),
http.DEFAULT_MAX_CONTENT_LENGTH
)
def test_document_order_by_is_folder(self):
# check that the order is "folder first", and then most recent first
doc_1 = self.env['documents.document'].create([{'name': 'D1'}])
doc_2 = self.env['documents.document'].create([{'name': 'D2', 'type': 'folder'}])
doc_3 = self.env['documents.document'].create([{'name': 'D3', 'type': 'url'}])
doc_4 = self.env['documents.document'].create([{'name': 'D4'}])
docs = doc_1 | doc_2 | doc_3 | doc_4
result = self.env['documents.document'].search([('id', 'in', docs.ids)], order='is_folder, create_date DESC, id DESC')
self.assertEqual(result[0], doc_2)
self.assertEqual(result[1], doc_4)
self.assertEqual(result[2], doc_3)
self.assertEqual(result[3], doc_1)
def test_document_order_by_last_access_date(self):
documents = self.env['documents.document'].create([{'name': 'D1'}, {'name': 'D2'}])
self.env['documents.access'].create([{
'document_id': documents[0].id,
'last_access_date': datetime.now() + timedelta(days=1),
'partner_id': self.env.user.partner_id.id,
}, {
'document_id': documents[1].id,
'last_access_date': datetime.now() + timedelta(days=2),
'partner_id': self.env.user.partner_id.id,
}])
result = self.env['documents.document'].search([('id', 'in', documents.ids)], order='last_access_date_group DESC')
self.assertEqual(result[0], documents[1])
self.assertEqual(result[1], documents[0])
result = self.env['documents.document'].search([('id', 'in', documents.ids)], order='last_access_date_group ASC')
self.assertEqual(result[0], documents[0])
self.assertEqual(result[1], documents[1])
def test_document_group_by_last_access_date(self):
Doc = self.env['documents.document']
documents = Doc.create([{'name': f'D{i}'} for i in range(6)])
self.env['documents.access'].create([{
'document_id': documents[0].id,
'last_access_date': datetime.now() - timedelta(hours=1),
'partner_id': self.env.user.partner_id.id,
}, {
'document_id': documents[1].id,
'last_access_date': datetime.now() - timedelta(days=2),
'partner_id': self.env.user.partner_id.id,
}, {
'document_id': documents[2].id,
'last_access_date': datetime.now() - timedelta(days=8),
'partner_id': self.env.user.partner_id.id,
}, {
'document_id': documents[3].id,
'last_access_date': datetime.now() - timedelta(days=40),
'partner_id': self.env.user.partner_id.id,
}, {
'document_id': documents[4].id,
'last_access_date': datetime.now() - timedelta(minutes=1),
'partner_id': self.env.user.partner_id.id,
}, {
'document_id': documents[5].id,
'last_access_date': datetime.now() - timedelta(days=1, hours=5),
'partner_id': self.env.user.partner_id.id,
}])
result = Doc. web_read_group(
[('id', 'in', documents.ids)],
['id', 'name'],
groupby=['last_access_date_group'],
orderby='last_access_date_group DESC')['groups']
self.assertEqual(len(result), 4)
self.assertEqual(result[0]['last_access_date_group'], '3_day')
self.assertEqual(result[0]['last_access_date_group_count'], 2)
result_day = Doc.search(result[0]['__domain'])
self.assertEqual(result_day[0], documents[4])
self.assertEqual(result_day[1], documents[0])
self.assertEqual(result_day.mapped('last_access_date_group'), ['3_day'] * 2)
self.assertEqual(result[1]['last_access_date_group'], '2_week')
self.assertEqual(result[1]['last_access_date_group_count'], 2)
result_week = Doc.search(result[1]['__domain'])
self.assertEqual(result_week[0], documents[5])
self.assertEqual(result_week[1], documents[1])
self.assertEqual(result_week.mapped('last_access_date_group'), ['2_week'] * 2)
self.assertEqual(result[2]['last_access_date_group'], '1_month')
self.assertEqual(result[2]['last_access_date_group_count'], 1)
self.assertEqual(Doc.search(result[2]['__domain']), documents[2])
self.assertEqual(documents[2].last_access_date_group, '1_month')
self.assertEqual(result[3]['last_access_date_group'], '0_older')
self.assertEqual(result[3]['last_access_date_group_count'], 1)
self.assertEqual(Doc.search(result[3]['__domain']), documents[3])
self.assertEqual(documents[3].last_access_date_group, '0_older')
def test_link_constrains(self):
folder = self.env['documents.document'].create({'name': 'folder', 'type': 'folder'})
for url in ("wrong URL format", "https:/ example.com", "test https://example.com"):
with self.assertRaises(ValidationError):
self.env['documents.document'].create({
'name': 'Test Document',
'folder_id': folder.id,
'url': url,
})