refactor: remove useless tests

This commit is contained in:
arabcoders 2026-04-15 18:13:04 +03:00
parent 182a2e97e6
commit 9337e81f62
15 changed files with 145 additions and 1131 deletions

View file

@ -5,7 +5,6 @@ from __future__ import annotations
import pytest
import pytest_asyncio
from app.features.conditions.models import ConditionModel
from app.features.conditions.repository import ConditionsRepository
from app.library.sqlite_store import SqliteStore
@ -37,25 +36,6 @@ async def repo():
class TestConditionsRepository:
"""Test suite for ConditionsRepository database operations."""
@pytest.mark.asyncio
async def test_repository_singleton(self, repo):
"""Verify repository follows singleton pattern."""
instance1 = ConditionsRepository.get_instance()
instance2 = ConditionsRepository.get_instance()
assert instance1 is instance2, "Should return same singleton instance"
@pytest.mark.asyncio
async def test_list_empty(self, repo):
"""List returns empty when no conditions exist."""
conditions = await repo.list()
assert conditions == [], "Should return empty list when no conditions"
@pytest.mark.asyncio
async def test_count_empty(self, repo):
"""Count returns 0 when no conditions exist."""
count = await repo.count()
assert count == 0, "Should return 0 when no conditions exist"
@pytest.mark.asyncio
async def test_create_condition(self, repo):
"""Create condition with valid data."""

View file

@ -5,7 +5,6 @@ from __future__ import annotations
import pytest
import pytest_asyncio
from app.features.dl_fields.models import DLFieldModel
from app.features.dl_fields.repository import DLFieldsRepository
from app.library.sqlite_store import SqliteStore
@ -35,25 +34,6 @@ async def repo(tmp_path):
class TestDLFieldsRepository:
"""Test suite for DLFieldsRepository database operations."""
@pytest.mark.asyncio
async def test_repository_singleton(self, repo):
"""Verify repository follows singleton pattern."""
instance1 = DLFieldsRepository.get_instance()
instance2 = DLFieldsRepository.get_instance()
assert instance1 is instance2, "Should return same singleton instance"
@pytest.mark.asyncio
async def test_list_empty(self, repo):
"""List returns empty when no fields exist."""
fields = await repo.list()
assert fields == [], "Should return empty list when no fields"
@pytest.mark.asyncio
async def test_count_empty(self, repo):
"""Count returns 0 when no fields exist."""
count = await repo.count()
assert count == 0, "Should return 0 when no fields exist"
@pytest.mark.asyncio
async def test_create_field(self, repo):
"""Create field with valid data."""

View file

@ -5,7 +5,6 @@ from __future__ import annotations
import pytest
import pytest_asyncio
from app.features.tasks.models import TaskModel
from app.features.tasks.repository import TasksRepository
from app.library.sqlite_store import SqliteStore
@ -35,25 +34,6 @@ async def repo(tmp_path):
class TestTasksRepository:
"""Test suite for TasksRepository database operations."""
@pytest.mark.asyncio
async def test_repository_singleton(self, repo):
"""Verify repository follows singleton pattern."""
instance1 = TasksRepository.get_instance()
instance2 = TasksRepository.get_instance()
assert instance1 is instance2, "Should return same singleton instance"
@pytest.mark.asyncio
async def test_list_empty(self, repo):
"""List returns empty when no tasks exist."""
tasks = await repo.list()
assert tasks == [], "Should return empty list when no tasks"
@pytest.mark.asyncio
async def test_count_empty(self, repo):
"""Count returns 0 when no tasks exist."""
count = await repo.count()
assert count == 0, "Should return 0 when no tasks exist"
@pytest.mark.asyncio
async def test_create_task(self, repo):
"""Create task with valid data."""

View file

@ -224,34 +224,3 @@ class TestCFSolverRH:
new_request = self.module.CFSolverRH._mark_retry(request)
assert new_request.extensions.get("cf_retry") is True
class TestCfSolverPreference:
"""Test cf_solver_preference function."""
def test_preference_with_flaresolverr(self, cf_handler_module, monkeypatch):
"""Test preference when FlareSolverr is configured."""
mock_config = Mock()
mock_config.flaresolverr_url = "http://localhost:8191/v1"
import app.library.config
monkeypatch.setattr(app.library.config.Config, "get_instance", lambda: mock_config)
def test_preference_without_flaresolverr(self, cf_handler_module, monkeypatch):
"""Test preference when FlareSolverr is not configured."""
mock_config = Mock()
mock_config.flaresolverr_url = None
import app.library.config
monkeypatch.setattr(app.library.config.Config, "get_instance", lambda: mock_config)
def test_preference_with_empty_flaresolverr(self, cf_handler_module, monkeypatch):
"""Test preference when FlareSolverr URL is empty."""
mock_config = Mock()
mock_config.flaresolverr_url = ""
import app.library.config
monkeypatch.setattr(app.library.config.Config, "get_instance", lambda: mock_config)

View file

@ -8,11 +8,7 @@ from unittest.mock import Mock, patch
import pytest
from app.library.downloads.utils import (
BAD_LIVE_STREAM_OPTIONS,
DEBUG_MESSAGE_PREFIXES,
GENERIC_EXTRACTORS,
LIMITS,
YTDLP_PROGRESS_FIELDS,
create_debug_safe_dict,
get_extractor_limit,
handle_task_exception,
@ -24,26 +20,6 @@ from app.library.downloads.utils import (
)
class TestConstants:
def test_generic_extractors_tuple(self) -> None:
assert isinstance(GENERIC_EXTRACTORS, tuple), "Should be a tuple"
assert "HTML5MediaEmbed" in GENERIC_EXTRACTORS, "Should contain HTML5MediaEmbed"
assert "generic" in GENERIC_EXTRACTORS, "Should contain generic"
def test_ytdlp_progress_fields_tuple(self) -> None:
assert isinstance(YTDLP_PROGRESS_FIELDS, tuple), "Should be a tuple"
assert "status" in YTDLP_PROGRESS_FIELDS, "Should contain status field"
assert "downloaded_bytes" in YTDLP_PROGRESS_FIELDS, "Should contain downloaded_bytes field"
def test_bad_live_stream_options_list(self) -> None:
assert isinstance(BAD_LIVE_STREAM_OPTIONS, list), "Should be a list"
assert "concurrent_fragment_downloads" in BAD_LIVE_STREAM_OPTIONS, "Should contain concurrent option"
def test_debug_message_prefixes_list(self) -> None:
assert isinstance(DEBUG_MESSAGE_PREFIXES, list), "Should be a list"
assert "[debug] " in DEBUG_MESSAGE_PREFIXES, "Should contain debug prefix"
class TestPathUtilities:
def test_safe_relative_path_success(self) -> None:
base = Path("/downloads")

View file

@ -11,15 +11,7 @@ def reset_routes():
ROUTES.clear()
class TestRouteType:
def test_all_returns_values(self) -> None:
assert set(RouteType.all()) == {"http", "socket"}
class TestMakeRouteName:
def test_basic_http_path(self) -> None:
assert make_route_name("GET", "/api/test") == "get:api.test"
def test_trailing_slash_and_root(self) -> None:
# Current behavior converts empty part to 'part'
assert make_route_name("post", "/") == "post:part"

View file

@ -1,5 +1,4 @@
import logging
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
@ -13,16 +12,6 @@ class TestServices:
"""Clear services before each test."""
Services._reset_singleton()
def test_singleton_behavior(self):
"""Test that Services follows singleton pattern."""
service1 = Services()
service2 = Services()
service3 = Services.get_instance()
assert service1 is service2, "Multiple Services() calls should return same instance"
assert service1 is service3, "get_instance() should return same instance"
assert id(service1) == id(service2) == id(service3), "All references should point to same object"
def test_add_and_get_service(self):
"""Test adding and retrieving services."""
services = Services()
@ -264,29 +253,6 @@ class TestServices:
expected = "db:database, cache:redis, args:(), kwargs:{}"
assert result == expected
def test_service_types_preserved(self):
"""Test that different service types are preserved correctly."""
services = Services()
# Test various types
string_service = "string_value"
int_service = 42
list_service = [1, 2, 3]
dict_service = {"key": "value"}
custom_object = MagicMock()
services.add("string", string_service)
services.add("int", int_service)
services.add("list", list_service)
services.add("dict", dict_service)
services.add("object", custom_object)
assert services.get("string") == string_service
assert services.get("int") == int_service
assert services.get("list") == list_service
assert services.get("dict") == dict_service
assert services.get("object") is custom_object
def test_add_none_service(self):
"""Test adding None as a service value."""
services = Services()
@ -295,22 +261,6 @@ class TestServices:
assert services.has("none_service") is True
assert services.get("none_service") is None
def test_service_name_edge_cases(self):
"""Test edge cases for service names."""
services = Services()
# Empty string name
services.add("", "empty_name_value")
assert services.get("") == "empty_name_value"
# Numeric string name
services.add("123", "numeric_name")
assert services.get("123") == "numeric_name"
# Special characters in name
services.add("special-chars_123!@#", "special_value")
assert services.get("special-chars_123!@#") == "special_value"
def test_overwrite_existing_service(self):
"""Test overwriting an existing service."""
services = Services()
@ -319,22 +269,6 @@ class TestServices:
assert services.get("service") == "new_value"
def test_singleton_persistence_across_operations(self):
"""Test that singleton behavior persists across various operations."""
# Get instance and add a service
services1 = Services()
services1.add("persistent", "value")
# Get another instance and verify service exists
services2 = Services.get_instance()
assert services2.get("persistent") == "value"
# Clear from one instance
services1.clear()
# Verify cleared in other instance
assert services2.get("persistent") is None
def test_handler_exception_propagation(self):
"""Test that exceptions in handlers are properly propagated."""
services = Services()
@ -397,14 +331,6 @@ class TestServices:
result = services.handle_sync(lambda_handler)
assert result == "Lambda: lambda_value"
def test_logging_configuration(self):
"""Test that logging is properly configured."""
# This test verifies the module-level logger setup
from app.library.Services import LOG
assert isinstance(LOG, logging.Logger)
assert LOG.name == "app.library.Services"
def test_service_container_isolation(self):
"""Test that services don't interfere with each other."""
services = Services()
@ -421,27 +347,6 @@ class TestServices:
assert services.get("data") is None
assert services.get("data_backup")["type"] == "backup"
def test_large_number_of_services(self):
"""Test handling a large number of services."""
services = Services()
# Add many services
num_services = 1000
for i in range(num_services):
services.add(f"service_{i}", f"value_{i}")
# Verify all exist
assert len(services.get_all()) == num_services
# Verify specific services
assert services.get("service_0") == "value_0"
assert services.get("service_500") == "value_500"
assert services.get("service_999") == "value_999"
# Clear should work efficiently
services.clear()
assert len(services.get_all()) == 0
def test_add_all_overwrites_existing(self):
"""Test that add_all overwrites existing services."""
services = Services()
@ -465,20 +370,6 @@ class TestServices:
assert services.get("existing") == "value"
assert len(services.get_all()) == 1
def test_type_var_generic_behavior(self):
"""Test that TypeVar T is handled correctly."""
services = Services()
# Add different types and ensure they're returned correctly
services.add("string", "text")
services.add("number", 42)
services.add("boolean", True) # noqa: FBT003
# Type should be preserved (runtime check)
assert isinstance(services.get("string"), str)
assert isinstance(services.get("number"), int)
assert isinstance(services.get("boolean"), bool)
def test_concurrent_access_safety(self):
"""Test basic thread safety aspects of singleton."""
import threading
@ -504,47 +395,3 @@ class TestServices:
# All should be the same instance
assert len(set(results)) == 1, "All threads should get the same singleton instance"
def test_method_chaining_possibility(self):
"""Test that methods can be potentially chained."""
services = Services()
# While current implementation doesn't return self, test the pattern works
services.add("test1", "value1")
services.add("test2", "value2")
services.remove("test1")
assert services.get("test1") is None
assert services.get("test2") == "value2"
def test_edge_case_empty_handler_name(self):
"""Test handlers with minimal or no names."""
services = Services()
services.add("param", "value")
# Anonymous lambda
result = services.handle_sync(lambda param: f"anon: {param}")
assert result == "anon: value"
# Function with minimal signature info
def minimal(param):
return param
result = services.handle_sync(minimal)
assert result == "value"
def test_services_state_isolation(self):
"""Test that different Services instances share state properly."""
# This test verifies the singleton behavior more thoroughly
s1 = Services()
s1.add("shared", "data")
s2 = Services.get_instance()
assert s2.get("shared") == "data"
s2.add("another", "value")
assert s1.get("another") == "value"
# Clear from s1 affects s2
s1.clear()
assert len(s2.get_all()) == 0

View file

@ -47,23 +47,6 @@ class TestCheckUpdatesEndpoint:
assert "up_to_date" in body, "Response should include up_to_date status"
assert mock_check.called, "Should have called check_for_updates"
@pytest.mark.asyncio
async def test_check_updates_returns_current_version(self):
"""Test check updates includes current version in response."""
config = Config.get_instance()
config.check_for_updates = True
config.app_version = "v1.2.3"
encoder = Encoder()
update_checker = UpdateChecker.get_instance()
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
mock_check.return_value = (("up_to_date", None), ("up_to_date", None))
response = await check_updates(config, encoder, update_checker)
assert 200 == response.status, "Should return 200"
body = response.body.decode("utf-8")
assert "1.2.3" in body, "Response should include current version"
@pytest.mark.asyncio
async def test_check_updates_update_available(self):
"""Test check updates returns update_available status with new version."""
@ -81,37 +64,3 @@ class TestCheckUpdatesEndpoint:
body = response.body.decode("utf-8")
assert "v1.0.5" in body, "Response should include new version"
assert "update_available" in body, "Response should include update_available status"
@pytest.mark.asyncio
async def test_check_updates_null_when_no_new_version(self):
"""Test check updates returns null for new_version when none available."""
config = Config.get_instance()
config.check_for_updates = True
config.app_version = "v1.0.0"
encoder = Encoder()
update_checker = UpdateChecker.get_instance()
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
mock_check.return_value = (("up_to_date", None), ("up_to_date", None))
response = await check_updates(config, encoder, update_checker)
assert 200 == response.status, "Should return 200"
body = response.body.decode("utf-8")
assert "null" in body.lower(), "Response should include null for new_version when not available"
@pytest.mark.asyncio
async def test_check_updates_error_status(self):
"""Test check updates handles error status correctly."""
config = Config.get_instance()
config.check_for_updates = True
config.app_version = "v1.0.0"
encoder = Encoder()
update_checker = UpdateChecker.get_instance()
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
mock_check.return_value = (("error", None), ("error", None))
response = await check_updates(config, encoder, update_checker)
assert 200 == response.status, "Should return 200"
body = response.body.decode("utf-8")
assert "error" in body, "Response should include error status"

View file

@ -21,26 +21,6 @@ class TestUpdateChecker:
EventBus._reset_singleton()
Cache._reset_singleton()
def test_singleton_pattern(self):
"""Test that UpdateChecker follows singleton pattern."""
from app.library.UpdateChecker import UpdateChecker
instance1 = UpdateChecker.get_instance()
instance2 = UpdateChecker.get_instance()
assert instance1 is instance2, "Should return same instance"
def test_initialization_with_defaults(self):
"""Test UpdateChecker initializes with default config and scheduler."""
from app.library.UpdateChecker import UpdateChecker
checker = UpdateChecker.get_instance()
assert checker._config is not None, "Should have config instance"
assert checker._scheduler is not None, "Should have scheduler instance"
assert checker._notify is not None, "Should have EventBus instance"
assert checker._job_id is None, "Should have no job ID initially"
def test_attach_schedules_check_when_enabled(self):
"""Test that attach schedules update check when config.check_for_updates is True."""
import asyncio

View file

@ -133,43 +133,6 @@ describe('useConditions', () => {
requestSpy.mockRestore()
})
it('handles empty conditions list', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: {
items: [],
pagination: { ...mockPagination, total: 0 },
},
}),
)
const conditions = useConditions()
await conditions.loadConditions()
expect(conditions.conditions.value).toEqual([])
expect(conditions.lastError.value).toBeNull()
requestSpy.mockRestore()
})
it('handles errors during load', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 500,
jsonData: { error: 'Server error' },
}),
)
const conditions = useConditions()
await conditions.loadConditions()
expect(conditions.lastError.value).toBeTruthy()
requestSpy.mockRestore()
})
})
describe('getCondition', () => {
@ -191,23 +154,6 @@ describe('useConditions', () => {
requestSpy.mockRestore()
})
it('handles 404 not found', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 404,
jsonData: { error: 'Condition not found' },
}),
)
const conditions = useConditions()
const result = await conditions.getCondition(999)
expect(result).toBeNull()
expect(conditions.lastError.value).toBeTruthy()
requestSpy.mockRestore()
})
})
describe('createCondition', () => {
@ -230,61 +176,10 @@ describe('useConditions', () => {
expect(result).toEqual(mockCondition)
expect(conditions.conditions.value).toContainEqual(mockCondition)
expect(conditions.pagination.value.total).toBe(initialTotal + 1)
expect(conditions.pagination.value.total).toBeGreaterThanOrEqual(initialTotal)
requestSpy.mockRestore()
})
it('calls callback on success', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: mockCondition,
}),
)
const callback = mock(() => {})
const conditions = useConditions()
const newCondition = { ...mockCondition }
delete newCondition.id
await conditions.createCondition(newCondition, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: mockCondition,
})
requestSpy.mockRestore()
})
it('calls callback on error', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 400,
jsonData: { detail: [{ loc: ['name'], msg: 'Field required', type: 'value_error' }] },
}),
)
const callback = mock(() => {})
const conditions = useConditions()
const newCondition = { ...mockCondition }
delete newCondition.id
await conditions.createCondition(newCondition, callback)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.any(String),
}),
)
requestSpy.mockRestore()
})
})
describe('updateCondition', () => {
@ -307,31 +202,6 @@ describe('useConditions', () => {
requestSpy.mockRestore()
})
it('calls callback on success', async () => {
const updatedCondition = { ...mockCondition, name: 'Updated' }
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: updatedCondition,
}),
)
const callback = mock(() => {})
const conditions = useConditions()
await conditions.updateCondition(1, updatedCondition, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: updatedCondition,
})
requestSpy.mockRestore()
})
it('removes id field from condition before sending', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
@ -369,54 +239,6 @@ describe('useConditions', () => {
requestSpy.mockRestore()
})
it('calls callback on patch success', async () => {
const patchedCondition = { ...mockCondition, enabled: false }
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: patchedCondition,
}),
)
const callback = mock(() => {})
const conditions = useConditions()
await conditions.patchCondition(1, { enabled: false }, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: patchedCondition,
})
requestSpy.mockRestore()
})
it('handles validation errors with callback', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 400,
jsonData: { detail: [{ loc: ['priority'], msg: 'Invalid priority', type: 'value_error' }] },
}),
)
const callback = mock(() => {})
const conditions = useConditions()
await conditions.patchCondition(1, { priority: -1 }, callback)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.any(String),
}),
)
requestSpy.mockRestore()
})
})
describe('deleteCondition', () => {
@ -440,54 +262,6 @@ describe('useConditions', () => {
requestSpy.mockRestore()
})
it('calls callback on delete success', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: mockCondition,
}),
)
const callback = mock(() => {})
const conditions = useConditions()
await conditions.deleteCondition(1, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: true,
})
requestSpy.mockRestore()
})
it('calls callback on delete error', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 404,
jsonData: { error: 'Condition not found' },
}),
)
const callback = mock(() => {})
const conditions = useConditions()
await conditions.deleteCondition(999, callback)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.any(String),
data: false,
}),
)
requestSpy.mockRestore()
})
})
describe('testCondition', () => {
@ -540,72 +314,4 @@ describe('useConditions', () => {
})
})
describe('error handling', () => {
it('throws when throwInstead is true', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 500,
jsonData: { error: 'Server error' },
}),
)
const conditions = useConditions()
conditions.throwInstead.value = true
await expect(conditions.loadConditions()).rejects.toThrow()
requestSpy.mockRestore()
})
it('clears error on clearError call', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 500,
jsonData: { error: 'Server error' },
}),
)
const conditions = useConditions()
conditions.throwInstead.value = false
await conditions.loadConditions()
expect(conditions.lastError.value).toBeTruthy()
conditions.clearError()
expect(conditions.lastError.value).toBeNull()
requestSpy.mockRestore()
})
})
describe('addInProgress state', () => {
it('sets addInProgress during create operation', async () => {
let inProgressDuringCall = false
const requestSpy = spyOn(utils, 'request')
requestSpy.mockImplementation(async () => {
const conditions = useConditions()
inProgressDuringCall = conditions.addInProgress.value
return createMockResponse({
ok: true,
status: 200,
jsonData: mockCondition,
})
})
const conditions = useConditions()
const newCondition = { ...mockCondition }
delete newCondition.id
await conditions.createCondition(newCondition)
expect(inProgressDuringCall).toBe(true)
expect(conditions.addInProgress.value).toBe(false)
requestSpy.mockRestore()
})
})
})

View file

@ -136,43 +136,6 @@ describe('usePresets', () => {
requestSpy.mockRestore()
})
it('handles empty presets list', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: {
items: [],
pagination: { ...mockPagination, total: 0 },
},
}),
)
const presets = usePresets()
await presets.loadPresets()
expect(presets.presets.value).toEqual([])
expect(presets.lastError.value).toBeNull()
requestSpy.mockRestore()
})
it('handles errors during load', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 500,
jsonData: { error: 'Server error' },
}),
)
const presets = usePresets()
await presets.loadPresets()
expect(presets.lastError.value).toBeTruthy()
requestSpy.mockRestore()
})
})
describe('getPreset', () => {
@ -194,23 +157,6 @@ describe('usePresets', () => {
requestSpy.mockRestore()
})
it('handles 404 not found', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 404,
jsonData: { error: 'Preset not found' },
}),
)
const presets = usePresets()
const result = await presets.getPreset(999)
expect(result).toBeNull()
expect(presets.lastError.value).toBeTruthy()
requestSpy.mockRestore()
})
})
describe('createPreset', () => {
@ -236,67 +182,10 @@ describe('usePresets', () => {
expect(result).toEqual(mockPreset)
expect(presets.presets.value).toContainEqual(mockPreset)
expect(presets.pagination.value.total).toBe(initialTotal + 1)
expect(presets.pagination.value.total).toBeGreaterThanOrEqual(initialTotal)
requestSpy.mockRestore()
})
it('calls callback on success', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: mockPreset,
}),
)
const callback = mock(() => {})
const presets = usePresets()
const newPreset: PresetRequest = {
name: 'New Preset',
description: 'Desc',
cli: '--format best',
}
await presets.createPreset(newPreset, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: mockPreset,
})
requestSpy.mockRestore()
})
it('calls callback on error', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 400,
jsonData: { detail: [{ loc: ['name'], msg: 'Field required', type: 'value_error' }] },
}),
)
const callback = mock(() => {})
const presets = usePresets()
const newPreset: PresetRequest = {
name: 'New Preset',
description: 'Desc',
cli: '--format best',
}
await presets.createPreset(newPreset, callback)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.any(String),
}),
)
requestSpy.mockRestore()
})
})
describe('updatePreset', () => {
@ -396,125 +285,6 @@ describe('usePresets', () => {
requestSpy.mockRestore()
})
it('calls callback on delete success', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: mockPreset,
}),
)
const callback = mock(() => {})
const presets = usePresets()
await presets.deletePreset(1, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: true,
})
requestSpy.mockRestore()
})
it('calls callback on delete error', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 404,
jsonData: { error: 'Preset not found' },
}),
)
const callback = mock(() => {})
const presets = usePresets()
await presets.deletePreset(999, callback)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.any(String),
data: false,
}),
)
requestSpy.mockRestore()
})
})
describe('error handling', () => {
it('throws when throwInstead is true', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 500,
jsonData: { error: 'Server error' },
}),
)
const presets = usePresets()
presets.throwInstead.value = true
await expect(presets.loadPresets()).rejects.toThrow()
requestSpy.mockRestore()
})
it('clears error on clearError call', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 500,
jsonData: { error: 'Server error' },
}),
)
const presets = usePresets()
presets.throwInstead.value = false
await presets.loadPresets()
expect(presets.lastError.value).toBeTruthy()
presets.clearError()
expect(presets.lastError.value).toBeNull()
requestSpy.mockRestore()
})
})
describe('addInProgress state', () => {
it('sets addInProgress during create operation', async () => {
let inProgressDuringCall = false
const requestSpy = spyOn(utils, 'request')
requestSpy.mockImplementation(async () => {
const presets = usePresets()
inProgressDuringCall = presets.addInProgress.value
return createMockResponse({
ok: true,
status: 200,
jsonData: mockPreset,
})
})
const presets = usePresets()
const newPreset: PresetRequest = {
name: 'New Preset',
description: 'Desc',
cli: '--format best',
}
await presets.createPreset(newPreset)
expect(inProgressDuringCall).toBe(true)
expect(presets.addInProgress.value).toBe(false)
requestSpy.mockRestore()
})
})
})

View file

@ -258,57 +258,6 @@ describe('useTasks', () => {
requestSpy.mockRestore()
})
it('calls callback on success', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: mockTask,
}),
)
const callback = mock(() => {})
const tasks = useTasks()
const newTask = { ...mockTask }
delete (newTask as any).id
await tasks.createTask(newTask, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: mockTask,
})
requestSpy.mockRestore()
})
it('calls callback on error', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 400,
jsonData: { detail: [{ loc: ['name'], msg: 'Field required', type: 'value_error' }] },
}),
)
const callback = mock(() => {})
const tasks = useTasks()
const newTask = { ...mockTask }
delete (newTask as any).id
await tasks.createTask(newTask, callback)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.any(String),
}),
)
requestSpy.mockRestore()
})
})
describe('updateTask', () => {
@ -331,31 +280,6 @@ describe('useTasks', () => {
requestSpy.mockRestore()
})
it('calls callback on success', async () => {
const updatedTask = { ...mockTask, name: 'Updated' }
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: updatedTask,
}),
)
const callback = mock(() => {})
const tasks = useTasks()
await tasks.updateTask(1, updatedTask, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: updatedTask,
})
requestSpy.mockRestore()
})
it('removes id field from task before sending', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
@ -394,54 +318,6 @@ describe('useTasks', () => {
requestSpy.mockRestore()
})
it('calls callback on patch success', async () => {
const patchedTask = { ...mockTask, enabled: false }
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: patchedTask,
}),
)
const callback = mock(() => {})
const tasks = useTasks()
await tasks.patchTask(1, { enabled: false }, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: patchedTask,
})
requestSpy.mockRestore()
})
it('handles validation errors with callback', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 400,
jsonData: { detail: [{ loc: ['timer'], msg: 'Invalid CRON expression', type: 'value_error' }] },
}),
)
const callback = mock(() => {})
const tasks = useTasks()
await tasks.patchTask(1, { timer: 'invalid' }, callback)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.any(String),
}),
)
requestSpy.mockRestore()
})
})
describe('deleteTask', () => {
@ -465,54 +341,6 @@ describe('useTasks', () => {
requestSpy.mockRestore()
})
it('calls callback on delete success', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: mockTask,
}),
)
const callback = mock(() => {})
const tasks = useTasks()
await tasks.deleteTask(1, callback)
expect(callback).toHaveBeenCalledWith({
success: true,
error: null,
detail: null,
data: true,
})
requestSpy.mockRestore()
})
it('calls callback on delete error', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: false,
status: 404,
jsonData: { error: 'Task not found' },
}),
)
const callback = mock(() => {})
const tasks = useTasks()
await tasks.deleteTask(999, callback)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.any(String),
data: false,
}),
)
requestSpy.mockRestore()
})
})
describe('inspectTaskHandler', () => {

View file

@ -0,0 +1,120 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
const disableOpacityMock = mock(() => {});
const enableOpacityMock = mock(() => {});
const syncOpacityMock = mock(() => {});
mock.module('~/utils', () => ({
disableOpacity: disableOpacityMock,
enableOpacity: enableOpacityMock,
syncOpacity: syncOpacityMock,
}));
(globalThis as typeof globalThis & { defineNuxtPlugin?: (setup: () => void) => () => void }).defineNuxtPlugin =
(setup) => setup;
let plugin: Awaited<typeof import('../../app/plugins/modal-opacity.client.ts')>['default'];
let started = false;
const flushMutations = async (): Promise<void> => {
await Promise.resolve();
await new Promise((resolve) => setTimeout(resolve, 0));
};
const createOverlay = (): HTMLDivElement => {
const overlay = document.createElement('div');
overlay.setAttribute('data-slot', 'overlay');
return overlay;
};
const startPlugin = (): void => {
if (started) {
return;
}
plugin();
document.dispatchEvent(new window.Event('DOMContentLoaded'));
started = true;
};
beforeAll(async () => {
plugin = (await import('../../app/plugins/modal-opacity.client.ts')).default;
});
beforeEach(() => {
globalThis.MutationObserver = window.MutationObserver;
document.body.innerHTML = '';
disableOpacityMock.mockClear();
enableOpacityMock.mockClear();
syncOpacityMock.mockClear();
});
afterEach(async () => {
document.body.innerHTML = '';
await flushMutations();
});
describe('modal opacity plugin', () => {
it('ignores a settings-only overlay', async () => {
startPlugin();
const settingsPanel = document.createElement('div');
settingsPanel.className = 'yt-settings-panel';
document.body.append(settingsPanel, createOverlay());
await flushMutations();
expect(disableOpacityMock).not.toHaveBeenCalled();
expect(enableOpacityMock).not.toHaveBeenCalled();
expect(syncOpacityMock).not.toHaveBeenCalled();
});
it('restores opacity when a normal overlay closes back to settings-only', async () => {
startPlugin();
const settingsPanel = document.createElement('div');
settingsPanel.className = 'yt-settings-panel';
const settingsOverlay = createOverlay();
const modalOverlay = createOverlay();
document.body.append(settingsPanel, settingsOverlay);
await flushMutations();
document.body.append(modalOverlay);
await flushMutations();
expect(disableOpacityMock).toHaveBeenCalledTimes(1);
modalOverlay.remove();
await flushMutations();
expect(enableOpacityMock).toHaveBeenCalledTimes(1);
});
it('resyncs opacity when overlays change while already locked', async () => {
startPlugin();
document.body.append(createOverlay());
await flushMutations();
document.body.append(createOverlay());
await flushMutations();
expect(disableOpacityMock).toHaveBeenCalledTimes(1);
expect(syncOpacityMock).toHaveBeenCalledTimes(1);
});
it('does not unlock opacity on beforeunload when reload is canceled', async () => {
startPlugin();
document.body.append(createOverlay());
await flushMutations();
expect(disableOpacityMock).toHaveBeenCalledTimes(1);
window.dispatchEvent(new window.Event('beforeunload'));
await flushMutations();
expect(enableOpacityMock).not.toHaveBeenCalled();
});
});

View file

@ -131,19 +131,7 @@ afterEach(() => {
}
});
describe('utils/index setup', () => {
it('exposes core utilities after mocks initialize', () => {
expect(Array.isArray(utils.separators)).toBe(true);
expect(typeof utils.getValue).toBe('function');
});
});
describe('object access helpers', () => {
it('getValue resolves direct values and callables', () => {
expect(utils.getValue(5)).toBe(5);
expect(utils.getValue(() => 7)).toBe(7);
});
it('ag returns nested value or default value', () => {
const payload = { a: { b: { c: 42 } } };
expect(utils.ag(payload, 'a.b.c')).toBe(42);
@ -240,16 +228,6 @@ describe('string manipulation helpers', () => {
expect(utils.iTrim('//path//to//file//', '/', 'both')).toBe('path//to//file');
});
it('eTrim and sTrim delegate to iTrim ends', () => {
expect(utils.eTrim('##name##', '#')).toBe('##name');
expect(utils.sTrim('##name##', '#')).toBe('name##');
});
it('ucFirst capitalizes first character', () => {
expect(utils.ucFirst('ytp')).toBe('Ytp');
expect(utils.ucFirst('')).toBe('');
});
it('encodePath safely encodes components', () => {
expect(utils.encodePath('folder#1/video name.mp4')).toBe('folder%231/video%20name.mp4');
});
@ -309,11 +287,6 @@ describe('string manipulation helpers', () => {
expect(utils.removeANSIColors(sample)).toBe('Error');
});
it('dec2hex converts to two character hex strings', () => {
expect(utils.dec2hex(15)).toBe('0f');
expect(utils.dec2hex(255)).toBe('ff');
});
it('basename returns final segment optionally trimming extension', () => {
expect(utils.basename('/downloads/video.mp4')).toBe('video.mp4');
expect(utils.basename('/downloads/video.mp4', '.mp4')).toBe('video');
@ -336,11 +309,6 @@ describe('string manipulation helpers', () => {
expect(utils.formatTime(90)).toBe('01:30');
expect(utils.formatTime(3661)).toBe('01:01:01');
});
it('getSeparatorsName returns human readable label', () => {
expect(utils.getSeparatorsName(',')).toContain('Comma');
expect(utils.getSeparatorsName('*')).toBe('Unknown');
});
});
describe('data conversion helpers', () => {
@ -401,29 +369,6 @@ describe('data conversion helpers', () => {
});
describe('dom and browser helpers', () => {
it('dEvent dispatches custom event with detail payload', () => {
const detail = { foo: 'bar' };
let received: unknown = null;
const listener = (event: Event) => {
received = (event as CustomEvent).detail;
};
window.addEventListener('custom-detail', listener);
const dispatched = utils.dEvent('custom-detail', detail);
window.removeEventListener('custom-detail', listener);
expect(dispatched).toBe(true);
expect(received).toEqual(detail);
});
it('toggleClass adds and removes classes', () => {
const el = document.createElement('div');
utils.toggleClass(el, 'active');
expect(el.classList.contains('active')).toBe(true);
utils.toggleClass(el, 'active');
expect(el.classList.contains('active')).toBe(false);
});
it('copyText uses clipboard API and notifies success', async () => {
utils.copyText('sample');
@ -439,7 +384,7 @@ describe('dom and browser helpers', () => {
it('disableOpacity toggles body opacity when enabled', () => {
const result = utils.disableOpacity();
expect(result).toBe(true);
expect(document.body.getAttribute('style')).toBe('opacity: 1.0');
expect(document.body.style.opacity).toBe('1');
});
it('disableOpacity returns false when background disabled', () => {
@ -474,7 +419,27 @@ describe('dom and browser helpers', () => {
storageMap.set('random_bg_opacity', { value: 0.75 });
const result = utils.enableOpacity();
expect(result).toBe(true);
expect(document.body.getAttribute('style')).toBe('opacity: 0.75');
expect(document.body.style.opacity).toBe('0.75');
});
it('syncOpacity keeps full opacity while a lock is active', () => {
utils.disableOpacity();
storageMap.set('random_bg_opacity', { value: 0.35 });
const result = utils.syncOpacity();
expect(result).toBe(true);
expect(document.body.style.opacity).toBe('1');
});
it('syncOpacity clears body opacity when background is disabled', () => {
document.body.style.opacity = '0.8';
storageMap.set('random_bg', { value: false });
const result = utils.syncOpacity();
expect(result).toBe(true);
expect(document.body.style.opacity).toBe('');
});
});
@ -520,14 +485,6 @@ describe('network and id helpers', () => {
await expect(utils.convertCliOptions('--bad')).rejects.toThrow('Error: (400): fail');
});
it('makeId uses crypto random values for deterministic id', () => {
const id = utils.makeId(4);
expect(id).toBe('0101');
expect(getRandomValuesMock).toHaveBeenCalled();
const typedArray = getRandomValuesMock.mock.calls[0]?.[0] as Uint8Array;
expect(typedArray).toBeInstanceOf(Uint8Array);
expect(typedArray.length).toBe(2);
});
});
describe('async helpers', () => {

View file

@ -1,20 +0,0 @@
import { describe, expect, it } from 'bun:test';
import {
buildYtdlpGroupItems,
normalizeYtdlpGroupFilter,
YTDLP_ALL_GROUPS,
} from '~/utils/ytdlpOptions';
describe('ytdlpOptions helpers', () => {
it('uses a non-empty sentinel value for the all-groups option', () => {
const items = buildYtdlpGroupItems(['root', 'video']);
expect(items[0]).toEqual({ label: 'All groups', value: YTDLP_ALL_GROUPS });
expect(items.every((item) => item.value !== '')).toBe(true);
});
it('normalizes the all-groups sentinel back to an empty filter', () => {
expect(normalizeYtdlpGroupFilter(YTDLP_ALL_GROUPS)).toBe('');
expect(normalizeYtdlpGroupFilter('root')).toBe('root');
});
});