2990c3c903
Three leftovers from the manifest refactor: 1. provision/cred_proxy.py:223 referenced u.kind == 'gitea' for the tea login count — kind was removed from the runtime class, so any bottle with a tea-login route raised AttributeError at provision time. Switch to `'tea-login' in r.roles`. 2. The runtime class CredProxyUpstream is renamed to CredProxyRoute (its data is a route on the proxy, not an "upstream"; the field route.upstream is the upstream URL). Module's own naming now aligns with manifest.CredProxyRoute and routes.json. 3. cred_proxy_upstreams_for_bottle -> cred_proxy_routes_for_bottle; CredProxyPlan.upstreams -> CredProxyPlan.routes; local `upstreams` collections become `routes`. Callers in backend.py, launch.py, prepare.py, bottle_plan.py, provision/cred_proxy.py, and tests updated. Also strips lingering `bottle.tokens` references from docstrings (pipelock.py, cred_proxy.py prepare(), manifest._parse_https_host, test_pipelock_allowlist.py module doc) and removes dead helpers from the integration test (the _bottle helper used a tokens field that no longer parses).
162 lines
6.3 KiB
Python
162 lines
6.3 KiB
Python
"""Unit: cred-proxy agent-side provisioner renderers (PRD 0010).
|
|
|
|
The docker cp / docker exec side effects are exercised by integration
|
|
tests; these unit tests cover the pure render functions."""
|
|
|
|
import unittest
|
|
|
|
from claude_bottle.backend.docker.provision.cred_proxy import (
|
|
render_cred_proxy_gitconfig,
|
|
render_npmrc,
|
|
render_tea_config,
|
|
)
|
|
from claude_bottle.cred_proxy import cred_proxy_routes_for_bottle
|
|
from claude_bottle.manifest import Manifest
|
|
|
|
|
|
def _bottle(routes):
|
|
return Manifest.from_json_obj({
|
|
"bottles": {"dev": {"cred_proxy": {"routes": routes}}},
|
|
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
|
}).bottles["dev"]
|
|
|
|
|
|
def _upstreams(routes):
|
|
return cred_proxy_routes_for_bottle(_bottle(routes))
|
|
|
|
|
|
class TestRenderNpmrc(unittest.TestCase):
|
|
def test_empty_when_no_role(self):
|
|
self.assertEqual("", render_npmrc(_upstreams([])))
|
|
self.assertEqual("", render_npmrc(_upstreams([
|
|
{"path": "/x/", "upstream": "https://x.example",
|
|
"auth_scheme": "Bearer", "token_ref": "T"},
|
|
])))
|
|
|
|
def test_writes_registry_line_for_npm_registry_role(self):
|
|
out = render_npmrc(_upstreams([
|
|
{"path": "/npm/", "upstream": "https://registry.npmjs.org",
|
|
"auth_scheme": "Bearer", "token_ref": "NPM_TOKEN",
|
|
"role": "npm-registry"},
|
|
]))
|
|
self.assertEqual("registry=http://cred-proxy:9099/npm/\n", out)
|
|
|
|
def test_omits_authtoken(self):
|
|
# The proxy injects Authorization at request time.
|
|
out = render_npmrc(_upstreams([
|
|
{"path": "/npm/", "upstream": "https://registry.npmjs.org",
|
|
"auth_scheme": "Bearer", "token_ref": "NPM_TOKEN",
|
|
"role": "npm-registry"},
|
|
]))
|
|
self.assertNotIn("_authToken", out)
|
|
self.assertNotIn("NPM_TOKEN", out)
|
|
|
|
|
|
class TestRenderGitconfig(unittest.TestCase):
|
|
def test_empty_when_no_role(self):
|
|
self.assertEqual("", render_cred_proxy_gitconfig(_upstreams([
|
|
{"path": "/anthropic/", "upstream": "https://api.anthropic.com",
|
|
"auth_scheme": "Bearer", "token_ref": "A"},
|
|
])))
|
|
|
|
def test_writes_insteadof_for_git_insteadof_role(self):
|
|
out = render_cred_proxy_gitconfig(_upstreams([
|
|
{"path": "/gh-git/", "upstream": "https://github.com",
|
|
"auth_scheme": "Bearer", "token_ref": "GH",
|
|
"role": "git-insteadof"},
|
|
]))
|
|
self.assertIn('[url "http://cred-proxy:9099/gh-git/"]', out)
|
|
self.assertIn("insteadOf = https://github.com/", out)
|
|
|
|
def test_gitea_writes_per_host_insteadof(self):
|
|
out = render_cred_proxy_gitconfig(_upstreams([
|
|
{"path": "/gitea/dideric/", "upstream": "https://gitea.dideric.is",
|
|
"auth_scheme": "token", "token_ref": "GITEA",
|
|
"role": "git-insteadof"},
|
|
]))
|
|
self.assertIn('[url "http://cred-proxy:9099/gitea/dideric/"]', out)
|
|
self.assertIn("insteadOf = https://gitea.dideric.is/", out)
|
|
|
|
def test_two_routes_yield_two_rules(self):
|
|
out = render_cred_proxy_gitconfig(_upstreams([
|
|
{"path": "/gh-git/", "upstream": "https://github.com",
|
|
"auth_scheme": "Bearer", "token_ref": "GH",
|
|
"role": "git-insteadof"},
|
|
{"path": "/gitea/x/", "upstream": "https://gitea.example.com",
|
|
"auth_scheme": "token", "token_ref": "GT",
|
|
"role": "git-insteadof"},
|
|
]))
|
|
self.assertEqual(2, out.count("insteadOf"))
|
|
self.assertIn("github.com", out)
|
|
self.assertIn("gitea.example.com", out)
|
|
|
|
def test_suppressed_when_git_gate_covers_host(self):
|
|
# When bottle.git brokers github.com over SSH, git-gate is the
|
|
# canonical git path. The cred-proxy https://github.com/
|
|
# rewrite would let the agent push over HTTPS — bypassing
|
|
# gitleaks. Suppress it.
|
|
out = render_cred_proxy_gitconfig(
|
|
_upstreams([
|
|
{"path": "/gh-git/", "upstream": "https://github.com",
|
|
"auth_scheme": "Bearer", "token_ref": "GH",
|
|
"role": "git-insteadof"},
|
|
]),
|
|
{"github.com"},
|
|
)
|
|
self.assertEqual("", out)
|
|
|
|
def test_partial_suppression_keeps_other_hosts(self):
|
|
out = render_cred_proxy_gitconfig(
|
|
_upstreams([
|
|
{"path": "/gitea/a/", "upstream": "https://gitea.dideric.is",
|
|
"auth_scheme": "token", "token_ref": "T1",
|
|
"role": "git-insteadof"},
|
|
{"path": "/gitea/b/", "upstream": "https://gitea.example.com",
|
|
"auth_scheme": "token", "token_ref": "T2",
|
|
"role": "git-insteadof"},
|
|
]),
|
|
{"gitea.dideric.is"},
|
|
)
|
|
self.assertNotIn("gitea.dideric.is/", out)
|
|
self.assertIn("gitea.example.com/", out)
|
|
|
|
|
|
class TestRenderTeaConfig(unittest.TestCase):
|
|
def test_empty_when_no_role(self):
|
|
self.assertEqual("", render_tea_config(_upstreams([
|
|
{"path": "/gh-git/", "upstream": "https://github.com",
|
|
"auth_scheme": "Bearer", "token_ref": "G"},
|
|
])))
|
|
|
|
def test_single_login_block(self):
|
|
out = render_tea_config(_upstreams([
|
|
{"path": "/gitea/dideric/", "upstream": "https://gitea.dideric.is",
|
|
"auth_scheme": "token", "token_ref": "GITEA",
|
|
"role": "tea-login"},
|
|
]))
|
|
self.assertIn("logins:", out)
|
|
# Login name comes from the upstream host, not the path —
|
|
# the path may not encode the host.
|
|
self.assertIn("- name: gitea.dideric.is", out)
|
|
self.assertIn("url: http://cred-proxy:9099/gitea/dideric/", out)
|
|
self.assertIn("token: cred-proxy-placeholder", out)
|
|
self.assertNotIn("GITEA", out)
|
|
|
|
|
|
class TestCombinedRoles(unittest.TestCase):
|
|
"""A single gitea route typically carries both `git-insteadof`
|
|
and `tea-login` — the renderers should each fire independently."""
|
|
|
|
def test_gitea_route_fires_both_renderers(self):
|
|
routes = _upstreams([
|
|
{"path": "/gitea/x/", "upstream": "https://gitea.example.com",
|
|
"auth_scheme": "token", "token_ref": "T",
|
|
"role": ["git-insteadof", "tea-login"]},
|
|
])
|
|
self.assertIn("insteadOf", render_cred_proxy_gitconfig(routes))
|
|
self.assertIn("logins:", render_tea_config(routes))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|