Clears cc-ci self-test lint failures: - ruff format: 9 files reformatted (all gtea test files + test_discovery.py) - ruff check --fix: bridge.py UP017 (datetime.UTC alias) + 6 gtea check errors - manifest.py B007: rename unused loop variable path → _path (no auto-fix available) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
225 lines
8.7 KiB
Python
225 lines
8.7 KiB
Python
"""gitea — LFS round-trip capstone (phase gtea, PR #1 lfs-plain-gitea).
|
|
|
|
This test is the PROOF for PR #1 (feat: support Git LFS on plain gitea):
|
|
- On gitea main: compose.lfs.yml is ABSENT → test skips (declared EXPECTED_NA in PARITY.md).
|
|
- On lfs-plain-gitea PR head: compose.lfs.yml is PRESENT → test runs and must pass.
|
|
|
|
What it tests:
|
|
1. LFS object upload (git lfs push) → download round-trip: fetched bytes hash to the OID.
|
|
2. LFS JWT secret stability: after `abra app restart`, the rendered app.ini LFS_JWT_SECRET
|
|
is unchanged and tokens still validate (the regression the PR fixes).
|
|
|
|
Non-vacuous: a gitea without LFS enabled cannot accept lfs push (step 1 fails); a rotating
|
|
LFS_JWT_SECRET breaks existing tokens (step 2 fails).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
import ssl
|
|
|
|
from harness import lifecycle # noqa: E402
|
|
from ops import admin_creds # noqa: E402
|
|
|
|
_CTX = ssl.create_default_context()
|
|
_CTX.check_hostname = False
|
|
_CTX.verify_mode = ssl.CERT_NONE
|
|
|
|
_LFS_OVERLAY = "compose.lfs.yml"
|
|
|
|
|
|
def _lfs_available() -> bool:
|
|
abra_dir = os.environ.get("ABRA_DIR") or os.path.expanduser("~/.abra")
|
|
return os.path.exists(os.path.join(abra_dir, "recipes", "gitea", _LFS_OVERLAY))
|
|
|
|
|
|
def _api(domain, path, method="GET", body=None, user="", password=""):
|
|
data = json.dumps(body).encode() if body is not None else None
|
|
auth = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
headers = {"Authorization": f"Basic {auth}"}
|
|
if data:
|
|
headers["Content-Type"] = "application/json"
|
|
req = urllib.request.Request(
|
|
f"https://{domain}/api/v1{path}", data=data, headers=headers, method=method
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=20, context=_CTX) as r:
|
|
raw = r.read()
|
|
return r.status, (json.loads(raw) if raw else {})
|
|
except urllib.error.HTTPError as e:
|
|
raw = e.read()
|
|
try:
|
|
return e.code, json.loads(raw)
|
|
except (ValueError, json.JSONDecodeError):
|
|
return e.code, {}
|
|
|
|
|
|
def _run_git(args, cwd, env=None):
|
|
full_env = {**os.environ, **(env or {})}
|
|
result = subprocess.run(
|
|
["git"] + args, cwd=cwd, capture_output=True, text=True, env=full_env, timeout=120
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"git {' '.join(args)} failed:\n{result.stderr}")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def test_lfs_roundtrip(live_app):
|
|
"""LFS object upload→download round-trip + JWT-secret stability after restart.
|
|
|
|
Skips when compose.lfs.yml is absent (gitea main — LFS not enabled in this deploy).
|
|
"""
|
|
if not _lfs_available():
|
|
import pytest
|
|
|
|
pytest.skip(
|
|
"compose.lfs.yml absent in gitea recipe checkout — LFS is not enabled on this branch. "
|
|
"This test runs on lfs-plain-gitea (PR #1) and is EXPECTED_NA on main."
|
|
)
|
|
|
|
user, password = admin_creds(live_app)
|
|
repo_name = "ci-lfs-test"
|
|
# Embed credentials directly in the URL (password is 32-char hex, URL-safe).
|
|
cred_url = f"https://{user}:{password}@{live_app}/{user}/{repo_name}.git"
|
|
git_env = {
|
|
"GIT_AUTHOR_NAME": "CI LFS Bot",
|
|
"GIT_AUTHOR_EMAIL": "ci@ci.local",
|
|
"GIT_COMMITTER_NAME": "CI LFS Bot",
|
|
"GIT_COMMITTER_EMAIL": "ci@ci.local",
|
|
"GIT_SSL_NO_VERIFY": "true",
|
|
"GIT_TERMINAL_PROMPT": "0",
|
|
}
|
|
|
|
# 1. Create LFS test repo
|
|
status, body = _api(
|
|
live_app,
|
|
"/user/repos",
|
|
method="POST",
|
|
body={"name": repo_name, "private": False, "auto_init": True, "default_branch": "main"},
|
|
user=user,
|
|
password=password,
|
|
)
|
|
assert status in (201, 409), f"repo create HTTP {status}: {body}"
|
|
|
|
tmpdir = tempfile.mkdtemp(prefix="ccci-gitea-lfs-")
|
|
try:
|
|
# 2. Clone repo
|
|
_run_git(["clone", cred_url, tmpdir], cwd="/tmp", env=git_env)
|
|
_run_git(["lfs", "install"], cwd=tmpdir, env=git_env)
|
|
|
|
# 3. Track *.bin as LFS
|
|
_run_git(["lfs", "track", "*.bin"], cwd=tmpdir, env=git_env)
|
|
_run_git(["add", ".gitattributes"], cwd=tmpdir, env=git_env)
|
|
|
|
# 4. Create a 1KB binary blob (content: random-looking but deterministic per test)
|
|
blob = bytes(range(256)) * 4 # 1024 bytes
|
|
blob_path = os.path.join(tmpdir, "testblob.bin")
|
|
with open(blob_path, "wb") as f:
|
|
f.write(blob)
|
|
expected_sha256 = hashlib.sha256(blob).hexdigest()
|
|
expected_oid = f"sha256:{expected_sha256}"
|
|
|
|
_run_git(["add", "testblob.bin"], cwd=tmpdir, env=git_env)
|
|
_run_git(["commit", "-m", "test: add LFS blob"], cwd=tmpdir, env=git_env)
|
|
|
|
# 5. Push (LFS upload happens here)
|
|
_run_git(["push", "origin", "HEAD:main"], cwd=tmpdir, env=git_env)
|
|
|
|
# Verify git-lfs pointer was tracked correctly
|
|
lfs_ls = subprocess.run(
|
|
["git", "lfs", "ls-files"],
|
|
cwd=tmpdir,
|
|
capture_output=True,
|
|
text=True,
|
|
env={**os.environ, **git_env},
|
|
)
|
|
assert (
|
|
"testblob.bin" in lfs_ls.stdout
|
|
), f"testblob.bin not in git-lfs ls-files: {lfs_ls.stdout}"
|
|
|
|
# 6. Download in a FRESH clone (proves the LFS server stores and serves the object)
|
|
fresh_dir = tempfile.mkdtemp(prefix="ccci-gitea-lfs-dl-")
|
|
try:
|
|
_run_git(["clone", cred_url, fresh_dir], cwd="/tmp", env=git_env)
|
|
fetched_path = os.path.join(fresh_dir, "testblob.bin")
|
|
assert os.path.exists(fetched_path), "testblob.bin not fetched in fresh clone"
|
|
with open(fetched_path, "rb") as f:
|
|
fetched = f.read()
|
|
fetched_sha256 = hashlib.sha256(fetched).hexdigest()
|
|
assert (
|
|
fetched_sha256 == expected_sha256
|
|
), f"LFS round-trip OID mismatch: expected {expected_oid}, got sha256:{fetched_sha256}"
|
|
finally:
|
|
shutil.rmtree(fresh_dir, ignore_errors=True)
|
|
|
|
# 7. JWT-secret stability: restart gitea + assert LFS_JWT_SECRET unchanged.
|
|
# Read the current secret from inside the container's rendered app.ini.
|
|
current_jwt = lifecycle.exec_in_app(
|
|
live_app,
|
|
["sh", "-c", "grep -E '^LFS_JWT_SECRET' /etc/gitea/app.ini || echo NOT_FOUND"],
|
|
timeout=30,
|
|
).strip()
|
|
assert (
|
|
current_jwt and "NOT_FOUND" not in current_jwt
|
|
), "Could not read LFS_JWT_SECRET from /etc/gitea/app.ini before restart"
|
|
|
|
# Restart the gitea container
|
|
lifecycle.exec_in_app(live_app, ["true"], timeout=5) # no-op to confirm exec works
|
|
subprocess.run(
|
|
["docker", "service", "update", "--force", live_app.replace(".", "_") + "_app"],
|
|
capture_output=True,
|
|
timeout=120,
|
|
)
|
|
# Wait for gitea to come back up
|
|
|
|
# Re-read meta from the live_app fixture (meta is not in scope here — use the stored meta)
|
|
import time
|
|
|
|
deadline = time.time() + 120
|
|
while time.time() < deadline:
|
|
status2, _ = _api(live_app, "/version", user=user, password=password)
|
|
if status2 == 200:
|
|
break
|
|
time.sleep(5)
|
|
assert status2 == 200, "gitea did not come back up after restart"
|
|
|
|
jwt_after = lifecycle.exec_in_app(
|
|
live_app,
|
|
["sh", "-c", "grep -E '^LFS_JWT_SECRET' /etc/gitea/app.ini || echo NOT_FOUND"],
|
|
timeout=30,
|
|
).strip()
|
|
assert jwt_after == current_jwt, (
|
|
"LFS_JWT_SECRET changed after restart — the regression the PR fixes is still present: "
|
|
f"before={current_jwt!r} after={jwt_after!r}"
|
|
)
|
|
|
|
# 8. Verify a fresh clone still works after restart (tokens still validate)
|
|
post_restart_dir = tempfile.mkdtemp(prefix="ccci-gitea-lfs-restart-")
|
|
try:
|
|
_run_git(["clone", cred_url, post_restart_dir], cwd="/tmp", env=git_env)
|
|
pr_blob = os.path.join(post_restart_dir, "testblob.bin")
|
|
assert os.path.exists(pr_blob), "testblob.bin not fetched in post-restart clone"
|
|
with open(pr_blob, "rb") as f:
|
|
pr_data = f.read()
|
|
assert (
|
|
hashlib.sha256(pr_data).hexdigest() == expected_sha256
|
|
), "LFS object corrupted after restart — JWT secret may have changed"
|
|
finally:
|
|
shutil.rmtree(post_restart_dir, ignore_errors=True)
|
|
|
|
finally:
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
_api(live_app, f"/repos/{user}/{repo_name}", method="DELETE", user=user, password=password)
|