mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(forge): Phase 0 — git_provider column + registration-time forge validation (#569)
* feat(forge): Phase 0 — git_provider column + registration-time forge validation Pointing a project at a GitLab/Gitea git_url used to fail silently, several steps deep, at first PR. New pure policy module (foundation/policy/forge.py) detects the provider from the git_url host and validates at the ProjectService create/update chokepoint: github auto-detects and auto-stamps, explicit git_provider=github is the GitHub Enterprise escape hatch, gitlab/gitea are recognized-but-not-yet-supported, unknown hosts get a loud rejection with guidance. An update changing git_url does NOT inherit a stored auto-stamped provider (restating the override is required), so a host swap can't smuggle the escape hatch past validation. Migration 075 adds the nullable projects.git_provider column; the panel project dialogs show the detected forge. Phase 0 of the forge-providers spec. * fix(panel): mock-mode forge detection extracts the real host CodeQL js/incomplete-url-substring-sanitization: the substring check matched github.com anywhere in the URL. Extract the hostname (URL parse or scp-form regex, mirroring forge.py) and require an exact match. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""Forge provider detection + registration-time validation — pure, no DB/IO."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from roboco.foundation.policy.forge import (
|
||||
KNOWN_PROVIDERS,
|
||||
detect_provider,
|
||||
validate_project_forge,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# detect_provider — host extraction across https/ssh/.git/subgroup shapes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_detect_https_github() -> None:
|
||||
assert detect_provider("https://github.com/owner/repo.git") == "github"
|
||||
|
||||
|
||||
def test_detect_https_github_no_dot_git_suffix() -> None:
|
||||
assert detect_provider("https://github.com/owner/repo") == "github"
|
||||
|
||||
|
||||
def test_detect_https_github_with_token_userinfo() -> None:
|
||||
url = "https://x-access-token:ghp_abc123@github.com/owner/repo.git"
|
||||
assert detect_provider(url) == "github"
|
||||
|
||||
|
||||
def test_detect_ssh_scp_syntax_github() -> None:
|
||||
assert detect_provider("git@github.com:owner/repo.git") == "github"
|
||||
|
||||
|
||||
def test_detect_ssh_url_scheme_github() -> None:
|
||||
assert detect_provider("ssh://git@github.com/owner/repo.git") == "github"
|
||||
|
||||
|
||||
def test_detect_https_gitlab_com() -> None:
|
||||
assert detect_provider("https://gitlab.com/group/project.git") == "gitlab"
|
||||
|
||||
|
||||
def test_detect_ssh_scp_syntax_gitlab() -> None:
|
||||
assert detect_provider("git@gitlab.com:group/project.git") == "gitlab"
|
||||
|
||||
|
||||
def test_detect_gitlab_subgroup_path_still_detects_host() -> None:
|
||||
# Subgroup paths (3+ segments) don't change host resolution — detect_provider
|
||||
# only looks at the host, never the path shape.
|
||||
url = "https://gitlab.com/group/subgroup/project.git"
|
||||
assert detect_provider(url) == "gitlab"
|
||||
|
||||
|
||||
def test_detect_self_hosted_gitlab_host_is_unresolvable() -> None:
|
||||
# A self-hosted host can't be told apart from GHE/Gitea by host alone.
|
||||
assert detect_provider("https://gitlab.example.com/group/project.git") is None
|
||||
|
||||
|
||||
def test_detect_self_hosted_https_unknown_host() -> None:
|
||||
assert detect_provider("https://git.internal.example/owner/repo.git") is None
|
||||
|
||||
|
||||
def test_detect_bitbucket_host_unresolvable() -> None:
|
||||
assert detect_provider("https://bitbucket.org/owner/repo.git") is None
|
||||
|
||||
|
||||
def test_detect_empty_string() -> None:
|
||||
assert detect_provider("") is None
|
||||
|
||||
|
||||
def test_detect_unparseable_garbage() -> None:
|
||||
assert detect_provider("not a url at all") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# validate_project_forge — the registration-time gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_empty_git_url_is_ok() -> None:
|
||||
assert validate_project_forge(None, None) is None
|
||||
assert validate_project_forge("", None) is None
|
||||
|
||||
|
||||
def test_detected_github_no_explicit_provider_is_ok() -> None:
|
||||
assert validate_project_forge("https://github.com/owner/repo.git", None) is None
|
||||
|
||||
|
||||
def test_detected_github_ssh_scp_no_explicit_provider_is_ok() -> None:
|
||||
assert validate_project_forge("git@github.com:owner/repo.git", None) is None
|
||||
|
||||
|
||||
def test_explicit_github_provider_is_ok_regardless_of_host() -> None:
|
||||
# The GitHub Enterprise escape hatch — a non-github.com host is accepted
|
||||
# once the operator explicitly names the provider.
|
||||
url = "https://ghe.internal.example/owner/repo.git"
|
||||
assert validate_project_forge(url, "github") is None
|
||||
|
||||
|
||||
def test_explicit_gitlab_provider_rejected_as_not_yet_supported() -> None:
|
||||
error = validate_project_forge("https://gitlab.com/group/project.git", "gitlab")
|
||||
assert error is not None
|
||||
assert "not yet" in error.lower()
|
||||
assert "gitlab" in error.lower()
|
||||
|
||||
|
||||
def test_explicit_gitea_provider_rejected_as_not_yet_supported() -> None:
|
||||
error = validate_project_forge("https://gitea.example.com/owner/repo.git", "gitea")
|
||||
assert error is not None
|
||||
assert "not yet" in error.lower()
|
||||
assert "gitea" in error.lower()
|
||||
|
||||
|
||||
def test_unknown_host_no_explicit_provider_rejected() -> None:
|
||||
error = validate_project_forge("https://git.internal.example/owner/repo.git", None)
|
||||
assert error is not None
|
||||
assert "github" in error.lower()
|
||||
|
||||
|
||||
def test_detected_gitlab_no_explicit_provider_rejected() -> None:
|
||||
error = validate_project_forge("https://gitlab.com/group/project.git", None)
|
||||
assert error is not None
|
||||
assert "github" in error.lower()
|
||||
|
||||
|
||||
def test_unknown_provider_string_rejected_naming_known_providers() -> None:
|
||||
error = validate_project_forge("https://github.com/owner/repo.git", "bitbucket")
|
||||
assert error is not None
|
||||
for provider in KNOWN_PROVIDERS:
|
||||
assert provider in error
|
||||
|
||||
|
||||
def test_known_providers_tuple_is_the_documented_set() -> None:
|
||||
assert KNOWN_PROVIDERS == ("github", "gitlab", "gitea")
|
||||
Reference in New Issue
Block a user