diff --git a/tests/unit/test_scoring_availability.py b/tests/unit/test_scoring_availability.py new file mode 100644 index 0000000..bf4a5ac --- /dev/null +++ b/tests/unit/test_scoring_availability.py @@ -0,0 +1,183 @@ +"""Unit tests for capiscio_sdk.scoring.availability — AvailabilityScorer.""" + +from capiscio_sdk.types import ValidationIssue, ValidationSeverity +from capiscio_sdk.scoring.availability import AvailabilityScorer +from capiscio_sdk.scoring.types import AvailabilityRating + + +def _issue(code: str, msg: str = "test") -> ValidationIssue: + return ValidationIssue(severity=ValidationSeverity.ERROR, code=code, message=msg) + + +class TestAvailabilityScorerNotTested: + """score_not_tested() should return an untested placeholder.""" + + def test_defaults(self): + scorer = AvailabilityScorer() + result = scorer.score_not_tested() + assert result.tested is False + assert result.total is None + assert result.rating is None + assert result.breakdown is None + assert result.not_tested_reason == "Network tests not enabled" + + def test_custom_reason(self): + scorer = AvailabilityScorer() + result = scorer.score_not_tested("Behind firewall") + assert result.not_tested_reason == "Behind firewall" + + +class TestAvailabilityScorerPerfect: + """Fully available endpoint: responds fast, TLS valid, CORS present.""" + + def test_maximum_score(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test( + endpoint_responded=True, + response_time=0.5, + has_cors=True, + valid_tls=True, + ) + assert result.tested is True + assert result.total == 100 + assert result.rating == AvailabilityRating.FULLY_AVAILABLE + + +class TestAvailabilityScorerPrimaryEndpoint: + """Primary endpoint subscore (50 points max).""" + + def test_no_response(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test(endpoint_responded=False) + pe = result.breakdown.primary_endpoint + assert pe.responds is False + assert pe.score == 0 + assert "Endpoint did not respond" in pe.errors + + def test_fast_response(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test(endpoint_responded=True, response_time=1.0) + pe = result.breakdown.primary_endpoint + assert pe.score >= 40 # 30 responds + 10 fast + + def test_medium_response(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test(endpoint_responded=True, response_time=3.0) + pe = result.breakdown.primary_endpoint + # 30 responds + 5 medium speed + assert pe.score >= 35 + + def test_slow_response(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test(endpoint_responded=True, response_time=10.0) + pe = result.breakdown.primary_endpoint + # 30 responds + 0 speed + assert pe.score == 30 + assert any("Slow response" in e for e in pe.errors) + + def test_invalid_tls(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test( + endpoint_responded=True, valid_tls=False + ) + pe = result.breakdown.primary_endpoint + assert "Invalid TLS certificate" in pe.errors + + def test_missing_cors(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test( + endpoint_responded=True, has_cors=False + ) + pe = result.breakdown.primary_endpoint + assert "Missing CORS headers" in pe.errors + + def test_tls_and_cors_none_no_penalty(self): + """When TLS/CORS weren't checked (None), no errors are added.""" + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test( + endpoint_responded=True, + valid_tls=None, + has_cors=None, + ) + pe = result.breakdown.primary_endpoint + assert pe.errors == [] + + +class TestAvailabilityScorerTransportSupport: + """Transport support subscore (30 points max).""" + + def test_transport_works(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test(endpoint_responded=True) + ts = result.breakdown.transport_support + assert ts.preferred_transport_works is True + assert ts.score == 30 + + def test_transport_failed(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test( + endpoint_responded=True, + issues=[_issue("TRANSPORT_FAILED")] + ) + ts = result.breakdown.transport_support + assert ts.preferred_transport_works is False + assert ts.score == 0 + + def test_transport_not_tested_when_down(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test(endpoint_responded=False) + ts = result.breakdown.transport_support + assert ts.preferred_transport_works is False + assert ts.score == 0 + + +class TestAvailabilityScorerResponseQuality: + """Response quality subscore (20 points max).""" + + def test_valid_response(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test(endpoint_responded=True) + rq = result.breakdown.response_quality + assert rq.valid_structure is True + assert rq.proper_content_type is True + assert rq.proper_error_handling is True + assert rq.score == 20 + + def test_malformed_json(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test( + endpoint_responded=True, + issues=[_issue("MALFORMED_JSON")] + ) + rq = result.breakdown.response_quality + assert rq.valid_structure is False + assert rq.score == 10 # content_type 5 + error_handling 5 + + def test_invalid_content_type(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test( + endpoint_responded=True, + issues=[_issue("INVALID_CONTENT_TYPE")] + ) + rq = result.breakdown.response_quality + assert rq.proper_content_type is False + + def test_no_quality_when_down(self): + scorer = AvailabilityScorer() + result = scorer.score_endpoint_test(endpoint_responded=False) + rq = result.breakdown.response_quality + assert rq.score == 0 + + +class TestAvailabilityScorerIssueFiltering: + """Only availability-related issues appear in the result.""" + + def test_availability_issues_included(self): + scorer = AvailabilityScorer() + issues = [ + _issue("ENDPOINT_UNREACHABLE", "endpoint down"), + _issue("INVALID_SEMVER", "compliance issue"), + ] + result = scorer.score_endpoint_test(endpoint_responded=True, issues=issues) + assert "endpoint down" in result.issues + assert "compliance issue" not in result.issues diff --git a/tests/unit/test_scoring_compliance.py b/tests/unit/test_scoring_compliance.py new file mode 100644 index 0000000..1da0d36 --- /dev/null +++ b/tests/unit/test_scoring_compliance.py @@ -0,0 +1,182 @@ +"""Unit tests for capiscio_sdk.scoring.compliance — ComplianceScorer.""" + +import pytest + +from capiscio_sdk.types import ValidationIssue, ValidationSeverity +from capiscio_sdk.scoring.compliance import ComplianceScorer +from capiscio_sdk.scoring.types import ComplianceRating + + +def _issue(code: str, msg: str = "test") -> ValidationIssue: + """Shorthand for creating a ValidationIssue.""" + return ValidationIssue(severity=ValidationSeverity.ERROR, code=code, message=msg) + + +def _full_card() -> dict: + """Agent card with every required field populated.""" + return { + "name": "TestAgent", + "description": "A test agent", + "url": "https://example.com", + "version": "1.0.0", + "protocolVersion": "1.0", + "preferredTransport": "https", + "capabilities": {"securitySchemes": []}, + "provider": {"organization": "Acme", "url": "https://acme.example.com"}, + "skills": [ + {"id": "s1", "name": "Skill 1", "description": "Does things", "tags": ["general"]}, + {"id": "s2", "name": "Skill 2", "description": "Does more", "tags": ["general"]}, + ], + } + + +class TestComplianceScorerPerfect: + """A fully-populated, issue-free card should score 100 / Perfect.""" + + def test_perfect_score(self): + scorer = ComplianceScorer() + result = scorer.score_agent_card(_full_card(), []) + + assert result.total == 100 + assert result.rating == ComplianceRating.PERFECT + assert result.breakdown.core_fields.score == 60 + assert result.breakdown.skills_quality.score == 20 + assert result.breakdown.format_compliance.score == 15 + assert result.breakdown.data_quality.score == 5 + + +class TestComplianceScorerCoreFields: + """Core fields subscore (60 points).""" + + def test_empty_card_zero_core(self): + scorer = ComplianceScorer() + result = scorer.score_agent_card({}, []) + assert result.breakdown.core_fields.score == 0 + assert set(result.breakdown.core_fields.missing) == set(ComplianceScorer.REQUIRED_FIELDS) + + def test_partial_fields(self): + scorer = ComplianceScorer() + card = {"name": "A", "version": "1.0.0"} + result = scorer.score_agent_card(card, []) + assert result.breakdown.core_fields.score == int(2 * ComplianceScorer.POINTS_PER_CORE_FIELD) + assert "name" in result.breakdown.core_fields.present + assert "version" in result.breakdown.core_fields.present + + def test_falsy_values_count_as_missing(self): + scorer = ComplianceScorer() + card = {"name": "", "version": None} + result = scorer.score_agent_card(card, []) + assert result.breakdown.core_fields.score == 0 + + +class TestComplianceScorerSkillsQuality: + """Skills quality subscore (20 points).""" + + def test_no_skills(self): + scorer = ComplianceScorer() + result = scorer.score_agent_card({"skills": []}, []) + assert result.breakdown.skills_quality.score == 0 + assert result.breakdown.skills_quality.skills_present is False + + def test_skills_without_required_fields(self): + scorer = ComplianceScorer() + card = {"skills": [{"id": "s1"}]} # missing name, description + result = scorer.score_agent_card(card, []) + bd = result.breakdown.skills_quality + assert bd.skills_present is True + assert bd.all_skills_have_required_fields is False + assert bd.score == 5 # only "skills present" bonus + + def test_skills_without_tags(self): + scorer = ComplianceScorer() + card = {"skills": [{"id": "s1", "name": "S", "description": "D"}]} # no tags + result = scorer.score_agent_card(card, []) + bd = result.breakdown.skills_quality + assert bd.all_skills_have_required_fields is True + assert bd.all_skills_have_tags is False + assert bd.score == 15 # 5 present + 10 required fields + + def test_non_dict_skills_handled(self): + """Skills that aren't dicts should be counted as issues.""" + scorer = ComplianceScorer() + card = {"skills": ["not-a-dict"]} + result = scorer.score_agent_card(card, []) + bd = result.breakdown.skills_quality + assert bd.all_skills_have_required_fields is False + assert bd.issue_count >= 1 + + def test_non_list_skills_handled(self): + """skills: 'invalid' should be treated as empty.""" + scorer = ComplianceScorer() + card = {"skills": "invalid"} + result = scorer.score_agent_card(card, []) + assert result.breakdown.skills_quality.skills_present is False + + +class TestComplianceScorerFormatCompliance: + """Format compliance subscore (15 points).""" + + def test_all_valid_formats(self): + scorer = ComplianceScorer() + result = scorer.score_agent_card({}, []) + assert result.breakdown.format_compliance.score == 15 + + @pytest.mark.parametrize("issue_code, field", [ + ("INVALID_SEMVER", "valid_semver"), + ("INVALID_PROTOCOL_VERSION", "valid_protocol_version"), + ("INVALID_TRANSPORT", "valid_transports"), + ("INVALID_MIME_TYPE", "valid_mime_types"), + ]) + def test_individual_format_failures(self, issue_code, field): + scorer = ComplianceScorer() + result = scorer.score_agent_card({}, [_issue(issue_code)]) + bd = result.breakdown.format_compliance + assert getattr(bd, field) is False + assert bd.score == 12 # 15 - 3 + + def test_insecure_url_counts(self): + scorer = ComplianceScorer() + result = scorer.score_agent_card({}, [_issue("INSECURE_URL")]) + assert result.breakdown.format_compliance.valid_url is False + + +class TestComplianceScorerDataQuality: + """Data quality subscore (5 points).""" + + def test_duplicate_skill_ids(self): + scorer = ComplianceScorer() + card = {"skills": [ + {"id": "dup", "name": "A", "description": "A"}, + {"id": "dup", "name": "B", "description": "B"}, + ]} + result = scorer.score_agent_card(card, []) + assert result.breakdown.data_quality.no_duplicate_skill_ids is False + + def test_field_length_exceeded(self): + scorer = ComplianceScorer() + result = scorer.score_agent_card({}, [_issue("FIELD_LENGTH_EXCEEDED")]) + assert result.breakdown.data_quality.field_lengths_valid is False + + def test_ssrf_risk(self): + scorer = ComplianceScorer() + result = scorer.score_agent_card({}, [_issue("SSRF_RISK")]) + assert result.breakdown.data_quality.no_ssrf_risks is False + + +class TestComplianceScorerIssueFiltering: + """Only compliance-related issues appear in the result.""" + + def test_compliance_issues_included(self): + scorer = ComplianceScorer() + issues = [ + _issue("MISSING_REQUIRED_FIELD", "name is required"), + _issue("SIGNATURE_VERIFICATION_FAILED", "trust issue"), # not compliance + ] + result = scorer.score_agent_card({}, issues) + assert "name is required" in result.issues + assert "trust issue" not in result.issues + + def test_total_clamped_to_0_100(self): + scorer = ComplianceScorer() + result = scorer.score_agent_card({}, []) + assert 0 <= result.total <= 100 diff --git a/tests/unit/test_scoring_trust.py b/tests/unit/test_scoring_trust.py new file mode 100644 index 0000000..dd1663d --- /dev/null +++ b/tests/unit/test_scoring_trust.py @@ -0,0 +1,192 @@ +"""Unit tests for capiscio_sdk.scoring.trust — TrustScorer.""" + +from capiscio_sdk.types import ValidationIssue, ValidationSeverity +from capiscio_sdk.scoring.trust import TrustScorer +from capiscio_sdk.scoring.types import TrustRating + + +def _issue(code: str, msg: str = "test") -> ValidationIssue: + return ValidationIssue(severity=ValidationSeverity.ERROR, code=code, message=msg) + + +def _full_card() -> dict: + """Card with all trust-relevant fields present.""" + return { + "signatures": [{"sig": "a"}, {"sig": "b"}], + "provider": {"organization": "Acme", "url": "https://acme.example.com"}, + "capabilities": { + "securitySchemes": [{"type": "apiKey", "name": "x-api-key"}], + }, + "documentationUrl": "https://docs.example.com", + "termsOfService": "https://tos.example.com", + "privacyPolicy": "https://privacy.example.com", + } + + +class TestTrustScorerPerfect: + """Fully trusted card with valid signatures, full provider, etc.""" + + def test_maximum_score(self): + scorer = TrustScorer() + result = scorer.score_agent_card(_full_card(), []) + + assert result.raw_score == 100 + assert result.confidence_multiplier == 1.0 + assert result.total == 100 + assert result.rating == TrustRating.HIGHLY_TRUSTED + assert result.partial_validation is False + + +class TestTrustScorerSignatures: + """Signatures subscore (40 points max).""" + + def test_skip_verification(self): + scorer = TrustScorer() + result = scorer.score_agent_card(_full_card(), [], skip_signature_verification=True) + assert result.breakdown.signatures.tested is False + assert result.breakdown.signatures.score == 0 + assert result.partial_validation is True + + def test_missing_signature(self): + scorer = TrustScorer() + result = scorer.score_agent_card(_full_card(), [_issue("MISSING_SIGNATURE")]) + sig = result.breakdown.signatures + assert sig.has_valid_signature is False + assert result.confidence_multiplier == 0.6 + + def test_invalid_signature(self): + scorer = TrustScorer() + result = scorer.score_agent_card( + _full_card(), + [_issue("SIGNATURE_VERIFICATION_FAILED")] + ) + sig = result.breakdown.signatures + assert sig.has_invalid_signature is True + assert result.confidence_multiplier == 0.4 + + def test_expired_signature(self): + scorer = TrustScorer() + result = scorer.score_agent_card(_full_card(), [_issue("SIGNATURE_EXPIRED")]) + sig = result.breakdown.signatures + assert sig.has_expired_signature is True + assert sig.is_recent is False + + def test_single_signature_no_multi_bonus(self): + scorer = TrustScorer() + card = _full_card() + card["signatures"] = [{"sig": "only-one"}] + result = scorer.score_agent_card(card, []) + assert result.breakdown.signatures.multiple_signatures is False + + +class TestTrustScorerProvider: + """Provider subscore (25 points max).""" + + def test_full_provider(self): + scorer = TrustScorer() + result = scorer.score_agent_card(_full_card(), []) + prov = result.breakdown.provider + assert prov.has_organization is True + assert prov.has_url is True + assert prov.url_reachable is True + assert prov.score == 25 + + def test_no_provider(self): + scorer = TrustScorer() + card = _full_card() + card["provider"] = {} + result = scorer.score_agent_card(card, []) + assert result.breakdown.provider.score == 0 + + def test_provider_url_unreachable(self): + scorer = TrustScorer() + result = scorer.score_agent_card( + _full_card(), + [_issue("PROVIDER_URL_UNREACHABLE")] + ) + prov = result.breakdown.provider + assert prov.url_reachable is False + assert prov.score == 20 # 10 org + 10 url, no reachable bonus + + def test_non_dict_provider(self): + scorer = TrustScorer() + card = _full_card() + card["provider"] = "invalid" + result = scorer.score_agent_card(card, []) + assert result.breakdown.provider.score == 0 + + +class TestTrustScorerSecurity: + """Security subscore (20 points max).""" + + def test_https_only(self): + scorer = TrustScorer() + result = scorer.score_agent_card(_full_card(), []) + sec = result.breakdown.security + assert sec.https_only is True + assert sec.score == 20 + + def test_http_url_found(self): + scorer = TrustScorer() + result = scorer.score_agent_card(_full_card(), [_issue("HTTP_URL_FOUND")]) + sec = result.breakdown.security + assert sec.https_only is False + assert sec.has_http_urls is True + + def test_no_security_schemes(self): + scorer = TrustScorer() + card = _full_card() + card["capabilities"] = {"securitySchemes": []} + result = scorer.score_agent_card(card, []) + sec = result.breakdown.security + assert sec.has_security_schemes is False + assert sec.has_strong_auth is False + assert sec.score == 10 # https_only only + + def test_non_dict_capabilities(self): + scorer = TrustScorer() + card = _full_card() + card["capabilities"] = "invalid" + result = scorer.score_agent_card(card, []) + sec = result.breakdown.security + assert sec.has_security_schemes is False + + +class TestTrustScorerDocumentation: + """Documentation subscore (15 points max).""" + + def test_full_docs(self): + scorer = TrustScorer() + result = scorer.score_agent_card(_full_card(), []) + doc = result.breakdown.documentation + assert doc.score == 15 + + def test_no_docs(self): + scorer = TrustScorer() + card = _full_card() + del card["documentationUrl"] + del card["termsOfService"] + del card["privacyPolicy"] + result = scorer.score_agent_card(card, []) + assert result.breakdown.documentation.score == 0 + + +class TestTrustScorerConfidenceMultiplier: + """End-to-end multiplier application.""" + + def test_multiplier_applied_to_total(self): + scorer = TrustScorer() + # Missing signature → 0.6x + result = scorer.score_agent_card(_full_card(), [_issue("MISSING_SIGNATURE")]) + # raw_score still counts other components, total is int(raw * 0.6) + assert result.total == int(result.raw_score * 0.6) + + def test_issue_filtering(self): + scorer = TrustScorer() + issues = [ + _issue("MISSING_SIGNATURE", "trust issue"), + _issue("INVALID_SEMVER", "compliance issue"), + ] + result = scorer.score_agent_card(_full_card(), issues) + assert "trust issue" in result.issues + assert "compliance issue" not in result.issues diff --git a/tests/unit/test_scoring_types.py b/tests/unit/test_scoring_types.py new file mode 100644 index 0000000..5c29090 --- /dev/null +++ b/tests/unit/test_scoring_types.py @@ -0,0 +1,197 @@ +"""Unit tests for capiscio_sdk.scoring.types — rating helpers and dataclasses.""" + +import pytest + +from capiscio_sdk.scoring.types import ( + ComplianceRating, + TrustRating, + AvailabilityRating, + ComplianceScore, + TrustScore, + AvailabilityScore, + ComplianceBreakdown, + CoreFieldsBreakdown, + SkillsQualityBreakdown, + FormatComplianceBreakdown, + DataQualityBreakdown, + TrustBreakdown, + SignaturesBreakdown, + ProviderBreakdown, + SecurityBreakdown, + DocumentationBreakdown, + ScoringContext, + get_compliance_rating, + get_trust_rating, + get_availability_rating, + get_trust_confidence_multiplier, +) + + +# --------------------------------------------------------------------------- +# Rating helper functions +# --------------------------------------------------------------------------- + + +class TestGetComplianceRating: + """Tests for get_compliance_rating() boundary values.""" + + @pytest.mark.parametrize("score, expected", [ + (100, ComplianceRating.PERFECT), + (99, ComplianceRating.EXCELLENT), + (90, ComplianceRating.EXCELLENT), + (89, ComplianceRating.GOOD), + (75, ComplianceRating.GOOD), + (74, ComplianceRating.FAIR), + (60, ComplianceRating.FAIR), + (59, ComplianceRating.POOR), + (0, ComplianceRating.POOR), + ]) + def test_boundary_values(self, score, expected): + assert get_compliance_rating(score) == expected + + +class TestGetTrustRating: + """Tests for get_trust_rating() boundary values.""" + + @pytest.mark.parametrize("score, expected", [ + (100, TrustRating.HIGHLY_TRUSTED), + (80, TrustRating.HIGHLY_TRUSTED), + (79, TrustRating.TRUSTED), + (60, TrustRating.TRUSTED), + (59, TrustRating.MODERATE_TRUST), + (40, TrustRating.MODERATE_TRUST), + (39, TrustRating.LOW_TRUST), + (20, TrustRating.LOW_TRUST), + (19, TrustRating.UNTRUSTED), + (0, TrustRating.UNTRUSTED), + ]) + def test_boundary_values(self, score, expected): + assert get_trust_rating(score) == expected + + +class TestGetAvailabilityRating: + """Tests for get_availability_rating() boundary values.""" + + @pytest.mark.parametrize("score, expected", [ + (100, AvailabilityRating.FULLY_AVAILABLE), + (95, AvailabilityRating.FULLY_AVAILABLE), + (94, AvailabilityRating.AVAILABLE), + (80, AvailabilityRating.AVAILABLE), + (79, AvailabilityRating.DEGRADED), + (60, AvailabilityRating.DEGRADED), + (59, AvailabilityRating.UNSTABLE), + (40, AvailabilityRating.UNSTABLE), + (39, AvailabilityRating.UNAVAILABLE), + (0, AvailabilityRating.UNAVAILABLE), + ]) + def test_boundary_values(self, score, expected): + assert get_availability_rating(score) == expected + + +class TestGetTrustConfidenceMultiplier: + """Tests for get_trust_confidence_multiplier().""" + + def test_valid_signature(self): + assert get_trust_confidence_multiplier(True, False) == 1.0 + + def test_no_signature(self): + assert get_trust_confidence_multiplier(False, False) == 0.6 + + def test_invalid_signature(self): + assert get_trust_confidence_multiplier(False, True) == 0.4 + + def test_invalid_overrides_valid(self): + """Invalid signature takes precedence even if a valid one exists.""" + assert get_trust_confidence_multiplier(True, True) == 0.4 + + +# --------------------------------------------------------------------------- +# Dataclass post_init validation +# --------------------------------------------------------------------------- + + +def _make_compliance_breakdown(): + """Helper — minimal valid ComplianceBreakdown.""" + return ComplianceBreakdown( + core_fields=CoreFieldsBreakdown(score=60), + skills_quality=SkillsQualityBreakdown(score=20), + format_compliance=FormatComplianceBreakdown(score=15), + data_quality=DataQualityBreakdown(score=5), + ) + + +def _make_trust_breakdown(): + """Helper — minimal valid TrustBreakdown.""" + return TrustBreakdown( + signatures=SignaturesBreakdown(score=40), + provider=ProviderBreakdown(score=25), + security=SecurityBreakdown(score=20), + documentation=DocumentationBreakdown(score=15), + ) + + +class TestComplianceScoreValidation: + def test_valid_range(self): + cs = ComplianceScore( + total=85, + rating=ComplianceRating.GOOD, + breakdown=_make_compliance_breakdown(), + ) + assert cs.total == 85 + + def test_rejects_above_100(self): + with pytest.raises(AssertionError): + ComplianceScore( + total=101, + rating=ComplianceRating.PERFECT, + breakdown=_make_compliance_breakdown(), + ) + + def test_rejects_negative(self): + with pytest.raises(AssertionError): + ComplianceScore( + total=-1, + rating=ComplianceRating.POOR, + breakdown=_make_compliance_breakdown(), + ) + + +class TestTrustScoreValidation: + def test_valid_range(self): + ts = TrustScore( + total=60, + raw_score=100, + confidence_multiplier=0.6, + rating=TrustRating.TRUSTED, + breakdown=_make_trust_breakdown(), + ) + assert ts.total == 60 + + def test_rejects_bad_multiplier(self): + with pytest.raises(AssertionError): + TrustScore( + total=50, + raw_score=50, + confidence_multiplier=0.5, + rating=TrustRating.MODERATE_TRUST, + breakdown=_make_trust_breakdown(), + ) + + +class TestAvailabilityScoreValidation: + def test_not_tested(self): + a = AvailabilityScore(total=None, rating=None, breakdown=None, tested=False) + assert a.tested is False + + def test_rejects_above_100(self): + with pytest.raises(AssertionError): + AvailabilityScore(total=101, rating=AvailabilityRating.FULLY_AVAILABLE, breakdown=None) + + +class TestScoringContext: + def test_defaults(self): + ctx = ScoringContext() + assert ctx.schema_only is False + assert ctx.skip_signature_verification is False + assert ctx.test_live is False + assert ctx.strict_mode is False