fix(epics): use actual group_id for save/delete operations on nested epics by JohnVillalovos · Pull Request #3279 · python-gitlab/python-gitlab · GitHub
Skip to content
Open
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
46 changes: 41 additions & 5 deletions gitlab/mixins.py
23 changes: 23 additions & 0 deletions gitlab/v4/objects/epics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import Any, TYPE_CHECKING

import gitlab.utils
from gitlab import exceptions as exc
from gitlab import types
Comment thread
JohnVillalovos marked this conversation as resolved.
from gitlab.base import RESTObject
Expand Down Expand Up @@ -29,6 +30,28 @@ class GroupEpic(ObjectDeleteMixin, SaveMixin, RESTObject):
resourcelabelevents: GroupEpicResourceLabelEventManager
notes: GroupEpicNoteManager

def _epic_path(self) -> str:
"""Return the API path for this epic using its real group."""
if self._lazy:
raise AttributeError(
"Cannot compute epic path for a lazy epic: attribute 'group_id' "
"is missing. Fetch the epic without lazy=True before saving or "
"deleting it."
)

try:
group_id = self._attrs["group_id"]
except KeyError as error:
raise AttributeError(
"Cannot compute epic path: attribute 'group_id' is missing."
) from error

encoded_group_id = gitlab.utils.EncodedId(group_id)
return f"/groups/{encoded_group_id}/epics/{self.encoded_id}"

def _get_custom_path(self) -> str | None:
return self._epic_path()


class GroupEpicManager(CRUDMixin[GroupEpic]):
_path = "/groups/{group_id}/epics"
Expand Down
60 changes: 60 additions & 0 deletions tests/functional/api/test_epics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import collections.abc
import dataclasses
import uuid

import pytest

import gitlab
import gitlab.v4.objects.epics
import gitlab.v4.objects.groups
from tests.functional import helpers

pytestmark = pytest.mark.gitlab_premium


Expand Down Expand Up @@ -32,3 +41,54 @@ def test_epic_notes(epic):
epic.notes.create({"body": "Test note"})
new_notes = epic.notes.list(get_all=True)
assert len(new_notes) == (len(notes) + 1), f"{new_notes} {notes}"


@dataclasses.dataclass(frozen=True)
class NestedEpicInSubgroup:
subgroup: gitlab.v4.objects.groups.Group
nested_epic: gitlab.v4.objects.epics.GroupEpic


@pytest.fixture
def nested_epic_in_subgroup(
gl: gitlab.Gitlab, group: gitlab.v4.objects.groups.Group
) -> collections.abc.Generator[NestedEpicInSubgroup, None, None]:
subgroup_id = uuid.uuid4().hex
subgroup = gl.groups.create(
{
"name": f"subgroup-{subgroup_id}",
"path": f"sg-{subgroup_id}",
"parent_id": group.id,
}
)

nested_epic = subgroup.epics.create(
Comment thread
JohnVillalovos marked this conversation as resolved.
{"title": f"Nested epic {subgroup_id}", "description": "Nested epic"}
)

try:
yield NestedEpicInSubgroup(subgroup=subgroup, nested_epic=nested_epic)
finally:
helpers.safe_delete(nested_epic)
helpers.safe_delete(subgroup)


def test_epic_save_from_parent_group_updates_subgroup_epic(
group: gitlab.v4.objects.groups.Group, nested_epic_in_subgroup: NestedEpicInSubgroup
) -> None:
fetched_epics = group.epics.list(search=nested_epic_in_subgroup.nested_epic.title)
assert fetched_epics, "Expected to discover nested epic via parent group list"

fetched_epic = fetched_epics[0]
assert (
fetched_epic.id == nested_epic_in_subgroup.nested_epic.id
), "Parent group listing did not include nested epic"

new_label = f"nested-{uuid.uuid4().hex}"
fetched_epic.labels = [new_label]
fetched_epic.save()

refreshed_epic = nested_epic_in_subgroup.subgroup.epics.get(
nested_epic_in_subgroup.nested_epic.iid
)
assert new_label in refreshed_epic.labels
91 changes: 91 additions & 0 deletions tests/unit/mixins/test_mixin_methods.py
Loading