mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
479 lines
20 KiB
Python
479 lines
20 KiB
Python
"""
|
|
Tests for domain config import scenarios, specifically around directory detection
|
|
when archives are organized in year subdirectories.
|
|
|
|
These tests expose the bug where detect_domain_directory() returns the year directory
|
|
(e.g., archive/x.com/2026) instead of the domain directory (archive/x.com) when archives
|
|
are organized with year subdirectories.
|
|
|
|
These tests use the REAL import flow:
|
|
1. Create archive files on disk
|
|
2. Run refresh_files() to trigger file indexing and archive modeling
|
|
3. Archive modeling calls get_or_create_domain_collection() → detect_domain_directory()
|
|
4. Verify the detected directory is correct
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from modules.archive.lib import import_domains_config
|
|
from wrolpi import tags
|
|
from wrolpi.collections import Collection
|
|
|
|
|
|
# Valid singlefile content with the required header
|
|
def make_singlefile_content(url: str, title: str = 'Test Article') -> str:
|
|
return f'''<html><!--
|
|
Page saved with SingleFile
|
|
url: {url}
|
|
saved date: Thu May 12 2022 00:38:02 GMT+0000 (Coordinated Universal Time)
|
|
--><head><meta charset="utf-8">
|
|
<title>{title}</title>
|
|
</head>
|
|
<body>
|
|
{title} body contents
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
|
|
class TestNonTaggedDomainDirectoryDetection:
|
|
"""Tests that non-tagged domain directories are correctly detected."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_detect_domain_directory_non_tagged(
|
|
self, async_client, test_session, archive_directory):
|
|
"""
|
|
Non-tagged domain directories should be detected when they exist.
|
|
|
|
If archive/example.com exists, it should be returned by detect_domain_directory()
|
|
for a collection with no tag.
|
|
"""
|
|
from modules.archive.lib import detect_domain_directory
|
|
|
|
# Create a domain collection WITHOUT a tag
|
|
collection = Collection(name='example.com', kind='domain', directory=None)
|
|
test_session.add(collection)
|
|
test_session.flush()
|
|
|
|
# Verify no tag
|
|
assert collection.tag is None, 'Collection should not have a tag'
|
|
|
|
# Create the expected directory on filesystem
|
|
# For non-tagged domains, format_destination(None) should return archive/example.com
|
|
expected_dir = archive_directory / 'example.com'
|
|
expected_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# detect_domain_directory should find this directory
|
|
detected = detect_domain_directory(collection)
|
|
assert detected is not None, \
|
|
f'detect_domain_directory should find directory for non-tagged domain. Expected: archive/example.com'
|
|
assert 'example.com' in str(detected), f'Detected directory should contain domain name, got {detected}'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_detect_domain_directory_non_tagged_directory_missing(
|
|
self, async_client, test_session, archive_directory):
|
|
"""
|
|
When directory doesn't exist, detect_domain_directory should return None.
|
|
"""
|
|
from modules.archive.lib import detect_domain_directory
|
|
|
|
# Create a domain collection WITHOUT a tag
|
|
collection = Collection(name='missing.com', kind='domain', directory=None)
|
|
test_session.add(collection)
|
|
test_session.flush()
|
|
|
|
# DO NOT create the directory - it should not exist
|
|
|
|
# detect_domain_directory should return None
|
|
detected = detect_domain_directory(collection)
|
|
assert detected is None, \
|
|
'detect_domain_directory should return None when directory does not exist'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_archive_modeling_creates_non_tagged_collection_with_directory(
|
|
self, async_client, test_session, archive_directory, make_files_structure, refresh_files):
|
|
"""
|
|
When archives are created for a non-tagged domain, the collection
|
|
should be created with the correct directory auto-detected.
|
|
|
|
This is the full integration test: files on disk -> modeling -> collection with directory.
|
|
"""
|
|
# Create archive files in non-tagged domain directory (no tag subdirectory)
|
|
domain_dir = archive_directory / 'simple.com'
|
|
domain_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
make_files_structure({
|
|
str(domain_dir / '2026-01-01-00-00-01_Simple Article.html'):
|
|
make_singlefile_content('https://simple.com/article', 'Simple Article'),
|
|
str(domain_dir / '2026-01-01-00-00-01_Simple Article.readability.json'):
|
|
'{"url": "https://simple.com/article", "title": "Simple Article"}',
|
|
})
|
|
|
|
await refresh_files()
|
|
|
|
# Verify domain collection was created WITHOUT a tag
|
|
collection = test_session.query(Collection).filter_by(name='simple.com', kind='domain').one_or_none()
|
|
assert collection is not None, 'Domain collection should have been created'
|
|
assert collection.tag is None, 'Collection should NOT have a tag'
|
|
|
|
# Directory should have been auto-detected
|
|
assert collection.directory is not None, 'Directory should have been auto-detected'
|
|
dir_str = str(collection.directory)
|
|
assert 'archive/simple.com' in dir_str, f'Directory should be archive/simple.com, got {dir_str}'
|
|
|
|
|
|
class TestArchiveModelingWithYearSubdirs:
|
|
"""Tests using real archive modeling flow with year subdirectories."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_archive_modeling_year_subdirectory_detection(
|
|
self, async_client, test_session, archive_directory, make_files_structure, refresh_files):
|
|
"""
|
|
Bug test: When archives are in year subdirectories, the domain collection
|
|
should have directory archive/x.com, not archive/x.com/2026.
|
|
|
|
Uses real refresh flow to trigger archive modeling.
|
|
"""
|
|
# Create archive files in year subdirectory
|
|
year_dir = archive_directory / 'x.com' / '2026'
|
|
year_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
make_files_structure({
|
|
str(year_dir / '2026-01-01-00-00-01_Article 1.html'):
|
|
make_singlefile_content('https://x.com/article1', 'Article 1'),
|
|
str(year_dir / '2026-01-01-00-00-01_Article 1.readability.json'):
|
|
'{"url": "https://x.com/article1", "title": "Article 1"}',
|
|
str(year_dir / '2026-01-01-00-00-02_Article 2.html'):
|
|
make_singlefile_content('https://x.com/article2', 'Article 2'),
|
|
str(year_dir / '2026-01-01-00-00-02_Article 2.readability.json'):
|
|
'{"url": "https://x.com/article2", "title": "Article 2"}',
|
|
})
|
|
|
|
# Run file refresh - this triggers archive modeling which calls
|
|
# get_or_create_domain_collection() → detect_domain_directory()
|
|
await refresh_files()
|
|
|
|
# Verify domain collection was created
|
|
collection = test_session.query(Collection).filter_by(name='x.com', kind='domain').one_or_none()
|
|
assert collection is not None, 'Domain collection should have been created'
|
|
|
|
# BUG: Currently detects archive/x.com/2026 instead of archive/x.com
|
|
# The directory should be the domain directory, not the year subdirectory
|
|
assert collection.directory is not None, 'Directory should have been auto-detected'
|
|
dir_str = str(collection.directory)
|
|
assert 'archive/x.com' in dir_str, f'Directory should contain archive/x.com, got {dir_str}'
|
|
assert '2026' not in dir_str, f'Directory should NOT contain year subdirectory, got {dir_str}'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_archive_modeling_multiple_year_subdirectories(
|
|
self, async_client, test_session, archive_directory, make_files_structure, refresh_files):
|
|
"""
|
|
When archives span multiple years, the domain collection directory
|
|
should be the common parent (archive/example.org), not either year.
|
|
"""
|
|
# Create archives in different year subdirectories
|
|
year_2025 = archive_directory / 'example.org' / '2025'
|
|
year_2026 = archive_directory / 'example.org' / '2026'
|
|
year_2025.mkdir(parents=True, exist_ok=True)
|
|
year_2026.mkdir(parents=True, exist_ok=True)
|
|
|
|
make_files_structure({
|
|
str(year_2025 / '2025-06-01-00-00-01_Old Article.html'):
|
|
make_singlefile_content('https://example.org/old', 'Old Article'),
|
|
str(year_2025 / '2025-06-01-00-00-01_Old Article.readability.json'):
|
|
'{"url": "https://example.org/old", "title": "Old Article"}',
|
|
str(year_2026 / '2026-01-01-00-00-01_New Article.html'):
|
|
make_singlefile_content('https://example.org/new', 'New Article'),
|
|
str(year_2026 / '2026-01-01-00-00-01_New Article.readability.json'):
|
|
'{"url": "https://example.org/new", "title": "New Article"}',
|
|
})
|
|
|
|
await refresh_files()
|
|
|
|
collection = test_session.query(Collection).filter_by(name='example.org', kind='domain').one_or_none()
|
|
assert collection is not None
|
|
assert collection.directory is not None
|
|
|
|
dir_str = str(collection.directory)
|
|
assert 'archive/example.org' in dir_str
|
|
# Should not contain either year
|
|
assert '2025' not in dir_str, f'Directory should not contain 2025, got {dir_str}'
|
|
assert '2026' not in dir_str, f'Directory should not contain 2026, got {dir_str}'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_archive_modeling_tagged_domain_with_year_subdirs(
|
|
self, async_client, test_session, test_directory, archive_directory,
|
|
make_files_structure, test_tags_config, refresh_files):
|
|
"""
|
|
For tagged domains, the directory should be archive/tag/domain, not archive/tag/domain/year.
|
|
|
|
Tags are associated via config import, not auto-detected from paths.
|
|
"""
|
|
from wrolpi import tags
|
|
from modules.archive.lib import import_domains_config
|
|
|
|
# Create config directory
|
|
config_dir = test_directory / 'config'
|
|
config_dir.mkdir(exist_ok=True)
|
|
|
|
# Create tags config with the 'news' tag
|
|
test_tags_config.write_text('''
|
|
version: 0
|
|
tags:
|
|
news:
|
|
color: '#FF0000'
|
|
''')
|
|
|
|
# Create domains config with tag association
|
|
domains_config_path = config_dir / 'domains.yaml'
|
|
domains_config_path.write_text('''
|
|
version: 0
|
|
collections:
|
|
- name: x.com
|
|
kind: domain
|
|
tag_name: news
|
|
''')
|
|
|
|
# Import configs to set up tag association
|
|
tags.import_tags_config()
|
|
import_domains_config()
|
|
|
|
# Create archives in tagged domain with year subdirectory
|
|
tagged_year_dir = archive_directory / 'news' / 'x.com' / '2026'
|
|
tagged_year_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
make_files_structure({
|
|
str(tagged_year_dir / '2026-01-01-00-00-01_Tagged Article.html'):
|
|
make_singlefile_content('https://x.com/tagged', 'Tagged Article'),
|
|
str(tagged_year_dir / '2026-01-01-00-00-01_Tagged Article.readability.json'):
|
|
'{"url": "https://x.com/tagged", "title": "Tagged Article"}',
|
|
})
|
|
|
|
await refresh_files()
|
|
|
|
# The collection should exist with tag association from config
|
|
collection = test_session.query(Collection).filter_by(name='x.com', kind='domain').one_or_none()
|
|
assert collection is not None
|
|
assert collection.tag is not None
|
|
assert collection.tag.name == 'news'
|
|
|
|
# Directory should be archive/news/x.com, not archive/news/x.com/2026
|
|
assert collection.directory is not None
|
|
dir_str = str(collection.directory)
|
|
assert '2026' not in dir_str, f'Directory should not contain year, got {dir_str}'
|
|
|
|
|
|
class TestConfigImportThenModeling:
|
|
"""Tests for config import followed by archive modeling."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_config_import_then_archive_modeling(
|
|
self, async_client, test_session, test_directory, archive_directory,
|
|
make_files_structure, test_tags_config, refresh_files):
|
|
"""
|
|
Test full flow:
|
|
1. Import domains config (creates unrestricted collection)
|
|
2. Create archive files on disk
|
|
3. Run refresh (triggers modeling and directory detection)
|
|
4. Verify directory is correctly detected
|
|
"""
|
|
# Create tags config
|
|
config_dir = test_directory / 'config'
|
|
config_dir.mkdir(exist_ok=True)
|
|
test_tags_config.write_text('''
|
|
version: 0
|
|
tags:
|
|
news:
|
|
color: '#FF0000'
|
|
''')
|
|
|
|
# Create domains config - collection without directory initially
|
|
domains_config_path = config_dir / 'domains.yaml'
|
|
domains_config_path.write_text('''
|
|
version: 0
|
|
collections:
|
|
- name: test.com
|
|
kind: domain
|
|
tag_name: news
|
|
''')
|
|
|
|
# Import configs
|
|
tags.import_tags_config()
|
|
import_domains_config()
|
|
|
|
# Verify collection was created WITHOUT directory
|
|
collection = test_session.query(Collection).filter_by(name='test.com', kind='domain').one()
|
|
assert collection.tag is not None
|
|
assert collection.tag.name == 'news'
|
|
# Collection should be unrestricted (no directory yet)
|
|
initial_directory = collection.directory
|
|
|
|
# Now create archive files in year subdirectory
|
|
year_dir = archive_directory / 'news' / 'test.com' / '2026'
|
|
year_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
make_files_structure({
|
|
str(year_dir / '2026-01-01-00-00-01_Test Article.html'):
|
|
make_singlefile_content('https://test.com/article', 'Test Article'),
|
|
str(year_dir / '2026-01-01-00-00-01_Test Article.readability.json'):
|
|
'{"url": "https://test.com/article", "title": "Test Article"}',
|
|
})
|
|
|
|
# Run refresh to trigger archive modeling
|
|
await refresh_files()
|
|
|
|
# Refresh the collection from DB
|
|
test_session.expire(collection)
|
|
|
|
# Directory should now be detected - but NOT include the year
|
|
if collection.directory is not None:
|
|
dir_str = str(collection.directory)
|
|
assert '2026' not in dir_str, f'Directory should not contain year, got {dir_str}'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_startup_import_sequence(
|
|
self, async_client, test_session, test_directory, archive_directory, test_tags_config):
|
|
"""
|
|
Replicate exact main.py import order to verify tag associations exist
|
|
before domain directory detection runs.
|
|
"""
|
|
from wrolpi.tags import Tag
|
|
|
|
# Create tag
|
|
news_tag = Tag(name='news')
|
|
test_session.add(news_tag)
|
|
test_session.commit()
|
|
|
|
# Create tags config
|
|
config_dir = test_directory / 'config'
|
|
config_dir.mkdir(exist_ok=True)
|
|
test_tags_config.write_text('''
|
|
version: 0
|
|
tags:
|
|
news:
|
|
color: '#0000FF'
|
|
''')
|
|
|
|
# Create domains config
|
|
domains_config_path = config_dir / 'domains.yaml'
|
|
domains_config_path.write_text('''
|
|
version: 0
|
|
collections:
|
|
- name: startup-test.com
|
|
kind: domain
|
|
tag_name: news
|
|
''')
|
|
|
|
# Import in exact main.py order
|
|
tags.import_tags_config()
|
|
import_domains_config()
|
|
|
|
# Verify tag association was created
|
|
collection = test_session.query(Collection).filter_by(name='startup-test.com', kind='domain').one()
|
|
assert collection.tag is not None
|
|
assert collection.tag.name == 'news'
|
|
|
|
|
|
class TestArchiveFileFormatInteraction:
|
|
"""Tests for interaction between archive_file_format config and directory detection."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_archive_file_format_with_year_subdirectory(
|
|
self, async_client, test_session, test_directory, archive_directory,
|
|
make_files_structure, refresh_files, fake_now):
|
|
"""
|
|
Test that directory detection works correctly when archive_file_format
|
|
includes year subdirectories.
|
|
|
|
This tests the interaction between:
|
|
1. archive_file_format config (e.g., %(download_year)s/%(download_datetime)s_%(title)s.%(ext)s)
|
|
2. detect_domain_directory() which should return archive/example.com, NOT archive/example.com/2026
|
|
|
|
Uses get_new_archive_files() to create files exactly as the real download flow does.
|
|
"""
|
|
from datetime import datetime
|
|
from modules.archive.lib import get_new_archive_files, get_archive_downloader_config
|
|
|
|
# Set the fake time so we know what year subdirectory will be created
|
|
fake_now(datetime(2026, 6, 15))
|
|
|
|
# Configure archive_file_format to use year subdirectories
|
|
config = get_archive_downloader_config()
|
|
original_format = config._config['file_name_format']
|
|
config._config['file_name_format'] = '%(download_year)s/%(download_datetime)s_%(title)s.%(ext)s'
|
|
|
|
try:
|
|
# Use get_new_archive_files to create file paths exactly as real download does
|
|
archive_files = get_new_archive_files('https://fileformat-test.com/article', 'Test Article')
|
|
|
|
# Verify the files are in a year subdirectory
|
|
assert '2026' in str(archive_files.singlefile), f'File should be in year subdir: {archive_files.singlefile}'
|
|
|
|
# Create the directory structure and write the files
|
|
archive_files.singlefile.parent.mkdir(parents=True, exist_ok=True)
|
|
archive_files.singlefile.write_text(
|
|
make_singlefile_content('https://fileformat-test.com/article', 'Test Article'))
|
|
archive_files.readability_json.write_text(
|
|
'{"url": "https://fileformat-test.com/article", "title": "Test Article"}')
|
|
|
|
# Run refresh to trigger archive modeling
|
|
await refresh_files()
|
|
|
|
# Verify domain collection was created
|
|
collection = test_session.query(Collection).filter_by(
|
|
name='fileformat-test.com', kind='domain').one_or_none()
|
|
assert collection is not None, 'Domain collection should have been created'
|
|
|
|
# BUG: Currently detects archive/fileformat-test.com/2026 instead of archive/fileformat-test.com
|
|
assert collection.directory is not None, 'Directory should have been auto-detected'
|
|
dir_str = str(collection.directory)
|
|
assert 'archive/fileformat-test.com' in dir_str, f'Directory should contain domain, got {dir_str}'
|
|
assert '2026' not in dir_str, f'Directory should NOT contain year from file format, got {dir_str}'
|
|
|
|
finally:
|
|
# Restore original format
|
|
config._config['file_name_format'] = original_format
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_archive_file_format_multiple_downloads_same_year(
|
|
self, async_client, test_session, test_directory, archive_directory,
|
|
make_files_structure, refresh_files, fake_now):
|
|
"""
|
|
Test directory detection when multiple archives are downloaded in the same year
|
|
using archive_file_format with year subdirectories.
|
|
"""
|
|
from datetime import datetime
|
|
from modules.archive.lib import get_new_archive_files, get_archive_downloader_config
|
|
|
|
fake_now(datetime(2026, 3, 10))
|
|
|
|
config = get_archive_downloader_config()
|
|
original_format = config._config['file_name_format']
|
|
config._config['file_name_format'] = '%(download_year)s/%(download_datetime)s_%(title)s.%(ext)s'
|
|
|
|
try:
|
|
# Create multiple archives in the same year
|
|
for i, title in enumerate(['Article One', 'Article Two', 'Article Three'], 1):
|
|
fake_now(datetime(2026, 3, 10, 0, 0, i)) # Different times, same year
|
|
archive_files = get_new_archive_files(f'https://multi-test.com/article{i}', title)
|
|
archive_files.singlefile.parent.mkdir(parents=True, exist_ok=True)
|
|
archive_files.singlefile.write_text(
|
|
make_singlefile_content(f'https://multi-test.com/article{i}', title))
|
|
archive_files.readability_json.write_text(
|
|
f'{{"url": "https://multi-test.com/article{i}", "title": "{title}"}}')
|
|
|
|
await refresh_files()
|
|
|
|
collection = test_session.query(Collection).filter_by(
|
|
name='multi-test.com', kind='domain').one_or_none()
|
|
assert collection is not None
|
|
|
|
# BUG: All archives in 2026/, so common ancestor is archive/multi-test.com/2026
|
|
# Should be archive/multi-test.com
|
|
assert collection.directory is not None
|
|
dir_str = str(collection.directory)
|
|
assert 'archive/multi-test.com' in dir_str
|
|
assert '2026' not in dir_str, f'Directory should NOT contain year, got {dir_str}'
|
|
|
|
finally:
|
|
config._config['file_name_format'] = original_format
|