Files
ss-tools/backend/tests/services/clean_release/test_publication_service.py
busya 4205618ee6 chore: eliminate all deprecation warnings from tests and linter
Warnings fixed:
- datetime.utcnow() → datetime.now(UTC) across 48+ files (src/ + tests/)
- datetime.utcnow (callback ref) → lambda: datetime.now(UTC) in model fields (18 files)
- Pydantic class Config → model_config = ConfigDict(...) (16 files)
- Pydantic .dict() → .model_dump() (8 files)
- ConfigDict(allow_population_by_field_name=True) → validate_by_name=True
- SQLAlchemy declarative_base() import path updated
- FastAPI on_event → lifespan context manager (app.py)
- Import sorting (ruff I001) auto-fixed across all files
- Fixed broken re-export chains that ruff F401 cleanup broke:
  _validate_bcp47: service.py now imports from dictionary_validation directly
  job_to_response: _job_routes.py and test imports from service_utils directly
  fetch_datasource_metadata: restored re-export in service.py
- Added missing TranslateJobService import in _job_routes.py (was deleted by F401)
- Added ConfigDict(protected_namespaces=()) for DashboardDatasetItem schema field
- pytest.ini: replaced deprecated importmode with asyncio_mode

All 440 tests pass with zero deprecation warnings.
2026-05-26 19:18:28 +03:00

149 lines
5.7 KiB
Python

# #region TestPublicationService [C:2] [TYPE Module]
# @RELATION BINDS_TO -> SrcRoot
# @SEMANTICS: tests, clean-release, publication, revoke, gate
# @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records.
# @LAYER Tests
# @INVARIANT: Publish requires approval; revoke requires existing publication; republish after revoke is allowed as a new record.
from __future__ import annotations
from datetime import UTC, datetime
import pytest
from src.models.clean_release import ComplianceReport, ReleaseCandidate
from src.services.clean_release.enums import CandidateStatus, ComplianceDecision, PublicationStatus
from src.services.clean_release.exceptions import PublicationGateError
from src.services.clean_release.repository import CleanReleaseRepository
# #region _seed_candidate_with_passed_report [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestPublicationService
# @PURPOSE: Seed candidate/report fixtures for publication gate scenarios.
# @PRE: candidate_id and report_id are non-empty.
# @POST: Repository contains candidate and PASSED report.
def _seed_candidate_with_passed_report(
*,
candidate_id: str = "cand-publish-1",
report_id: str = "CCR-publish-1",
candidate_status: CandidateStatus = CandidateStatus.CHECK_PASSED,
) -> tuple[CleanReleaseRepository, str, str]:
repository = CleanReleaseRepository()
repository.save_candidate(
ReleaseCandidate(
id=candidate_id,
version="1.0.0",
source_snapshot_ref="git:sha-publish-1",
created_by="tester",
created_at=datetime.now(UTC),
status=candidate_status.value,
)
)
repository.save_report(
ComplianceReport(
id=report_id,
run_id="run-publish-1",
candidate_id=candidate_id,
final_status=ComplianceDecision.PASSED.value,
summary_json={"operator_summary": "seed", "violations_count": 0, "blocking_violations_count": 0},
generated_at=datetime.now(UTC),
immutable=True,
)
)
return repository, candidate_id, report_id
# #endregion _seed_candidate_with_passed_report
# #region test_publish_without_approval_rejected [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestPublicationService
# @PURPOSE: Ensure publish action is blocked until candidate is approved.
# @PRE: Candidate has PASSED report but status is not APPROVED.
# @POST: publish_candidate raises PublicationGateError.
def test_publish_without_approval_rejected():
from src.services.clean_release.publication_service import publish_candidate
repository, candidate_id, report_id = _seed_candidate_with_passed_report(
candidate_status=CandidateStatus.CHECK_PASSED,
)
with pytest.raises(PublicationGateError, match="APPROVED"):
publish_candidate(
repository=repository,
candidate_id=candidate_id,
report_id=report_id,
published_by="publisher",
target_channel="stable",
publication_ref="rel-1",
)
# #endregion test_publish_without_approval_rejected
# #region test_revoke_unknown_publication_rejected [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestPublicationService
# @PURPOSE: Ensure revocation is rejected for unknown publication id.
# @PRE: Repository has no matching publication record.
# @POST: revoke_publication raises PublicationGateError.
def test_revoke_unknown_publication_rejected():
from src.services.clean_release.publication_service import revoke_publication
repository, _, _ = _seed_candidate_with_passed_report()
with pytest.raises(PublicationGateError, match="not found"):
revoke_publication(
repository=repository,
publication_id="missing-publication",
revoked_by="publisher",
comment="unknown publication id",
)
# #endregion test_revoke_unknown_publication_rejected
# #region test_republish_after_revoke_creates_new_active_record [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestPublicationService
# @PURPOSE: Ensure republish after revoke is allowed and creates a new ACTIVE record.
# @PRE: Candidate is APPROVED and first publication has been revoked.
# @POST: New publish call returns distinct publication id with ACTIVE status.
def test_republish_after_revoke_creates_new_active_record():
from src.services.clean_release.approval_service import approve_candidate
from src.services.clean_release.publication_service import publish_candidate, revoke_publication
repository, candidate_id, report_id = _seed_candidate_with_passed_report(
candidate_status=CandidateStatus.CHECK_PASSED,
)
approve_candidate(
repository=repository,
candidate_id=candidate_id,
report_id=report_id,
decided_by="approver",
comment="approval before publication",
)
first = publish_candidate(
repository=repository,
candidate_id=candidate_id,
report_id=report_id,
published_by="publisher",
target_channel="stable",
publication_ref="release-1",
)
revoked = revoke_publication(
repository=repository,
publication_id=first.id,
revoked_by="publisher",
comment="rollback",
)
second = publish_candidate(
repository=repository,
candidate_id=candidate_id,
report_id=report_id,
published_by="publisher",
target_channel="stable",
publication_ref="release-2",
)
assert first.id != second.id
assert revoked.status == PublicationStatus.REVOKED.value
assert second.status == PublicationStatus.ACTIVE.value
# #endregion test_republish_after_revoke_creates_new_active_record
# #endregion TestPublicationService