Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions api/teams/teams_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,12 @@ def remove_team(team_id):
if hackathon_event_id:
hackathon_db_id = get_hackathon_by_event_id(hackathon_event_id)["id"]
event_collection = db.collection("hackathons").document(hackathon_db_id)
event_collection_dict = event_collection.get().to_dict()

# Remove the team from the hackathon event
event_collection_dict = event_collection.get().to_dict() or {}

# Remove the team from the hackathon event (tolerate docs with no
# `teams` key — same KeyError class as the queue_team linking bug)
new_teams = []
for t in event_collection_dict["teams"]:
for t in event_collection_dict.get("teams") or []:
if t != team_doc:
new_teams.append(t)

Expand Down Expand Up @@ -381,6 +382,25 @@ def edit_team(json):
"team_id": team_id
}


def _append_team_to_hackathon(db, hackathon_db_id, team_ref):
"""Link a team DocumentReference into hackathons/{id}.teams[].

Hackathon docs created through the admin UI may have no ``teams`` key at
all (not even an empty list); indexing it directly raised KeyError after
the team doc was already inserted, leaving the team orphaned from its
event (Sep 2026, test event fall-2026). Idempotent: an already-linked
team is not appended twice.
"""
event_ref = db.collection("hackathons").document(hackathon_db_id)
event_dict = event_ref.get().to_dict() or {}
teams = list(event_dict.get("teams") or [])
existing_ids = {getattr(t, "id", None) for t in teams}
if getattr(team_ref, "id", None) not in existing_ids:
teams.append(team_ref)
event_ref.set({"teams": teams}, merge=True)
return teams

def queue_team(propel_user_id, json):
"""
Create a team and queue it for nonprofit pairing
Expand Down Expand Up @@ -539,17 +559,7 @@ def queue_team(propel_user_id, json):

# Link the team to the hackathon event
hackathon_db_id = get_hackathon_by_event_id(hackathon_event_id)["id"]
event_collection = db.collection("hackathons").document(hackathon_db_id)
event_collection_dict = event_collection.get().to_dict()

new_teams = []
for t in event_collection_dict["teams"]:
new_teams.append(t)
new_teams.append(new_team_doc)

event_collection.set({
"teams": new_teams
}, merge=True)
_append_team_to_hackathon(db, hackathon_db_id, new_team_doc)

# Clear the cache
logger.info("Clearing cache for event_id=%s doc_id=%s",
Expand Down
77 changes: 77 additions & 0 deletions api/teams/tests/test_hackathon_team_linking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
Regression tests for the KeyError: 'teams' crash in team creation.

Hackathon docs created through the admin UI can lack a ``teams`` key
entirely. queue_team inserted the team doc, then did
``event_collection_dict["teams"]`` and blew up, leaving the new team
orphaned from its event (seen on test.ohack.dev with event fall-2026,
Sep 2026). The linking now goes through _append_team_to_hackathon, which
tolerates a missing key / missing doc and is idempotent.
"""
import os
from unittest.mock import MagicMock

os.environ.setdefault("ENVIRONMENT", "test")

import api.teams.teams_service as svc


def _db_with_hackathon(existing):
snapshot = MagicMock()
snapshot.to_dict.return_value = existing
event_ref = MagicMock()
event_ref.get.return_value = snapshot
collection = MagicMock()
collection.document.return_value = event_ref
db = MagicMock()
db.collection.return_value = collection
return db, event_ref


def _ref(doc_id):
r = MagicMock()
r.id = doc_id
return r


def test_links_team_when_hackathon_has_no_teams_key():
db, event_ref = _db_with_hackathon({"event_id": "fall-2026", "title": "ASU Fall"})
team = _ref("team-1")

result = svc._append_team_to_hackathon(db, "hack-doc", team)

assert result == [team]
written, kwargs = event_ref.set.call_args
assert written[0] == {"teams": [team]}
assert kwargs.get("merge") is True


def test_appends_to_existing_teams_list():
existing_team = _ref("team-0")
db, event_ref = _db_with_hackathon({"teams": [existing_team]})
team = _ref("team-1")

result = svc._append_team_to_hackathon(db, "hack-doc", team)

assert result == [existing_team, team]
assert event_ref.set.call_args[0][0]["teams"] == [existing_team, team]


def test_does_not_duplicate_an_already_linked_team():
team = _ref("team-1")
db, event_ref = _db_with_hackathon({"teams": [team]})

result = svc._append_team_to_hackathon(db, "hack-doc", team)

assert result == [team]
event_ref.set.assert_not_called()


def test_tolerates_missing_hackathon_snapshot():
db, event_ref = _db_with_hackathon(None)
team = _ref("team-1")

result = svc._append_team_to_hackathon(db, "hack-doc", team)

assert result == [team]
assert event_ref.set.call_args[0][0] == {"teams": [team]}
Loading