Skip to content

Commit bd8b57a

Browse files
Merge pull request #2 from robocode-dev/fix-ownership-authorization
Fix catalog ownership authorization
2 parents 4a8ada2 + e0a093f commit bd8b57a

2 files changed

Lines changed: 74 additions & 12 deletions

File tree

scripts/validate_bot.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,27 +139,35 @@ def check_governance(bots: list[Bot], root: Path, owner: str) -> None:
139139
disqualified_names = {entry["bot"] for entry in banned.get("disqualifiedBots", [])}
140140
if owner in banned_accounts:
141141
raise ValidationError(f"owner `{owner}` is banned from submissions")
142-
known_names: dict[str, str] = {name: record["ownerId"] for record in owners.get("owners", []) for name in record.get("bots", [])}
143142
catalog_entries = read_json(root / "bots" / "index.json").get("bots", []) if (root / "bots" / "index.json").exists() else []
144143
catalog_by_name = {entry["name"]: entry for entry in catalog_entries if entry.get("status") == "active"}
144+
submitted_bots = [
145+
bot
146+
for bot in bots
147+
if (previous := catalog_by_name.get(bot.name)) is None
148+
or previous.get("version") != bot.config["version"]
149+
or previous.get("sourceHash") != bot.source_hash
150+
]
151+
owner_by_bot = {name: record for record in owners.get("owners", []) for name in record.get("bots", [])}
145152
seen_skeletons: dict[str, str] = {}
146-
for bot in bots:
153+
for bot in submitted_bots:
147154
if bot.name in disqualified_names:
148155
raise ValidationError(f"bot `{bot.name}` is disqualified")
149156
bot_skeleton = skeleton(bot.name)
150157
previous = seen_skeletons.get(bot_skeleton)
151158
if previous is not None and previous != bot.name:
152159
raise ValidationError(f"bot `{bot.name}` is confusable with `{previous}`")
153160
seen_skeletons[bot_skeleton] = bot.name
154-
existing_owner = known_names.get(bot.name)
155-
if existing_owner is not None and existing_owner != owner:
156-
raise ValidationError(f"bot `{bot.name}` belongs to owner `{existing_owner}`")
161+
existing_owner = owner_by_bot.get(bot.name)
162+
if existing_owner is not None and owner not in existing_owner.get("accounts", []):
163+
raise ValidationError(f"bot `{bot.name}` belongs to owner `{existing_owner['ownerId']}`")
157164
previous = catalog_by_name.get(bot.name)
158165
if previous is not None and previous.get("version") == bot.config["version"] and previous.get("sourceHash") != bot.source_hash:
159166
raise ValidationError(f"bot `{bot.name}` changed source without increasing its version")
160167
active_by_owner: dict[str, int] = {}
161-
for bot in bots:
162-
active_by_owner[known_names.get(bot.name, owner)] = active_by_owner.get(known_names.get(bot.name, owner), 0) + 1
168+
for bot in submitted_bots:
169+
if bot.name not in owner_by_bot:
170+
active_by_owner[owner] = active_by_owner.get(owner, 0) + 1
163171
if active_by_owner.get(owner, 0) > 5:
164172
raise ValidationError(f"owner `{owner}` exceeds the five active bot slot limit")
165173

@@ -172,10 +180,28 @@ def generated_catalog(bots: list[Bot], root: Path, owner: str) -> tuple[dict[str
172180
for bot in bots:
173181
bot_owner = owner_by_bot.get(bot.name, owner)
174182
records.setdefault(bot_owner, []).append(bot.name)
175-
owners = {record["ownerId"]: set(record.get("bots", [])) for record in existing_owners.get("owners", [])}
183+
owners = {
184+
record["ownerId"]: {
185+
"accounts": record.get("accounts", []),
186+
"bots": set(record.get("bots", [])),
187+
}
188+
for record in existing_owners.get("owners", [])
189+
}
176190
for owner_id, names in records.items():
177-
owners.setdefault(owner_id, set()).update(names)
178-
owner_data = {"schemaVersion": 1, "owners": [{"ownerId": owner_id, "accounts": [owner_id], "bots": sorted(names), "activeSlots": len(names)} for owner_id, names in sorted(owners.items())]}
191+
owner_record = owners.setdefault(owner_id, {"accounts": [owner_id], "bots": set()})
192+
owner_record["bots"].update(names)
193+
owner_data = {
194+
"schemaVersion": 1,
195+
"owners": [
196+
{
197+
"ownerId": owner_id,
198+
"accounts": owner_record["accounts"],
199+
"bots": sorted(owner_record["bots"]),
200+
"activeSlots": len(owner_record["bots"]),
201+
}
202+
for owner_id, owner_record in sorted(owners.items())
203+
],
204+
}
179205
today = datetime.now(UTC).date().isoformat()
180206
current_by_name = {bot.name: bot for bot in bots}
181207
history = []

tests/test_validate_bot.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,20 @@ def setUp(self) -> None:
2121
def tearDown(self) -> None:
2222
self.temporary_directory.cleanup()
2323

24-
def run_validator(self, *arguments: str) -> subprocess.CompletedProcess[str]:
25-
return subprocess.run([sys.executable, str(VALIDATOR), "--root", str(self.root), "--owner", "flemming-n-larsen", *arguments], text=True, capture_output=True, check=False)
24+
def run_validator(self, *arguments: str, owner: str = "flemming-n-larsen") -> subprocess.CompletedProcess[str]:
25+
return subprocess.run([sys.executable, str(VALIDATOR), "--root", str(self.root), "--owner", owner, *arguments], text=True, capture_output=True, check=False)
26+
27+
def add_bot(self, name: str) -> None:
28+
source = self.root / "bots" / "python" / "Orbit"
29+
destination = self.root / "bots" / "python" / name
30+
shutil.copytree(source, destination)
31+
(destination / "Orbit.sh").rename(destination / f"{name}.sh")
32+
(destination / "Orbit.cmd").rename(destination / f"{name}.cmd")
33+
config_path = destination / "Orbit.json"
34+
config = json.loads(config_path.read_text(encoding="utf-8"))
35+
config["name"] = name
36+
config_path.unlink()
37+
(destination / f"{name}.json").write_text(json.dumps(config), encoding="utf-8")
2638

2739
def test_valid_submission_generates_an_active_catalog_entry(self) -> None:
2840
result = self.run_validator("--smoke", "--generate")
@@ -58,6 +70,30 @@ def test_version_increase_supersedes_the_previous_catalog_entry(self) -> None:
5870
catalog = json.loads((self.root / "bots" / "index.json").read_text(encoding="utf-8"))
5971
self.assertEqual(["superseded", "active"], [entry["status"] for entry in catalog["bots"]])
6072

73+
def test_new_bot_from_another_owner_ignores_unchanged_catalog_entries(self) -> None:
74+
self.assertEqual(0, self.run_validator("--generate").returncode)
75+
self.add_bot("Nova")
76+
result = self.run_validator("--generate", owner="alice")
77+
self.assertEqual(0, result.returncode, result.stderr)
78+
catalog = json.loads((self.root / "bots" / "index.json").read_text(encoding="utf-8"))
79+
nova = next(entry for entry in catalog["bots"] if entry["name"] == "Nova")
80+
self.assertEqual("alice", nova["owner"])
81+
82+
def test_registered_secondary_account_can_update_and_is_preserved(self) -> None:
83+
owners_path = self.root / "bots" / "owners.json"
84+
owners = json.loads(owners_path.read_text(encoding="utf-8"))
85+
owners["owners"][0]["ownerId"] = "primary"
86+
owners["owners"][0]["accounts"] = ["primary", "secondary"]
87+
owners_path.write_text(json.dumps(owners), encoding="utf-8")
88+
config_path = self.root / "bots" / "python" / "Orbit" / "Orbit.json"
89+
config = json.loads(config_path.read_text(encoding="utf-8"))
90+
config["version"] = "1.0.3"
91+
config_path.write_text(json.dumps(config), encoding="utf-8")
92+
result = self.run_validator("--generate", owner="secondary")
93+
self.assertEqual(0, result.returncode, result.stderr)
94+
regenerated_owners = json.loads(owners_path.read_text(encoding="utf-8"))
95+
self.assertEqual(["primary", "secondary"], regenerated_owners["owners"][0]["accounts"])
96+
6197

6298
if __name__ == "__main__":
6399
unittest.main()

0 commit comments

Comments
 (0)