7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
574 lines
19 KiB
Python
574 lines
19 KiB
Python
"""
|
|
Deep behavioral tests for all news exception classes.
|
|
Tests status codes, error codes, to_dict, details, and inheritance.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.news.exceptions import (
|
|
DatabaseAccessException,
|
|
InvalidLimitException,
|
|
InvalidParameterException,
|
|
NewsAPIException,
|
|
NewsFeatureDisabledException,
|
|
NewsFeedGenerationException,
|
|
NotImplementedException,
|
|
ResearchProcessingException,
|
|
SchedulerNotificationException,
|
|
SubscriptionCreationException,
|
|
SubscriptionDeletionException,
|
|
SubscriptionNotFoundException,
|
|
SubscriptionUpdateException,
|
|
)
|
|
|
|
|
|
# --- Base exception ---
|
|
|
|
|
|
class TestNewsAPIException:
|
|
"""Tests for base NewsAPIException."""
|
|
|
|
def test_message_stored(self):
|
|
e = NewsAPIException("something broke")
|
|
assert e.message == "something broke"
|
|
|
|
def test_default_status_code_500(self):
|
|
e = NewsAPIException("err")
|
|
assert e.status_code == 500
|
|
|
|
def test_custom_status_code(self):
|
|
e = NewsAPIException("err", status_code=400)
|
|
assert e.status_code == 400
|
|
|
|
def test_error_code_defaults_to_class_name(self):
|
|
e = NewsAPIException("err")
|
|
assert e.error_code == "NewsAPIException"
|
|
|
|
def test_custom_error_code(self):
|
|
e = NewsAPIException("err", error_code="CUSTOM")
|
|
assert e.error_code == "CUSTOM"
|
|
|
|
def test_details_defaults_empty(self):
|
|
e = NewsAPIException("err")
|
|
assert e.details == {}
|
|
|
|
def test_custom_details(self):
|
|
e = NewsAPIException("err", details={"key": "val"})
|
|
assert e.details == {"key": "val"}
|
|
|
|
def test_to_dict_structure(self):
|
|
e = NewsAPIException("broken", status_code=400, error_code="BAD")
|
|
d = e.to_dict()
|
|
assert d["error"] == "broken"
|
|
assert d["error_code"] == "BAD"
|
|
assert d["status_code"] == 400
|
|
|
|
def test_to_dict_excludes_empty_details(self):
|
|
e = NewsAPIException("err")
|
|
d = e.to_dict()
|
|
assert "details" not in d
|
|
|
|
def test_to_dict_includes_details_when_present(self):
|
|
e = NewsAPIException("err", details={"info": "extra"})
|
|
d = e.to_dict()
|
|
assert d["details"] == {"info": "extra"}
|
|
|
|
def test_is_exception(self):
|
|
e = NewsAPIException("err")
|
|
assert isinstance(e, Exception)
|
|
|
|
def test_str_representation(self):
|
|
e = NewsAPIException("message here")
|
|
assert str(e) == "message here"
|
|
|
|
|
|
class TestToDictMessageSanitization:
|
|
"""`to_dict()` runs `message` through the central client sanitizer.
|
|
|
|
This is a boundary backstop: the primary defence is curated messages at
|
|
the `news/api.py` raise sites. These tests lock in that the backstop
|
|
scrubs credential-bearing text while leaving legitimate messages — the
|
|
class of content a denylist would have false-positived on — untouched.
|
|
"""
|
|
|
|
def test_benign_message_passes_through_unchanged(self):
|
|
# An egress-policy rejection reason is legitimate, user-facing text.
|
|
# It must reach the client verbatim (the central sanitizer only
|
|
# redacts credential patterns, so it does not fire here).
|
|
msg = (
|
|
"Failed to create subscription: search engine 'brave' is not "
|
|
"permitted under the current egress policy"
|
|
)
|
|
assert NewsAPIException(msg).to_dict()["error"] == msg
|
|
|
|
def test_credential_keyword_in_content_not_scrubbed(self):
|
|
# Plain prose containing "password" is not a credential and must
|
|
# survive — the denylist approach wrongly nuked this.
|
|
msg = "how do I reset my password?"
|
|
assert NewsAPIException(msg).to_dict()["error"] == msg
|
|
|
|
def test_bearer_token_redacted(self):
|
|
e = NewsAPIException(
|
|
"auth failed: Authorization: Bearer sk-abc123DEF456ghi789"
|
|
)
|
|
error = e.to_dict()["error"]
|
|
# The load-bearing assertion is that the secret is gone. The redaction
|
|
# sentinel varies (`[REDACTED]` / `[REDACTED_KEY]`), so match on the
|
|
# order-independent substring rather than an exact bracket form.
|
|
assert "sk-abc123DEF456ghi789" not in error
|
|
assert "REDACTED" in error
|
|
|
|
def test_api_key_query_param_redacted(self):
|
|
e = NewsAPIException(
|
|
"call failed: ?api_key=AIzaSyD-EXAMPLE_KEY_1234567890abcd"
|
|
)
|
|
error = e.to_dict()["error"]
|
|
assert "AIzaSyD-EXAMPLE_KEY_1234567890abcd" not in error
|
|
assert "REDACTED" in error
|
|
|
|
def test_long_message_truncated_to_client_cap(self):
|
|
# sanitize_error_for_client caps the client-facing message at 200 chars.
|
|
# Locks in that to_dict uses the truncating client sanitizer (not the
|
|
# no-cap variant) so an over-long future message can't ship in full.
|
|
msg = "A" * 300
|
|
error = NewsAPIException(msg).to_dict()["error"]
|
|
assert len(error) <= 200
|
|
assert error.endswith("...")
|
|
|
|
def test_stored_message_attribute_is_not_mutated(self):
|
|
# Only the serialized output is scrubbed; the raw message stays intact
|
|
# for server-side diagnosis via logger.exception.
|
|
raw = "boom: Authorization: Bearer sk-secret999value000"
|
|
e = NewsAPIException(raw)
|
|
assert e.to_dict()["error"] != raw
|
|
assert e.message == raw
|
|
|
|
def test_error_code_and_status_code_untouched(self):
|
|
e = NewsAPIException(
|
|
"Bearer sk-abc123DEF456ghi789",
|
|
status_code=502,
|
|
error_code="UPSTREAM",
|
|
)
|
|
d = e.to_dict()
|
|
assert d["error_code"] == "UPSTREAM"
|
|
assert d["status_code"] == 502
|
|
|
|
def test_benign_details_pass_through_unchanged(self):
|
|
# Structured values (IDs, ints) and benign free text in details are
|
|
# not credential shapes, so they survive verbatim.
|
|
e = NewsAPIException(
|
|
"err",
|
|
details={
|
|
"subscription_id": "abc-123",
|
|
"min_limit": 1,
|
|
"query": "how do I reset my password?",
|
|
},
|
|
)
|
|
assert e.to_dict()["details"] == {
|
|
"subscription_id": "abc-123",
|
|
"min_limit": 1,
|
|
"query": "how do I reset my password?",
|
|
}
|
|
|
|
def test_credential_in_details_string_leaf_redacted(self):
|
|
# details can carry caller-supplied free text (e.g. the user query at
|
|
# api.py:1071/1134); a credential shape in a string leaf is scrubbed.
|
|
e = NewsAPIException(
|
|
"err",
|
|
details={
|
|
"query": "Authorization: Bearer sk-abc123DEF456ghi789",
|
|
"count": 3,
|
|
},
|
|
)
|
|
d = e.to_dict()["details"]
|
|
assert "sk-abc123DEF456ghi789" not in d["query"]
|
|
assert "REDACTED" in d["query"]
|
|
# Non-string leaves pass through untouched.
|
|
assert d["count"] == 3
|
|
|
|
def test_nested_details_are_sanitized_recursively(self):
|
|
e = NewsAPIException(
|
|
"err",
|
|
details={
|
|
"outer": {"inner": ["?api_key=AIzaSyD-EXAMPLE_1234567890"]}
|
|
},
|
|
)
|
|
leaf = e.to_dict()["details"]["outer"]["inner"][0]
|
|
assert "AIzaSyD-EXAMPLE_1234567890" not in leaf
|
|
assert "REDACTED" in leaf
|
|
|
|
def test_stored_details_attribute_is_not_mutated(self):
|
|
raw = {"query": "Bearer sk-secret999value000"}
|
|
e = NewsAPIException("err", details=raw)
|
|
assert "REDACTED" in e.to_dict()["details"]["query"]
|
|
assert e.details["query"] == "Bearer sk-secret999value000"
|
|
|
|
def test_tuple_detail_normalizes_to_list(self):
|
|
# A tuple leaf serializes to a JSON array anyway; sanitize returns a
|
|
# plain list so identity is irrelevant downstream.
|
|
e = NewsAPIException("err", details={"pair": ("a", "b")})
|
|
assert e.to_dict()["details"]["pair"] == ["a", "b"]
|
|
|
|
def test_namedtuple_detail_does_not_crash(self):
|
|
# Regression guard: `type(value)(<generator>)` would raise on a
|
|
# namedtuple (constructor isn't `(iterable) -> instance`), which inside
|
|
# the error handler would degrade a structured error into a bare 500.
|
|
from collections import namedtuple
|
|
|
|
Pair = namedtuple("Pair", ["a", "b"])
|
|
e = NewsAPIException(
|
|
"err", details={"pair": Pair("Bearer sk-abc123DEF456ghi789", "x")}
|
|
)
|
|
d = e.to_dict()["details"]["pair"]
|
|
assert d == ["Bearer [REDACTED]", "x"]
|
|
|
|
def test_subclass_message_sanitized_via_inherited_to_dict(self):
|
|
# A subclass that interpolates a caller-supplied string still gets the
|
|
# backstop, because to_dict() is inherited.
|
|
e = SubscriptionCreationException(
|
|
"Authorization: Bearer sk-abc123DEF456ghi789"
|
|
)
|
|
error = e.to_dict()["error"]
|
|
assert "sk-abc123DEF456ghi789" not in error
|
|
assert error.startswith("Failed to create subscription:")
|
|
|
|
|
|
# --- Specific exceptions ---
|
|
|
|
|
|
class TestNewsFeatureDisabledException:
|
|
"""Tests for disabled feature exception."""
|
|
|
|
def test_default_message(self):
|
|
e = NewsFeatureDisabledException()
|
|
assert e.message == "News system is disabled"
|
|
|
|
def test_custom_message(self):
|
|
e = NewsFeatureDisabledException("Feature X disabled")
|
|
assert e.message == "Feature X disabled"
|
|
|
|
def test_status_code_503(self):
|
|
e = NewsFeatureDisabledException()
|
|
assert e.status_code == 503
|
|
|
|
def test_error_code(self):
|
|
e = NewsFeatureDisabledException()
|
|
assert e.error_code == "NEWS_DISABLED"
|
|
|
|
def test_inherits_base(self):
|
|
e = NewsFeatureDisabledException()
|
|
assert isinstance(e, NewsAPIException)
|
|
|
|
|
|
class TestInvalidLimitException:
|
|
"""Tests for invalid limit exception."""
|
|
|
|
def test_message_includes_limit(self):
|
|
e = InvalidLimitException(0)
|
|
assert "0" in e.message
|
|
|
|
def test_status_code_400(self):
|
|
e = InvalidLimitException(-1)
|
|
assert e.status_code == 400
|
|
|
|
def test_error_code(self):
|
|
e = InvalidLimitException(0)
|
|
assert e.error_code == "INVALID_LIMIT"
|
|
|
|
def test_details_include_provided_limit(self):
|
|
e = InvalidLimitException(-5)
|
|
assert e.details["provided_limit"] == -5
|
|
|
|
def test_details_include_min_limit(self):
|
|
e = InvalidLimitException(0)
|
|
assert e.details["min_limit"] == 1
|
|
|
|
def test_to_dict_has_details(self):
|
|
e = InvalidLimitException(0)
|
|
d = e.to_dict()
|
|
assert d["details"]["provided_limit"] == 0
|
|
|
|
def test_inherits_base(self):
|
|
assert issubclass(InvalidLimitException, NewsAPIException)
|
|
|
|
|
|
class TestSubscriptionNotFoundException:
|
|
"""Tests for subscription not found exception."""
|
|
|
|
def test_message_includes_id(self):
|
|
e = SubscriptionNotFoundException("sub-123")
|
|
assert "sub-123" in e.message
|
|
|
|
def test_status_code_404(self):
|
|
e = SubscriptionNotFoundException("sub-1")
|
|
assert e.status_code == 404
|
|
|
|
def test_error_code(self):
|
|
e = SubscriptionNotFoundException("sub-1")
|
|
assert e.error_code == "SUBSCRIPTION_NOT_FOUND"
|
|
|
|
def test_details_include_subscription_id(self):
|
|
e = SubscriptionNotFoundException("sub-abc")
|
|
assert e.details["subscription_id"] == "sub-abc"
|
|
|
|
|
|
class TestSubscriptionCreationException:
|
|
"""Tests for subscription creation exception."""
|
|
|
|
def test_message_prefixed(self):
|
|
e = SubscriptionCreationException("query too long")
|
|
assert "Failed to create subscription" in e.message
|
|
assert "query too long" in e.message
|
|
|
|
def test_status_code_500(self):
|
|
e = SubscriptionCreationException("err")
|
|
assert e.status_code == 500
|
|
|
|
def test_error_code(self):
|
|
e = SubscriptionCreationException("err")
|
|
assert e.error_code == "SUBSCRIPTION_CREATE_FAILED"
|
|
|
|
def test_custom_details(self):
|
|
e = SubscriptionCreationException("err", details={"query": "test"})
|
|
assert e.details == {"query": "test"}
|
|
|
|
def test_none_details_becomes_empty(self):
|
|
e = SubscriptionCreationException("err", details=None)
|
|
assert e.details == {}
|
|
|
|
|
|
class TestSubscriptionUpdateException:
|
|
"""Tests for subscription update exception."""
|
|
|
|
def test_message_includes_id_and_reason(self):
|
|
e = SubscriptionUpdateException("sub-1", "invalid interval")
|
|
assert "sub-1" in e.message
|
|
assert "invalid interval" in e.message
|
|
|
|
def test_status_code_500(self):
|
|
e = SubscriptionUpdateException("sub-1", "err")
|
|
assert e.status_code == 500
|
|
|
|
def test_error_code(self):
|
|
e = SubscriptionUpdateException("sub-1", "err")
|
|
assert e.error_code == "SUBSCRIPTION_UPDATE_FAILED"
|
|
|
|
def test_details_include_subscription_id(self):
|
|
e = SubscriptionUpdateException("sub-42", "err")
|
|
assert e.details["subscription_id"] == "sub-42"
|
|
|
|
|
|
class TestSubscriptionDeletionException:
|
|
"""Tests for subscription deletion exception."""
|
|
|
|
def test_message_includes_id(self):
|
|
e = SubscriptionDeletionException("sub-1", "db locked")
|
|
assert "sub-1" in e.message
|
|
|
|
def test_status_code_500(self):
|
|
e = SubscriptionDeletionException("sub-1", "err")
|
|
assert e.status_code == 500
|
|
|
|
def test_error_code(self):
|
|
e = SubscriptionDeletionException("sub-1", "err")
|
|
assert e.error_code == "SUBSCRIPTION_DELETE_FAILED"
|
|
|
|
def test_details_include_subscription_id(self):
|
|
e = SubscriptionDeletionException("sub-7", "err")
|
|
assert e.details["subscription_id"] == "sub-7"
|
|
|
|
|
|
class TestDatabaseAccessException:
|
|
"""Tests for database access exception."""
|
|
|
|
def test_message_includes_operation(self):
|
|
e = DatabaseAccessException("INSERT", "table locked")
|
|
assert "INSERT" in e.message
|
|
|
|
def test_status_code_500(self):
|
|
e = DatabaseAccessException("SELECT", "err")
|
|
assert e.status_code == 500
|
|
|
|
def test_error_code(self):
|
|
e = DatabaseAccessException("DELETE", "err")
|
|
assert e.error_code == "DATABASE_ERROR"
|
|
|
|
def test_details_include_operation(self):
|
|
e = DatabaseAccessException("UPDATE", "err")
|
|
assert e.details["operation"] == "UPDATE"
|
|
|
|
|
|
class TestNewsFeedGenerationException:
|
|
"""Tests for feed generation exception."""
|
|
|
|
def test_message_prefixed(self):
|
|
e = NewsFeedGenerationException("timeout")
|
|
assert "Failed to generate news feed" in e.message
|
|
|
|
def test_status_code_500(self):
|
|
e = NewsFeedGenerationException("err")
|
|
assert e.status_code == 500
|
|
|
|
def test_error_code(self):
|
|
e = NewsFeedGenerationException("err")
|
|
assert e.error_code == "FEED_GENERATION_FAILED"
|
|
|
|
def test_no_user_id_means_empty_details(self):
|
|
e = NewsFeedGenerationException("err")
|
|
assert e.details == {}
|
|
|
|
def test_user_id_in_details(self):
|
|
e = NewsFeedGenerationException("err", user_id="user-42")
|
|
assert e.details["user_id"] == "user-42"
|
|
|
|
|
|
class TestResearchProcessingException:
|
|
"""Tests for research processing exception."""
|
|
|
|
def test_message_prefixed(self):
|
|
e = ResearchProcessingException("parse failed")
|
|
assert "Failed to process research item" in e.message
|
|
|
|
def test_error_code(self):
|
|
e = ResearchProcessingException("err")
|
|
assert e.error_code == "RESEARCH_PROCESSING_FAILED"
|
|
|
|
def test_no_research_id_means_empty_details(self):
|
|
e = ResearchProcessingException("err")
|
|
assert e.details == {}
|
|
|
|
def test_research_id_in_details(self):
|
|
e = ResearchProcessingException("err", research_id="res-1")
|
|
assert e.details["research_id"] == "res-1"
|
|
|
|
|
|
class TestNotImplementedException:
|
|
"""Tests for not implemented exception."""
|
|
|
|
def test_message_includes_feature(self):
|
|
e = NotImplementedException("live streaming")
|
|
assert "live streaming" in e.message
|
|
|
|
def test_status_code_501(self):
|
|
e = NotImplementedException("x")
|
|
assert e.status_code == 501
|
|
|
|
def test_error_code(self):
|
|
e = NotImplementedException("x")
|
|
assert e.error_code == "NOT_IMPLEMENTED"
|
|
|
|
def test_details_include_feature(self):
|
|
e = NotImplementedException("video analysis")
|
|
assert e.details["feature"] == "video analysis"
|
|
|
|
|
|
class TestInvalidParameterException:
|
|
"""Tests for invalid parameter exception."""
|
|
|
|
def test_message_includes_parameter_name(self):
|
|
e = InvalidParameterException("limit", -1, "must be positive")
|
|
assert "limit" in e.message
|
|
|
|
def test_message_includes_description(self):
|
|
e = InvalidParameterException("limit", -1, "must be positive")
|
|
assert "must be positive" in e.message
|
|
|
|
def test_status_code_400(self):
|
|
e = InvalidParameterException("p", "v", "m")
|
|
assert e.status_code == 400
|
|
|
|
def test_error_code(self):
|
|
e = InvalidParameterException("p", "v", "m")
|
|
assert e.error_code == "INVALID_PARAMETER"
|
|
|
|
def test_details_include_parameter(self):
|
|
e = InvalidParameterException("limit", -1, "bad")
|
|
assert e.details["parameter"] == "limit"
|
|
|
|
def test_details_include_value(self):
|
|
e = InvalidParameterException("limit", -1, "bad")
|
|
assert e.details["value"] == -1
|
|
|
|
|
|
class TestSchedulerNotificationException:
|
|
"""Tests for scheduler notification exception."""
|
|
|
|
def test_message_includes_action(self):
|
|
e = SchedulerNotificationException("user_login", "timeout")
|
|
assert "user_login" in e.message
|
|
|
|
def test_status_code_500(self):
|
|
e = SchedulerNotificationException("a", "m")
|
|
assert e.status_code == 500
|
|
|
|
def test_error_code(self):
|
|
e = SchedulerNotificationException("a", "m")
|
|
assert e.error_code == "SCHEDULER_NOTIFICATION_FAILED"
|
|
|
|
def test_details_include_action(self):
|
|
e = SchedulerNotificationException("refresh", "m")
|
|
assert e.details["action"] == "refresh"
|
|
|
|
|
|
# --- Cross-cutting tests ---
|
|
|
|
|
|
class TestExceptionHierarchy:
|
|
"""Test that all exceptions inherit from NewsAPIException."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"cls",
|
|
[
|
|
NewsFeatureDisabledException,
|
|
InvalidLimitException,
|
|
SubscriptionNotFoundException,
|
|
SubscriptionCreationException,
|
|
SubscriptionUpdateException,
|
|
SubscriptionDeletionException,
|
|
DatabaseAccessException,
|
|
NewsFeedGenerationException,
|
|
ResearchProcessingException,
|
|
NotImplementedException,
|
|
InvalidParameterException,
|
|
SchedulerNotificationException,
|
|
],
|
|
)
|
|
def test_inherits_from_base(self, cls):
|
|
assert issubclass(cls, NewsAPIException)
|
|
|
|
@pytest.mark.parametrize(
|
|
"cls",
|
|
[
|
|
NewsFeatureDisabledException,
|
|
InvalidLimitException,
|
|
SubscriptionNotFoundException,
|
|
SubscriptionCreationException,
|
|
SubscriptionUpdateException,
|
|
SubscriptionDeletionException,
|
|
DatabaseAccessException,
|
|
NewsFeedGenerationException,
|
|
ResearchProcessingException,
|
|
NotImplementedException,
|
|
InvalidParameterException,
|
|
SchedulerNotificationException,
|
|
],
|
|
)
|
|
def test_inherits_from_exception(self, cls):
|
|
assert issubclass(cls, Exception)
|
|
|
|
|
|
class TestExceptionCatchability:
|
|
"""Test that exceptions can be caught by their base class."""
|
|
|
|
def test_catch_by_base_class(self):
|
|
with pytest.raises(NewsAPIException):
|
|
raise SubscriptionNotFoundException("x")
|
|
|
|
def test_catch_by_exception(self):
|
|
with pytest.raises(Exception):
|
|
raise DatabaseAccessException("op", "msg")
|
|
|
|
def test_specific_catch_works(self):
|
|
with pytest.raises(InvalidLimitException):
|
|
raise InvalidLimitException(0)
|