from unittest import mock import pytest from aiodynamo.errors import ItemNotFound from dirty_equals import IsList, IsStr from url_shortener.config import Settings from url_shortener.exceptions import ( NotAllowedDomainError, PathAlreadyInUseError, PathAndPathPrefixProvidedError, ) from url_shortener.services import ShortenUrlService class TestShortenUrlService: async def test_shorten_url_service_random_path( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.get_item.side_effect = ItemNotFound result = await service.get_short_url( "https://example.com", settings.allowed_domains[0], path_length=6 ) dynamodb_client_mock.get_item.assert_called_once() dynamodb_client_mock.put_item.assert_called_with( settings.dynamodb_tablename, { "path": IsStr(regex=r"^[a-zA-Z0-9_-]{6}$"), "url": "https://example.com", "is_personalized": True, "created_at": result.created_at.timestamp(), "additional_attributes": None, }, ) assert result.url == "https://example.com" assert result.short_url == IsStr() assert result.is_personalized is True async def test_shorten_url_service_random_path_used( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.get_item.side_effect = [ {"path": "test", "url": "test"}, ItemNotFound, ] result = await service.get_short_url( "https://example.com", settings.allowed_domains[0], path_length=6 ) assert dynamodb_client_mock.get_item.call_count == 2 dynamodb_client_mock.put_item.assert_called_with( settings.dynamodb_tablename, { "path": IsStr(regex=r"^[a-zA-Z0-9_-]{6}$"), "url": "https://example.com", "is_personalized": True, "created_at": result.created_at.timestamp(), "additional_attributes": None, }, ) assert result.url == "https://example.com" assert result.is_personalized is True async def test_shorten_url_service_fixed_path_success( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.get_item.side_effect = ItemNotFound result = await service.get_short_url( "https://example.com", settings.allowed_domains[0], path_length=6, path="test", ) dynamodb_client_mock.get_item.assert_called_once() dynamodb_client_mock.put_item.assert_called_with( settings.dynamodb_tablename, { "path": "test", "url": "https://example.com", "is_personalized": False, "created_at": result.created_at.timestamp(), "additional_attributes": None, }, ) assert result.url == "https://example.com" assert "test" in result.short_url assert result.is_personalized is False async def test_shorten_url_service_fixed_path_used( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.get_item.return_value = {"path": "test", "url": "test"} with pytest.raises(PathAlreadyInUseError): await service.get_short_url( "https://example.com", settings.allowed_domains[0], path_length=6, path="test", ) dynamodb_client_mock.get_item.assert_called_once() dynamodb_client_mock.put_item.assert_not_called() async def test_shorten_url_service_path_prefix_success( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.get_item.side_effect = ItemNotFound path = await service.get_short_url( "https://example.com", settings.allowed_domains[0], path_length=6, path_prefix="test", ) dynamodb_client_mock.get_item.assert_called_once() dynamodb_client_mock.put_item.assert_called_with( settings.dynamodb_tablename, { "path": IsStr(regex=r"^test-[a-zA-Z0-9_-]{6}$"), "url": "https://example.com", "is_personalized": True, "created_at": path.created_at.timestamp(), "additional_attributes": None, }, ) assert "test-" in path.short_url assert path.is_personalized is True async def test_shorten_url_service_path_prefix_and_path_both_provided( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.get_item.side_effect = ItemNotFound with pytest.raises(PathAndPathPrefixProvidedError): await service.get_short_url( "https://example.com", settings.allowed_domains[0], path_length=6, path="test", path_prefix="test", ) dynamodb_client_mock.get_item.assert_not_called() dynamodb_client_mock.put_item.assert_not_called() async def test_shorten_url_service_get_short_urls( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.batch_write.return_value = {} response = await service.get_short_urls( "https://example.com", settings.allowed_domains[0], path_length=6, urls_count=100, ) assert len(response.short_urls) == 100 async def test_shorten_url_service_get_short_urls_with_prefix( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.batch_write.return_value = {} response = await service.get_short_urls( "https://example.com", settings.allowed_domains[0], path_length=6, urls_count=100, path_prefix="test", ) assert len(response.short_urls) == 100 short_urls = response.short_urls assert all("test-" in short_url.short_url for short_url in short_urls) async def test_shorten_url_service_not_allowed_domain( self, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock ) -> None: with pytest.raises(NotAllowedDomainError): await service.get_short_url( "https://example.com", "not-allowed-domain.com", path_length=6, ) dynamodb_client_mock.get_item.assert_not_called() dynamodb_client_mock.put_item.assert_not_called() async def test_shorten_url_service_bulk_shorten_not_allowed_domain( self, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock ) -> None: with pytest.raises(NotAllowedDomainError): await service.get_short_urls( "https://example.com", "not-allowed-domain.com", path_length=6, ) dynamodb_client_mock.get_item.assert_not_called() dynamodb_client_mock.put_item.assert_not_called() async def test_shorten_url_with_additional_attributes( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.get_item.side_effect = ItemNotFound result = await service.get_short_url( "https://example.com", settings.allowed_domains[0], path_length=6, additional_attributes={"key": "value"}, ) dynamodb_client_mock.get_item.assert_called_once() dynamodb_client_mock.put_item.assert_called_once() assert result.url == "https://example.com" assert result.short_url == IsStr() assert result.additional_attributes == {"key": "value"} async def test_bullk_shorten_url_with_additional_attributes( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.get_item.side_effect = ItemNotFound dynamodb_client_mock.batch_write.return_value = {} result = await service.get_short_urls( "https://example.com", settings.allowed_domains[0], path_length=6, additional_attributes=[{"key": "value"}], ) dynamodb_client_mock.batch_get.assert_called_once() dynamodb_client_mock.batch_write.assert_called_once() assert result.url == "https://example.com" assert result.short_urls == IsList(length=1) assert result.short_urls[0].additional_attributes == {"key": "value"} async def test_shorten_url_service_path_custom_length( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.get_item.side_effect = ItemNotFound result = await service.get_short_url( "https://example.com", settings.allowed_domains[0], path_length=12 ) dynamodb_client_mock.get_item.assert_called_once() dynamodb_client_mock.put_item.assert_called_with( settings.dynamodb_tablename, { "path": IsStr(regex=r"^[a-zA-Z0-9_-]{12}$"), "url": "https://example.com", "is_personalized": True, "created_at": result.created_at.timestamp(), "additional_attributes": None, }, ) assert result.url == "https://example.com" assert result.short_url == IsStr(regex=r"^.+/[a-zA-Z0-9_-]{12}$") assert result.is_personalized is True async def test_shorten_url_service_get_short_urls_path_custom_length( self, settings: Settings, service: ShortenUrlService, dynamodb_client_mock: mock.AsyncMock, ) -> None: dynamodb_client_mock.batch_write.return_value = {} response = await service.get_short_urls( "https://example.com", settings.allowed_domains[0], path_length=12, urls_count=100, ) assert len(response.short_urls) == 100 assert response.short_urls[0].short_url == IsStr( regex=r"^.+/[a-zA-Z0-9_-]{12}$" )