|
|
|
|
@@ -9,6 +9,12 @@ from unittest.mock import MagicMock
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
|
|
|
|
from src.api.routes import git as git_routes
|
|
|
|
|
from src.api.routes.git import _helpers as git_helpers
|
|
|
|
|
from src.services.git._sync import GitServiceSyncMixin
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _resolved_dashboard_id(*_args, **_kwargs):
|
|
|
|
|
return 12
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# #region test_get_repository_status_returns_no_repo_payload_for_missing_repo [TYPE Function]
|
|
|
|
|
@@ -18,9 +24,9 @@ from src.api.routes import git as git_routes
|
|
|
|
|
# @POST Route returns a deterministic NO_REPO status payload.
|
|
|
|
|
def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch):
|
|
|
|
|
class MissingRepoGitService:
|
|
|
|
|
def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
async def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
return f"/tmp/missing-repo-{dashboard_id}"
|
|
|
|
|
def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
async def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
raise AssertionError("get_status must not be called when repository path is missing")
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", MissingRepoGitService())
|
|
|
|
|
response = asyncio.run(git_routes.get_repository_status(34))
|
|
|
|
|
@@ -36,9 +42,9 @@ def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypa
|
|
|
|
|
# @POST Raised exception preserves original status and detail.
|
|
|
|
|
def test_get_repository_status_propagates_non_404_http_exception(monkeypatch):
|
|
|
|
|
class ConflictGitService:
|
|
|
|
|
def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
async def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
return f"/tmp/existing-repo-{dashboard_id}"
|
|
|
|
|
def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
async def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
raise HTTPException(status_code=409, detail="Conflict")
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", ConflictGitService())
|
|
|
|
|
monkeypatch.setattr(git_routes.os.path, "exists", lambda _path: True)
|
|
|
|
|
@@ -54,7 +60,7 @@ def test_get_repository_status_propagates_non_404_http_exception(monkeypatch):
|
|
|
|
|
# @POST Endpoint raises same HTTPException values.
|
|
|
|
|
def test_get_repository_diff_propagates_http_exception(monkeypatch):
|
|
|
|
|
class DiffGitService:
|
|
|
|
|
def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str:
|
|
|
|
|
async def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Repository missing")
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", DiffGitService())
|
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
|
|
|
@@ -69,7 +75,7 @@ def test_get_repository_diff_propagates_http_exception(monkeypatch):
|
|
|
|
|
# @POST Endpoint returns HTTPException with status 500 and route context.
|
|
|
|
|
def test_get_history_wraps_unexpected_error_as_500(monkeypatch):
|
|
|
|
|
class HistoryGitService:
|
|
|
|
|
def get_commit_history(self, dashboard_id: int, limit: int = 50):
|
|
|
|
|
async def get_commit_history(self, dashboard_id: int, limit: int = 50):
|
|
|
|
|
raise ValueError("broken parser")
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", HistoryGitService())
|
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
|
|
|
@@ -84,7 +90,7 @@ def test_get_history_wraps_unexpected_error_as_500(monkeypatch):
|
|
|
|
|
# @POST Endpoint raises HTTPException(500) with route context.
|
|
|
|
|
def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):
|
|
|
|
|
class CommitGitService:
|
|
|
|
|
def commit_changes(self, dashboard_id: int, message: str, files):
|
|
|
|
|
async def commit_changes(self, dashboard_id: int, message: str, files):
|
|
|
|
|
raise RuntimeError("index lock")
|
|
|
|
|
class CommitPayload:
|
|
|
|
|
message = "test"
|
|
|
|
|
@@ -102,9 +108,9 @@ def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):
|
|
|
|
|
# @POST Returned map includes resolved status for each requested dashboard ID.
|
|
|
|
|
def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):
|
|
|
|
|
class BatchGitService:
|
|
|
|
|
def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
async def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
return f"/tmp/repo-{dashboard_id}"
|
|
|
|
|
def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
async def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
if dashboard_id == 2:
|
|
|
|
|
return {"sync_state": "SYNCED", "sync_status": "OK"}
|
|
|
|
|
raise HTTPException(status_code=404, detail="not found")
|
|
|
|
|
@@ -123,9 +129,9 @@ def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):
|
|
|
|
|
# @POST Failed dashboard status is marked as ERROR.
|
|
|
|
|
def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch):
|
|
|
|
|
class BatchErrorGitService:
|
|
|
|
|
def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
async def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
return f"/tmp/repo-{dashboard_id}"
|
|
|
|
|
def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
async def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
raise RuntimeError("boom")
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", BatchErrorGitService())
|
|
|
|
|
monkeypatch.setattr(git_routes.os.path, "exists", lambda _path: True)
|
|
|
|
|
@@ -142,9 +148,9 @@ def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monk
|
|
|
|
|
# @POST Result contains unique IDs up to configured cap.
|
|
|
|
|
def test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch):
|
|
|
|
|
class SafeBatchGitService:
|
|
|
|
|
def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
async def _get_repo_path(self, dashboard_id: int) -> str:
|
|
|
|
|
return f"/tmp/repo-{dashboard_id}"
|
|
|
|
|
def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
async def get_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
return {"sync_state": "SYNCED", "sync_status": "OK"}
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", SafeBatchGitService())
|
|
|
|
|
monkeypatch.setattr(git_routes.os.path, "exists", lambda _path: True)
|
|
|
|
|
@@ -164,9 +170,9 @@ def test_commit_changes_applies_profile_identity_before_commit(monkeypatch):
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.configured_identity = None
|
|
|
|
|
self.commit_payload = None
|
|
|
|
|
def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):
|
|
|
|
|
async def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):
|
|
|
|
|
self.configured_identity = (dashboard_id, git_username, git_email)
|
|
|
|
|
def commit_changes(self, dashboard_id: int, message: str, files):
|
|
|
|
|
async def commit_changes(self, dashboard_id: int, message: str, files):
|
|
|
|
|
self.commit_payload = (dashboard_id, message, files)
|
|
|
|
|
class PreferenceRow:
|
|
|
|
|
git_username = "user_1"
|
|
|
|
|
@@ -189,7 +195,7 @@ def test_commit_changes_applies_profile_identity_before_commit(monkeypatch):
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
git_routes,
|
|
|
|
|
"_resolve_dashboard_id_from_ref",
|
|
|
|
|
lambda *_args, **_kwargs: 12,
|
|
|
|
|
_resolved_dashboard_id,
|
|
|
|
|
)
|
|
|
|
|
asyncio.run(
|
|
|
|
|
git_routes.commit_changes(
|
|
|
|
|
@@ -213,10 +219,10 @@ def test_pull_changes_applies_profile_identity_before_pull(monkeypatch):
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.configured_identity = None
|
|
|
|
|
self.pulled_dashboard_id = None
|
|
|
|
|
def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):
|
|
|
|
|
async def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):
|
|
|
|
|
self.configured_identity = (dashboard_id, git_username, git_email)
|
|
|
|
|
def pull_changes(self, dashboard_id: int):
|
|
|
|
|
self.pulled_dashboard_id = dashboard_id
|
|
|
|
|
async def pull_changes(self, dashboard_id: int, pat: str | None = None):
|
|
|
|
|
self.pulled_payload = (dashboard_id, pat)
|
|
|
|
|
class PreferenceRow:
|
|
|
|
|
git_username = "user_1"
|
|
|
|
|
git_email = "user1@mail.ru"
|
|
|
|
|
@@ -235,7 +241,7 @@ def test_pull_changes_applies_profile_identity_before_pull(monkeypatch):
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
git_routes,
|
|
|
|
|
"_resolve_dashboard_id_from_ref",
|
|
|
|
|
lambda *_args, **_kwargs: 12,
|
|
|
|
|
_resolved_dashboard_id,
|
|
|
|
|
)
|
|
|
|
|
asyncio.run(
|
|
|
|
|
git_routes.pull_changes(
|
|
|
|
|
@@ -246,8 +252,127 @@ def test_pull_changes_applies_profile_identity_before_pull(monkeypatch):
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
assert identity_service.configured_identity == (12, "user_1", "user1@mail.ru")
|
|
|
|
|
assert identity_service.pulled_dashboard_id == 12
|
|
|
|
|
assert identity_service.pulled_payload == (12, None)
|
|
|
|
|
# #endregion test_pull_changes_applies_profile_identity_before_pull
|
|
|
|
|
# #region test_push_changes_passes_decrypted_profile_pat [TYPE Function]
|
|
|
|
|
# @RELATION BINDS_TO -> TestGitStatusRoute
|
|
|
|
|
# @BRIEF Ensure push route decrypts current user's profile PAT and passes it to GitService.
|
|
|
|
|
# @PRE Profile preference contains encrypted git_personal_access_token.
|
|
|
|
|
# @POST git_service.push_changes receives the decrypted token via pat keyword.
|
|
|
|
|
def test_push_changes_passes_decrypted_profile_pat(monkeypatch):
|
|
|
|
|
class PushGitService:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.pushed_payload = None
|
|
|
|
|
async def push_changes(self, dashboard_id: int, pat: str | None = None):
|
|
|
|
|
self.pushed_payload = (dashboard_id, pat)
|
|
|
|
|
class PreferenceRow:
|
|
|
|
|
git_personal_access_token_encrypted = "encrypted-token"
|
|
|
|
|
class PreferenceQuery:
|
|
|
|
|
def filter(self, *_args, **_kwargs):
|
|
|
|
|
return self
|
|
|
|
|
def first(self):
|
|
|
|
|
return PreferenceRow()
|
|
|
|
|
class DbStub:
|
|
|
|
|
def query(self, _model):
|
|
|
|
|
return PreferenceQuery()
|
|
|
|
|
class UserStub:
|
|
|
|
|
id = "u-1"
|
|
|
|
|
class EncryptionStub:
|
|
|
|
|
def decrypt(self, encrypted_data: str) -> str:
|
|
|
|
|
assert encrypted_data == "encrypted-token"
|
|
|
|
|
return "profile-pat"
|
|
|
|
|
push_service = PushGitService()
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", push_service)
|
|
|
|
|
monkeypatch.setattr(git_helpers, "EncryptionManager", lambda: EncryptionStub())
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", _resolved_dashboard_id)
|
|
|
|
|
asyncio.run(
|
|
|
|
|
git_routes.push_changes(
|
|
|
|
|
"dashboard-12",
|
|
|
|
|
config_manager=MagicMock(),
|
|
|
|
|
db=DbStub(),
|
|
|
|
|
current_user=UserStub(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
assert push_service.pushed_payload == (12, "profile-pat")
|
|
|
|
|
# #endregion test_push_changes_passes_decrypted_profile_pat
|
|
|
|
|
# #region test_pull_changes_passes_decrypted_profile_pat [TYPE Function]
|
|
|
|
|
# @RELATION BINDS_TO -> TestGitStatusRoute
|
|
|
|
|
# @BRIEF Ensure pull route decrypts current user's profile PAT and passes it to GitService.
|
|
|
|
|
# @PRE Profile preference contains git identity and encrypted PAT.
|
|
|
|
|
# @POST git_service.pull_changes receives decrypted token after identity configuration.
|
|
|
|
|
def test_pull_changes_passes_decrypted_profile_pat(monkeypatch):
|
|
|
|
|
class PullGitService:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.configured_identity = None
|
|
|
|
|
self.pulled_payload = None
|
|
|
|
|
async def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):
|
|
|
|
|
self.configured_identity = (dashboard_id, git_username, git_email)
|
|
|
|
|
async def pull_changes(self, dashboard_id: int, pat: str | None = None):
|
|
|
|
|
self.pulled_payload = (dashboard_id, pat)
|
|
|
|
|
class PreferenceRow:
|
|
|
|
|
git_username = "user_1"
|
|
|
|
|
git_email = "user1@mail.ru"
|
|
|
|
|
git_personal_access_token_encrypted = "encrypted-token"
|
|
|
|
|
class PreferenceQuery:
|
|
|
|
|
def filter(self, *_args, **_kwargs):
|
|
|
|
|
return self
|
|
|
|
|
def first(self):
|
|
|
|
|
return PreferenceRow()
|
|
|
|
|
class DbStub:
|
|
|
|
|
def query(self, _model):
|
|
|
|
|
return PreferenceQuery()
|
|
|
|
|
class UserStub:
|
|
|
|
|
id = "u-1"
|
|
|
|
|
class EncryptionStub:
|
|
|
|
|
def decrypt(self, encrypted_data: str) -> str:
|
|
|
|
|
assert encrypted_data == "encrypted-token"
|
|
|
|
|
return "profile-pat"
|
|
|
|
|
pull_service = PullGitService()
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", pull_service)
|
|
|
|
|
monkeypatch.setattr(git_helpers, "EncryptionManager", lambda: EncryptionStub())
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", _resolved_dashboard_id)
|
|
|
|
|
asyncio.run(
|
|
|
|
|
git_routes.pull_changes(
|
|
|
|
|
"dashboard-12",
|
|
|
|
|
config_manager=MagicMock(),
|
|
|
|
|
db=DbStub(),
|
|
|
|
|
current_user=UserStub(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
assert pull_service.configured_identity == (12, "user_1", "user1@mail.ru")
|
|
|
|
|
assert pull_service.pulled_payload == (12, "profile-pat")
|
|
|
|
|
# #endregion test_pull_changes_passes_decrypted_profile_pat
|
|
|
|
|
# #region test_git_sync_mixin_embeds_and_redacts_pat [TYPE Function]
|
|
|
|
|
# @RELATION BINDS_TO -> TestGitStatusRoute
|
|
|
|
|
# @BRIEF Ensure PAT embedding URL-encodes secrets and redaction removes them from messages.
|
|
|
|
|
# @PRE Origin remote uses an HTTPS URL and PAT contains URL-sensitive characters.
|
|
|
|
|
# @POST Origin URL receives encoded PAT; redaction hides raw and URL-shaped tokens.
|
|
|
|
|
def test_git_sync_mixin_embeds_and_redacts_pat():
|
|
|
|
|
class OriginStub:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.urls = ["https://github.com/org/repo.git"]
|
|
|
|
|
self.set_urls = []
|
|
|
|
|
def set_url(self, url: str):
|
|
|
|
|
self.set_urls.append(url)
|
|
|
|
|
self.urls = [url]
|
|
|
|
|
class RepoStub:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.origin = OriginStub()
|
|
|
|
|
def remote(self, name: str):
|
|
|
|
|
assert name == "origin"
|
|
|
|
|
return self.origin
|
|
|
|
|
repo = RepoStub()
|
|
|
|
|
original_url = GitServiceSyncMixin._embed_pat_in_origin_url(repo, "tok@:/secret")
|
|
|
|
|
assert original_url == "https://github.com/org/repo.git"
|
|
|
|
|
assert repo.origin.set_urls == ["https://git-user:tok%40%3A%2Fsecret@github.com/org/repo.git"]
|
|
|
|
|
redacted = GitServiceSyncMixin._redact_pat_from_message(
|
|
|
|
|
"fatal https://git-user:tok%40%3A%2Fsecret@github.com/org/repo.git tok@:/secret",
|
|
|
|
|
"tok@:/secret",
|
|
|
|
|
)
|
|
|
|
|
assert "tok@:/secret" not in redacted
|
|
|
|
|
assert "tok%40%3A%2Fsecret" not in redacted
|
|
|
|
|
assert "***" in redacted
|
|
|
|
|
# #endregion test_git_sync_mixin_embeds_and_redacts_pat
|
|
|
|
|
# #region test_get_merge_status_returns_service_payload [TYPE Function]
|
|
|
|
|
# @RELATION BINDS_TO -> TestGitStatusRoute
|
|
|
|
|
# @BRIEF Ensure merge status route returns service payload as-is.
|
|
|
|
|
@@ -255,7 +380,7 @@ def test_pull_changes_applies_profile_identity_before_pull(monkeypatch):
|
|
|
|
|
# @POST Route response contains has_unfinished_merge=True.
|
|
|
|
|
def test_get_merge_status_returns_service_payload(monkeypatch):
|
|
|
|
|
class MergeStatusGitService:
|
|
|
|
|
def get_merge_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
async def get_merge_status(self, dashboard_id: int) -> dict:
|
|
|
|
|
return {
|
|
|
|
|
"has_unfinished_merge": True,
|
|
|
|
|
"repository_path": "/tmp/repo-12",
|
|
|
|
|
@@ -266,7 +391,7 @@ def test_get_merge_status_returns_service_payload(monkeypatch):
|
|
|
|
|
"conflicts_count": 2,
|
|
|
|
|
}
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", MergeStatusGitService())
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *_args, **_kwargs: 12)
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", _resolved_dashboard_id)
|
|
|
|
|
response = asyncio.run(
|
|
|
|
|
git_routes.get_merge_status(
|
|
|
|
|
"dashboard-12",
|
|
|
|
|
@@ -284,7 +409,7 @@ def test_get_merge_status_returns_service_payload(monkeypatch):
|
|
|
|
|
def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch):
|
|
|
|
|
captured = {}
|
|
|
|
|
class MergeResolveGitService:
|
|
|
|
|
def resolve_merge_conflicts(self, dashboard_id: int, resolutions):
|
|
|
|
|
async def resolve_merge_conflicts(self, dashboard_id: int, resolutions):
|
|
|
|
|
captured["dashboard_id"] = dashboard_id
|
|
|
|
|
captured["resolutions"] = resolutions
|
|
|
|
|
return ["dashboards/a.yaml"]
|
|
|
|
|
@@ -294,7 +419,7 @@ def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch)
|
|
|
|
|
return {"file_path": "dashboards/a.yaml", "resolution": "mine", "content": None}
|
|
|
|
|
resolutions = [_Resolution()]
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", MergeResolveGitService())
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *_args, **_kwargs: 12)
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", _resolved_dashboard_id)
|
|
|
|
|
response = asyncio.run(
|
|
|
|
|
git_routes.resolve_merge_conflicts(
|
|
|
|
|
"dashboard-12",
|
|
|
|
|
@@ -313,11 +438,11 @@ def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch)
|
|
|
|
|
# @POST Route returns aborted status.
|
|
|
|
|
def test_abort_merge_calls_service_and_returns_result(monkeypatch):
|
|
|
|
|
class AbortGitService:
|
|
|
|
|
def abort_merge(self, dashboard_id: int):
|
|
|
|
|
async def abort_merge(self, dashboard_id: int):
|
|
|
|
|
assert dashboard_id == 12
|
|
|
|
|
return {"status": "aborted"}
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", AbortGitService())
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *_args, **_kwargs: 12)
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", _resolved_dashboard_id)
|
|
|
|
|
response = asyncio.run(
|
|
|
|
|
git_routes.abort_merge(
|
|
|
|
|
"dashboard-12",
|
|
|
|
|
@@ -333,14 +458,14 @@ def test_abort_merge_calls_service_and_returns_result(monkeypatch):
|
|
|
|
|
# @POST Route returns committed status and hash.
|
|
|
|
|
def test_continue_merge_passes_message_and_returns_commit(monkeypatch):
|
|
|
|
|
class ContinueGitService:
|
|
|
|
|
def continue_merge(self, dashboard_id: int, message: str):
|
|
|
|
|
async def continue_merge(self, dashboard_id: int, message: str):
|
|
|
|
|
assert dashboard_id == 12
|
|
|
|
|
assert message == "Resolve all conflicts"
|
|
|
|
|
return {"status": "committed", "commit_hash": "abc123"}
|
|
|
|
|
class ContinueData:
|
|
|
|
|
message = "Resolve all conflicts"
|
|
|
|
|
monkeypatch.setattr(git_routes, "git_service", ContinueGitService())
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *_args, **_kwargs: 12)
|
|
|
|
|
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", _resolved_dashboard_id)
|
|
|
|
|
response = asyncio.run(
|
|
|
|
|
git_routes.continue_merge(
|
|
|
|
|
"dashboard-12",
|
|
|
|
|
|