import json import pathlib import tempfile from datetime import datetime, timedelta from http import HTTPStatus import mock import pytest from PIL import Image from modules.archive import lib from modules.archive.lib import get_or_create_domain, get_new_archive_files, delete_archives, do_archive, get_domains from modules.archive.models import Archive, Domain from wrolpi.db import get_db_session from wrolpi.files import lib as files_lib from wrolpi.files.models import FileGroup from wrolpi.root_api import CustomJSONEncoder from wrolpi.test.common import skip_circleci def make_fake_request_archive(readability=True, screenshot=True, title=True): async def fake_request_archive(_): with tempfile.NamedTemporaryFile(suffix='.png') as fh: Image.new('RGB', (1, 1), color='grey').save(fh) fh.seek(0) image = fh.read() singlefile = '''' make_files_structure({ # These are all for an individual Archive. 'archive/example.com/2021-10-05-16-20-10_NA.html': singlefile_contents, 'archive/example.com/2021-10-05-16-20-10_NA.png': None, 'archive/example.com/2021-10-05-16-20-10_NA.readability.txt': 'article text contents', 'archive/example.com/2021-10-05-16-20-10_NA.readability.json': '{"url": "https://example.com"}', 'archive/example.com/2021-10-05-16-20-10_NA.readability.html': '', # These should log an error because they are missing the singlefile path. 'archive/example.com/2021-10-05 16:20:10.readability.html': '', 'archive/example.com/2021-10-05 16:20:10.readability.json': None, # Bad files should also be ignored. 'archive/example.com/random file': 'hello', 'archive/example.com/2021-10-05 16:20:10 no extension': None, }) # The single archive is found. test_client.post('/api/files/refresh') assert test_session.query(Archive).count() == 1 # Cause a re-index of the archive. test_session.query(FileGroup).filter_by( primary_path=str(test_directory / 'archive/example.com/2021-10-05-16-20-10_NA.html')).one().indexed = False test_session.commit() # Running the refresh does not result in a new archive. test_client.post('/api/files/refresh') assert test_session.query(Archive).count() == 1 # Archives file format was changed, lets check the new formats are found. make_files_structure({ 'archive/example.com/2021-10-05-16-20-11_The Title.html': '', 'archive/example.com/2021-10-05-16-20-11_The Title.readability.json': '{"url": "https://example.com"}', }) test_client.post('/api/files/refresh') # The old formatted archive above is renamed. assert (test_directory / 'archive/example.com/2021-10-05-16-20-10_NA.html').is_file() assert test_session.query(Archive).count() == 2 content = json.dumps({'search_str': 'text', 'model': 'archive'}) request, response = test_client.post('/api/files/search', content=content) assert response.status_code == HTTPStatus.OK assert response.json['file_groups'], 'No files matched "text"' assert response.json['file_groups'][0]['model'] == 'archive', 'Returned file was not an archive' assert response.json['file_groups'][0]['data'].get('readability_path') == \ 'archive/example.com/2021-10-05-16-20-10_NA.readability.html', \ 'Could not find readability html file' assert response.json['file_groups'][0]['data'].get('readability_txt_path') == \ 'archive/example.com/2021-10-05-16-20-10_NA.readability.txt', \ 'Could not find readability text file containing "text"' assert response.json['file_groups'][0]['data']['archive_datetime'], 'Archive has no datetime' @pytest.mark.asyncio async def test_refresh_archives_index(test_session, test_directory, test_client, make_files_structure): """Archives are indexed using ArchiveIndexer and archive_modeler.""" # The start of a typical singlefile html file. singlefile_contents = ''' ''' singlefile, *_ = make_files_structure({ 'archive/example.com/2021-10-05-16-20-10_NA.html': singlefile_contents, 'archive/example.com/2021-10-05-16-20-10_NA.png': None, 'archive/example.com/2021-10-05-16-20-10_NA.readability.txt': 'article text contents', 'archive/example.com/2021-10-05-16-20-10_NA.readability.json': '{"url": "https://example.com", "title": "the title"}', 'archive/example.com/2021-10-05-16-20-10_NA.readability.html': '', }) await files_lib.refresh_files() archive: Archive = test_session.query(Archive).one() assert archive.singlefile_path == singlefile assert archive.file_group.a_text == 'the title' assert archive.file_group.d_text == 'article text contents' assert archive.file_group.indexed is True @pytest.mark.asyncio async def test_refresh_archives_deleted_singlefile(test_session, make_files_structure, singlefile_contents_factory): """Removing a Singlefile file from a FileGroup makes that group no longer an Archive.""" singlefile, readability = make_files_structure({ '2022-09-04-16-20-11_The Title.html': singlefile_contents_factory(), '2022-09-04-16-20-11_The Title.readability.json': '{"url": "https://example.com"}', }) await files_lib.refresh_files() assert test_session.query(FileGroup).one().model == 'archive' assert test_session.query(Archive).count() == 1 # Remove singlefile, FileGroup is no longer an Archive. singlefile.unlink() await files_lib.refresh_files() assert test_session.query(FileGroup).one().model is None assert test_session.query(Archive).count() == 0 @pytest.mark.parametrize( 'name,expected', [ ('foo', False), ('2000-01-01-00-00-00_Some NA.html', True), ('2000-01-01-00-00-00_Some NA.readability.html', False), ('2000-01-01-00-00-00_ジにてこちら.html', True), ('2000-01-01-00-00-00_Some Title.readability.txt', False), ('foo.txt', False), ('foo.html', True), ] ) def test_is_singlefile_file(name, expected, make_files_structure, singlefile_contents_factory): """A Singlefile may not be created by WROLPi, an HTML file created by Singlefile can also be an Archive.""" path, = make_files_structure({name: singlefile_contents_factory()}) assert lib.is_singlefile_file(path) == expected def test_archive_files(test_directory): af = lib.ArchiveFiles( singlefile=test_directory / 'archive/singlefile.html', ) assert repr(af) == \ "" def test_archive_order(test_session, test_directory, archive_factory): archive1 = archive_factory() archive2 = archive_factory() archive2.archive_datetime += timedelta(seconds=10) test_session.commit() assert archive2 > archive1 assert archive1 < archive2 @pytest.mark.asyncio async def test_archive_download_index(test_session, test_directory, image_file): """An Archive is indexed when it is downloaded.""" with mock.patch('modules.archive.lib.request_archive') as mock_request_archive: singlefile = 'the singlefile' readability = dict( content='the readability', textContent='the readability', ) mock_request_archive.return_value = (singlefile, readability, image_file.read_bytes()) archive = await lib.do_archive('https://example.com') assert isinstance(archive, lib.Archive), 'Did not get an archive' assert archive.singlefile_path and archive.singlefile_path.exists() and \ archive.singlefile_path.read_text() == singlefile, 'Singlefile was not stored' assert archive.readability_path and archive.readability_path.exists() assert archive.readability_json_path assert archive.readability_txt_path and archive.readability_txt_path.exists() assert archive.file_group.d_text == 'the readability', 'Readability text was not indexed' assert archive.file_group.title == 'the singlefile', 'Did not get the title from the singlefile' assert archive.screenshot_path and archive.screenshot_file and archive.screenshot_path.is_file(), \ 'Did not store the screenshot' def test_archive_history(test_session, archive_factory): """Archive's can have a history of other archives. Archive's with an empty URL are not associated.""" archive1 = archive_factory(url='https://example.com/1') archive2 = archive_factory(url='https://example.com/1') archive3 = archive_factory(url='https://example.com/2') archive4 = archive_factory() archive5 = archive_factory() assert archive1 in archive2.history, 'archive1 is a history for archive2' assert archive2 in archive1.history, 'archive2 is a history for archive2' assert archive3 not in archive1.history and archive3 not in archive2.history, 'archive3 is unique' assert not archive3.history, 'archive3 has no history' assert not archive4.history, 'archive4 has no history' assert not archive5.history, 'archive5 has no history' test_session.commit() assert archive1 in archive2.history, 'archive1 is a history for archive2' assert archive2 in archive1.history, 'archive2 is a history for archive2' assert archive3 not in archive1.history and archive3 not in archive2.history, 'archive3 is unique' assert not archive3.history, 'archive3 has no history' assert not archive4.history, 'archive4 has no history' assert not archive5.history, 'archive5 has no history'