diff --git a/README.md b/README.md index 7a5bec4d..ad231757 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,14 @@ This library enables you to manage Artifactory resources such as users, groups, + [Copy artifact to a new location](#copy-artifact-to-a-new-location) + [Move artifact to a new location](#move-artifact-to-a-new-location) + [Delete an artifact](#delete-an-artifact) + * [Builds](#builds) + + [Get a list of all builds](#get-a-list-of-all-builds) + + [Get the information about a build](#get-the-information-about-a-build) + + [Create build](#create-build) + + [Promote a build](#promote-a-build) + + [Delete one or more builds](#delete-one-or-more-builds) + + [Rename a build](#rename-a-build) + + [Get differences between two builds](#get-differences-between-two-builds) * [Contributing](#contributing) @@ -476,5 +484,65 @@ art.artifacts.delete("") ``` + +### Builds + +#### Get a list of all builds +```python +build_list: BuildListResponse = art.builds.list() +``` + +#### Get the information about a build + +```python +build_info: BuildInfo = art.builds.get_build_info("", "") +``` + +Note: optional BuildProperties can be used to query the correct build info of interest. + +```python +build_properties = BuildProperties(diff="") +build_info: BuildInfo = art.builds.get_build_info("", "", properties=build_properties) +# build_info contains diff between and +``` + + +```python +# started is the earliest build time to return +build_properties = BuildProperties(started="") +build_info: BuildInfo = art.builds.get_build_info("", "", properties=build_properties) +``` + +#### Create build + +```python +build_create_request = BuildCreateRequest(name="", number="", started="") +create_build = art.builds.create_build(build_create_request) +``` + + +#### Promote a build +```python +build_promote_request = BuildPromotionRequest(sourceRepo="", targetRepo="") +promote_build: BuildPromotionResult = art.builds.promote_build("", "", build_promote_request) +``` + +#### Delete one or more builds +```python +build_delete_request = BuildDeleteRequest(buildName="", buildNumbers=["", "", ...]) +art.builds.delete(build_delete_request) +``` + +#### Rename a build +```python +art.builds.build_rename("", "") +``` + +#### Get differences between two builds +```python +build_diffs: BuildDiffResponse = art.builds.build_diff("", "", "") +``` + + ### Contributing Please read the [Development - Contributing](./CONTRIBUTING.md) guidelines. diff --git a/pyartifactory/__init__.py b/pyartifactory/__init__.py index 35b21837..621e7f74 100644 --- a/pyartifactory/__init__.py +++ b/pyartifactory/__init__.py @@ -9,6 +9,7 @@ from pyartifactory.models.auth import AccessTokenModel from pyartifactory.objects.artifact import ArtifactoryArtifact from pyartifactory.objects.artifactory import Artifactory +from pyartifactory.objects.build import ArtifactoryBuild from pyartifactory.objects.group import ArtifactoryGroup from pyartifactory.objects.permission import ArtifactoryPermission from pyartifactory.objects.repository import ArtifactoryRepository @@ -24,6 +25,7 @@ "ArtifactoryRepository", "ArtifactorySecurity", "ArtifactoryUser", + "ArtifactoryBuild", ] with contextlib.suppress(PackageNotFoundError): diff --git a/pyartifactory/exception.py b/pyartifactory/exception.py index cd6457fd..086ed1e0 100644 --- a/pyartifactory/exception.py +++ b/pyartifactory/exception.py @@ -54,3 +54,7 @@ class PropertyNotFoundError(ArtifactoryError): class InvalidTokenDataError(ArtifactoryError): """The token contains invalid data.""" + + +class BuildNotFoundError(ArtifactoryError): + """Requested build were not found""" diff --git a/pyartifactory/models/__init__.py b/pyartifactory/models/__init__.py index f83cce4d..9d2bca85 100644 --- a/pyartifactory/models/__init__.py +++ b/pyartifactory/models/__init__.py @@ -13,6 +13,25 @@ ArtifactStatsResponse, ) from .auth import AccessTokenModel, ApiKeyModel, AuthModel, PasswordModel +from .build import ( + BuildAgent, + BuildArtifact, + BuildCreateRequest, + BuildDeleteRequest, + BuildDiffResponse, + BuildDiffResponseDetail, + BuildError, + BuildInfo, + BuildInfoDetail, + BuildListResponse, + BuildModules, + BuildPromotionRequest, + BuildPromotionResult, + BuildProperties, + BuildRun, + Run, + SimpleBuild, +) from .group import Group, SimpleGroup from .permission import Permission, PermissionV2, SimplePermission from .repository import ( @@ -70,4 +89,21 @@ "AnyRepositoryResponse", "AnyRepository", "AnyPermission", + "SimpleBuild", + "BuildListResponse", + "Run", + "BuildRun", + "BuildArtifact", + "BuildModules", + "BuildAgent", + "BuildInfoDetail", + "BuildInfo", + "BuildPromotionResult", + "BuildPromotionRequest", + "BuildProperties", + "BuildError", + "BuildDeleteRequest", + "BuildDiffResponseDetail", + "BuildDiffResponse", + "BuildCreateRequest", ] diff --git a/pyartifactory/models/build.py b/pyartifactory/models/build.py new file mode 100644 index 00000000..2fa08c58 --- /dev/null +++ b/pyartifactory/models/build.py @@ -0,0 +1,252 @@ +""" +Definition of all build models. +""" +from __future__ import annotations + +from typing import Dict, List, Optional + +from pydantic import BaseModel, Field + + +class BuildProperties(BaseModel): + started: Optional[str] = None + diff: Optional[str] = None + project: Optional[str] = None + + # Method to convert model to query string + def to_query_string(self) -> str: + # Create a list of key=value pairs for all non-None fields + properties = [f"{key}={value}" for key, value in self.model_dump(exclude_none=True).items()] + + return "?" + "&".join(properties) if properties else "" + + +class SimpleBuild(BaseModel): + """Models an artifactory single build.""" + + uri: str + lastStarted: str + + +class BuildListResponse(BaseModel): + """Models all artifactory builds.""" + + uri: str = "" + builds: Optional[List[SimpleBuild]] = None + + +class Run(BaseModel): + """Models an artifactory single build run.""" + + uri: str + started: str + + +class BuildRun(BaseModel): + """Models artifactory build runs.""" + + uri: str + buildsNumber: Optional[List[Run]] = None + + +class BuildArtifact(BaseModel): + """Models artifactory build artifact.""" + + type: str + sha1: str + sha256: str + md5: str + name: str + path: str + + +class BuildModules(BaseModel): + """Models artifactory's build modules.""" + + properties: Dict[str, str] + type: str + id: str + artifacts: List[BuildArtifact] + + +class BuildAgent(BaseModel): + name: str = "" + version: str = "" + + +class Vcs(BaseModel): + revision: Optional[str] = None + message: Optional[str] = None + branch: Optional[str] = None + url: Optional[str] = None + + +class BuildInfoDetail(BaseModel): + """Models artifactory buildInfo dict.""" + + properties: Optional[Dict[str, str]] = None + version: str = "" + name: str = "" + number: str = "" + buildAgent: BuildAgent = BuildAgent() + agent: BuildAgent = BuildAgent() + started: str = "" + durationMillis: int = 0 + artifactoryPrincipal: str = "" + vcs: Optional[List[Vcs]] = None + modules: List[BuildModules] = [] + + +class BuildInfo(BaseModel): + """Models artifactory build.""" + + uri: str = "" + buildInfo: BuildInfoDetail = BuildInfoDetail() + + +class BuildPromotionResult(BaseModel): + messages: List[Dict[str, str]] = [] + + +class BuildPromotionRequest(BaseModel): + status: str = "" + comment: str = "" + ciUser: str = "" + timestamp: str = "" + dryRun: bool = False + sourceRepo: str + targetRepo: str + copyArtifact: bool = Field(False, alias="copy") + artifacts: bool = True + dependencies: bool = False + scopes: List[str] = [] + properties: Dict[str, List[str]] = {} + failFast: bool = True + + +class BuildDeleteRequest(BaseModel): + project: str = "" + buildName: str + buildNumbers: List[str] + deleteArtifacts: bool = False + deleteAll: bool = False + + +class BuildDiffResponseDetail(BaseModel): + updated: List[str] = [] + unchanged: List[str] = [] + removed: List[str] = [] + new: List[str] = [] + + +class BuildDiffResponse(BaseModel): + artifacts: BuildDiffResponseDetail = BuildDiffResponseDetail() + dependencies: BuildDiffResponseDetail = BuildDiffResponseDetail() + properties: BuildDiffResponseDetail = BuildDiffResponseDetail() + + +class BuildErrorDetail(BaseModel): + status: int = 0 + message: str = "" + + +class BuildError(BaseModel): + errors: List[BuildErrorDetail] + + # Method to extract error message + def to_error_message(self) -> str: + return "\n".join([f"Error status: {_error.status} - {_error.message}" for _error in self.errors]) + + +class LicenseControl(BaseModel): + """Build's license.""" + + runChecks: Optional[bool] = None + includePublishedArtifacts: Optional[bool] = None + autoDiscover: Optional[bool] = None + scopesList: Optional[str] = None + licenseViolationsRecipientsList: Optional[str] = None + + +class BuildRetention(BaseModel): + """Build's retention.""" + + deleteBuildArtifacts: Optional[bool] = None + count: Optional[int] = None + minimumBuildDate: Optional[int] = None + buildNumbersNotToBeDiscarded: Optional[List[str]] = None + + +class Artifacts(BaseModel): + """Build's artifacts.""" + + type: Optional[str] = None + sha1: Optional[str] = None + md5: Optional[str] = None + name: Optional[str] = None + + +class Dependencies(BaseModel): + """Build's dependency.""" + + type: Optional[str] = None + sha1: Optional[str] = None + md5: Optional[str] = None + id: Optional[str] = None + scopes: Optional[List[str]] = None + + +class BuildSingleModule(BaseModel): + """Build's module model.""" + + properties: Optional[Dict[str, str]] = None + id: Optional[str] = None + artifacts: Optional[List[Artifacts]] = None + dependencies: Optional[List[Dependencies]] = None + + +class Tracker(BaseModel): + """Build's issue tracker.""" + + name: Optional[str] = None + version: Optional[str] = None + + +class AffectedIssue(BaseModel): + """Build's affected issue.""" + + key: Optional[str] = None + url: Optional[str] = None + summary: Optional[str] = None + aggregated: Optional[bool] = None + + +class Issue(BaseModel): + """Build's issue.""" + + tracker: Optional[Tracker] = None + aggregateBuildIssues: Optional[bool] = None + aggregationBuildStatus: Optional[str] = None + affectedIssues: Optional[List[AffectedIssue]] = None + + +class BuildCreateRequest(BaseModel): + """Models artifactory build creation data.""" + + properties: Dict[str, str] = {} + version: str = "1.0.1" + name: str + number: str + type: Optional[str] = None + buildAgent: Optional[BuildAgent] = None + agent: Optional[BuildAgent] = None + started: str + artifactoryPluginVersion: Optional[str] = None + durationMillis: Optional[int] = None + artifactoryPrincipal: Optional[str] = None + url: Optional[str] = None + vcs: Optional[List[Vcs]] = None + licenseControl: Optional[LicenseControl] = None + buildRetention: Optional[BuildRetention] = None + modules: Optional[List[BuildSingleModule]] = None + issues: Optional[Issue] = None diff --git a/pyartifactory/objects/artifactory.py b/pyartifactory/objects/artifactory.py index 11eedffa..14111998 100644 --- a/pyartifactory/objects/artifactory.py +++ b/pyartifactory/objects/artifactory.py @@ -6,6 +6,7 @@ from pyartifactory.models.auth import AuthModel from pyartifactory.objects.artifact import ArtifactoryArtifact +from pyartifactory.objects.build import ArtifactoryBuild from pyartifactory.objects.group import ArtifactoryGroup from pyartifactory.objects.permission import ArtifactoryPermission from pyartifactory.objects.repository import ArtifactoryRepository @@ -39,3 +40,4 @@ def __init__( self.repositories = ArtifactoryRepository(self.artifactory) self.artifacts = ArtifactoryArtifact(self.artifactory) self.permissions = ArtifactoryPermission(self.artifactory) + self.builds = ArtifactoryBuild(self.artifactory) diff --git a/pyartifactory/objects/build.py b/pyartifactory/objects/build.py new file mode 100644 index 00000000..68661aca --- /dev/null +++ b/pyartifactory/objects/build.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import logging +from typing import Union + +import requests +from requests import Response + +from pyartifactory.exception import ArtifactoryError, BuildNotFoundError +from pyartifactory.models.build import ( + BuildCreateRequest, + BuildDeleteRequest, + BuildDiffResponse, + BuildError, + BuildInfo, + BuildListResponse, + BuildPromotionRequest, + BuildPromotionResult, + BuildProperties, +) +from pyartifactory.objects.object import ArtifactoryObject + +logger = logging.getLogger("pyartifactory") + + +class ArtifactoryBuild(ArtifactoryObject): + """Models an artifactory build.""" + + _uri = "build" + + def get_build_info( + self, + build_name: str, + build_number: str, + properties: BuildProperties = BuildProperties(), + ) -> BuildInfo: + """ + :param build_name: Build name to be retrieved + :param build_number: Build number to be retrieved + :param properties: Build properties model, for admitted values + see https://jfrog.com/help/r/jfrog-rest-apis/build-info + :return: BuildInfo model object containing server response + """ + try: + response = self._get( + f"api/{self._uri}/{build_name}/{build_number}{properties.to_query_string()}", + ) + logger.debug("Build Info successfully retrieved") + except requests.exceptions.HTTPError as error: + self._raise_exception(error) + + return BuildInfo(**response.json()) + + def create_build(self, create_build_request: BuildCreateRequest) -> None: + try: + self.get_build_info(create_build_request.name, create_build_request.number) + except BuildNotFoundError: + # other exception from get_build_info are forwarded to caller. + try: + # build does not exist, can be created here + self._put(f"api/{self._uri}", json=create_build_request.model_dump()) + logging.debug( + "Build %s in %s successfully created", + create_build_request.number, + create_build_request.name, + ) + except requests.exceptions.HTTPError as error: + self._raise_exception(error) + else: + logger.error("Build %s in %s already exists", create_build_request.number, create_build_request.name) + raise ArtifactoryError(f"Build {create_build_request.number} in {create_build_request.name} already exists") + + def promote_build( + self, + build_name: str, + build_number: str, + promotion_request: BuildPromotionRequest, + ) -> BuildPromotionResult: + """ + :param build_name: Build name to be promoted + :param build_number: Build number to be promoted + :param promotion_request: Model object containing parameters for promotion + :return: BuildPromotionResponse containing server response + """ + try: + self._get( + f"api/{self._uri}/{build_name}/{build_number}", + ) + except requests.exceptions.HTTPError as error: + self._raise_exception(error) + else: + try: + response = self._post( + f"api/{self._uri}/promote/{build_name}/{build_number}", + json=promotion_request.model_dump(), + ) + logging.debug( + "Build %s in %s promoted from %s to %s", + build_number, + build_name, + promotion_request.sourceRepo, + promotion_request.targetRepo, + ) + except requests.exceptions.HTTPError as error: + self._raise_exception(error) + + return BuildPromotionResult(**response.json()) + + def list(self) -> BuildListResponse: + """ + :return: BuildListResponse model object containing server response + """ + response = self._get(f"api/{self._uri}") + logger.debug("List all builds successful") + return BuildListResponse.model_validate(response.json()) + + def delete(self, delete_build: BuildDeleteRequest) -> None: + """ + :param delete_build: Model object containing required parameters + :return: None + """ + try: + for _build_number in delete_build.buildNumbers: + self._get( + f"api/{self._uri}/{delete_build.buildName}/{_build_number}", + ) + # all build numbers exist + self._post(f"api/{self._uri}/delete", json=delete_build.model_dump()) + logger.debug("Builds %s deleted from %s", ",".join(delete_build.buildNumbers), delete_build.buildName) + except requests.exceptions.HTTPError as error: + self._raise_exception(error) + + def build_rename(self, build_name: str, new_build_name: str) -> None: + """ + :param build_name: Build to be renamed + :param new_build_name: New Build name + :return: None + """ + try: + self._get( + f"api/{self._uri}/{build_name}", + ) + except requests.exceptions.HTTPError as error: + self._raise_exception(error) + else: + try: + self._post(f"api/{self._uri}/rename/{build_name}?to={new_build_name}") + logger.debug("Build %s successfully renamed to %s", build_name, new_build_name) + except requests.exceptions.HTTPError as error: + self._raise_exception(error) + + def build_diff(self, build_name: str, build_number: str, older_build_number: str) -> BuildDiffResponse: + """ + :param build_name: Build name to be compared + :param build_number: More recent build to be compared + :param older_build_number: Starting build to be compared + :return: BuildDiffResponse model object containing server response + """ + try: + response = self._get( + f"api/{self._uri}/{build_name}/{build_number}?diff={older_build_number}", + ) + logger.debug("Build Diff successfully retrieved between %s and %s", build_number, older_build_number) + except requests.exceptions.HTTPError as error: + self._raise_exception(error) + + return BuildDiffResponse(**response.json()) + + def _raise_exception(self, error: requests.exceptions.HTTPError): + http_response: Union[Response, None] = error.response + if isinstance(http_response, Response): + _http_error = BuildError(**http_response.json()) + if http_response.status_code == 404: + raise BuildNotFoundError(_http_error.to_error_message()) from error + raise ArtifactoryError(_http_error.to_error_message()) from error + else: + raise ArtifactoryError from error diff --git a/tests/test_build.py b/tests/test_build.py new file mode 100644 index 00000000..f54744a8 --- /dev/null +++ b/tests/test_build.py @@ -0,0 +1,266 @@ +from __future__ import annotations + +import pytest +import requests +import responses +from requests.models import Response + +from pyartifactory import ArtifactoryBuild +from pyartifactory.exception import ArtifactoryError, BuildNotFoundError +from pyartifactory.models import ( + AuthModel, + BuildCreateRequest, + BuildDeleteRequest, + BuildDiffResponse, + BuildError, + BuildInfo, + BuildListResponse, + BuildPromotionRequest, + BuildPromotionResult, + BuildProperties, +) + +URL = "http://localhost:8080/artifactory" +AUTH = ("user", "password_or_apiKey") + +BUILD_INFO = BuildInfo(uri=f"{URL}/api/build/build_name/number") +BUILD_LIST_RESPONSE = BuildListResponse(uri=f"{URL}/api/build") +BUILD_NOT_FOUND_ERROR = BuildError(errors=[{"status": 404, "message": "Not found"}]) +BUILD_GENERIC_ERROR = BuildError(errors=[{"status": 500, "message": "Generic error"}]) +BUILD_DIFF = BuildDiffResponse() +BUILD_PROMOTION_REQUEST = BuildPromotionRequest(sourceRepo="repo-abc", targetRepo="repo-def") +BUILD_PROMOTION_RESPONSE = BuildPromotionResult() +BUILD_DELETE_REQUEST = BuildDeleteRequest(buildName="build", buildNumbers=["abc", "123"]) +BUILD_DELETE_ERROR = BuildError(errors=[{"status": 404, "message": "Not found"}]) +BUILD_CREATE_REQUEST = BuildCreateRequest(name="a-build", number="build-xx", started="2014-09-30T12:00:19.893+0300") + +NOT_FOUND_HTTP_RESPONSE = Response() +NOT_FOUND_HTTP_RESPONSE.status_code = 404 +NOT_FOUND_HTTP_RESPONSE.reason = "Not Found" +NOT_FOUND_HTTP_RESPONSE._content = b'{"errors": [{"status": 404, "message": "Not found ... "}]}' +NOT_FOUND_HTTP_RESPONSE.encoding = "utf-8" +NOT_FOUND_HTTP_RESPONSE.url = "http://jfrog-server..." + +NOT_FOUND_EXCEPTION_BODY = requests.exceptions.HTTPError(response=NOT_FOUND_HTTP_RESPONSE) + + +@responses.activate +def test_get_build_info_success(mocker): + responses.add( + responses.GET, + f"{URL}/api/build/build_name/abc", + json=BUILD_INFO.model_dump(), + status=200, + ) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "get_build_info") + get_build = artifactory_build.get_build_info("build_name", "abc") + + assert isinstance(get_build, BuildInfo) + assert get_build == BUILD_INFO + + +@responses.activate +@pytest.mark.parametrize( + "build_num,properties_input,expected_query,raised_exc", + [ + ("11", BuildProperties(), "", BuildNotFoundError), + ("11", BuildProperties(diff="43"), "?diff=43", BuildNotFoundError), + ("11", BuildProperties(diff="43", started="2024-08"), "?diff=43&started=2024-08", BuildNotFoundError), + ("11", BuildProperties(diff="abc"), "?diff=abc", ArtifactoryError), + ("def", BuildProperties(diff="22"), "?diff=22", ArtifactoryError), + ], +) +def test_get_build_info_errors(mocker, build_num, properties_input, expected_query, raised_exc): + responses.add( + responses.GET, + f"{URL}/api/build/build_name/{build_num}{expected_query}", + body=NOT_FOUND_EXCEPTION_BODY, + status=404, + ) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "get_build_info") + with pytest.raises(raised_exc): + artifactory_build.get_build_info("build_name", build_num, properties=properties_input) + + +@responses.activate +@pytest.mark.parametrize( + "error_model_dump,raised_exc", + [ + (BUILD_NOT_FOUND_ERROR.model_dump(), BuildNotFoundError), + (BUILD_GENERIC_ERROR.model_dump(), ArtifactoryError), + ], +) +def test_get_build_info_response_error(mocker, error_model_dump, raised_exc): + responses.add(responses.GET, f"{URL}/api/build/build_name/123", body=NOT_FOUND_EXCEPTION_BODY, status=200) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "get_build_info") + with pytest.raises(raised_exc): + artifactory_build.get_build_info("build_name", "123") + + +@responses.activate +def test_promote_build_success(mocker): + responses.add( + responses.GET, + f"{URL}/api/build/build_proj/build_number", + json=BUILD_INFO.model_dump(), + status=200, + ) + _promotion_request = BUILD_PROMOTION_REQUEST.model_dump() + responses.add( + responses.POST, + f"{URL}/api/build/promote/build_proj/build_number", + json=_promotion_request, + status=200, + ) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "promote_build") + build_promotion = artifactory_build.promote_build("build_proj", "build_number", BUILD_PROMOTION_REQUEST) + + assert isinstance(build_promotion, BuildPromotionResult) + assert build_promotion == BUILD_PROMOTION_RESPONSE + + +@responses.activate +def test_promote_build_errors(mocker): + responses.add( + responses.GET, + f"{URL}/api/build/build_name/build_123", + body=NOT_FOUND_EXCEPTION_BODY, + status=200, + ) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "promote_build") + with pytest.raises(BuildNotFoundError): + artifactory_build.promote_build("build_name", "build_123", BUILD_PROMOTION_REQUEST) + + +@responses.activate +def test_promote_build_error_not_exist(mocker): + responses.add(responses.GET, f"{URL}/api/build/build_name/123", body=NOT_FOUND_EXCEPTION_BODY, status=200) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "promote_build") + with pytest.raises(ArtifactoryError): + artifactory_build.promote_build("build_name", "123", BUILD_PROMOTION_REQUEST) + + +@responses.activate +def test_list_build(mocker): + responses.add( + responses.GET, + f"{URL}/api/build", + json=BUILD_LIST_RESPONSE.model_dump(), + status=200, + ) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "list") + build_list = artifactory_build.list() + + assert isinstance(build_list, BuildListResponse) + assert build_list == BUILD_LIST_RESPONSE + + +@responses.activate +def test_delete_build_success(mocker): + for _build_number in BUILD_DELETE_REQUEST.buildNumbers: + responses.add( + responses.GET, + f"{URL}/api/build/{BUILD_DELETE_REQUEST.buildName}/{_build_number}", + json=BUILD_INFO.model_dump(), + status=200, + ) + + responses.add(responses.POST, f"{URL}/api/build/delete", json=BUILD_DELETE_REQUEST.model_dump(), status=200) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "delete") + artifactory_build.delete(BUILD_DELETE_REQUEST) + + +@responses.activate +def test_delete_build_error_not_exist(mocker): + responses.add( + responses.GET, + f"{URL}/api/build/{BUILD_DELETE_REQUEST.buildName}/{BUILD_DELETE_REQUEST.buildNumbers[0]}", + json=BUILD_INFO.model_dump(), + status=200, + ) + responses.add( + responses.GET, + f"{URL}/api/build/{BUILD_DELETE_REQUEST.buildName}/{BUILD_DELETE_REQUEST.buildNumbers[-1]}", + body=NOT_FOUND_EXCEPTION_BODY, + status=200, + ) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "delete") + with pytest.raises(ArtifactoryError): + artifactory_build.delete(BUILD_DELETE_REQUEST) + + +@responses.activate +def test_rename_build_success(mocker): + responses.add(responses.GET, f"{URL}/api/build/build_name", json=BUILD_INFO.model_dump(), status=200) + responses.add(responses.POST, f"{URL}/api/build/rename/build_name?to=new_name", status=200) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "build_rename") + artifactory_build.build_rename("build_name", "new_name") + + +@responses.activate +def test_rename_build_error_not_exist(mocker): + responses.add(responses.GET, f"{URL}/api/build/build_name", json=BUILD_NOT_FOUND_ERROR.model_dump(), status=404) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "build_rename") + with pytest.raises(BuildNotFoundError): + artifactory_build.build_rename("build_name", "new_name") + + +@responses.activate +def test_build_diff_success(mocker): + responses.add(responses.GET, f"{URL}/api/build/build_name/123?diff=456", json=BUILD_DIFF.model_dump(), status=200) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "build_diff") + artifactory_build.build_diff("build_name", "123", "456") + + +@responses.activate +def test_create_build_success(mocker): + responses.add( + responses.GET, + f"{URL}/api/build/{BUILD_CREATE_REQUEST.name}/{BUILD_CREATE_REQUEST.number}", + body=NOT_FOUND_EXCEPTION_BODY, + status=200, + ) + responses.add(responses.PUT, f"{URL}/api/build", status=204) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "create_build") + artifactory_build.create_build(BUILD_CREATE_REQUEST) + + +@responses.activate +def test_create_build_error_already_exist(mocker): + responses.add( + responses.GET, + f"{URL}/api/build/{BUILD_CREATE_REQUEST.name}/{BUILD_CREATE_REQUEST.number}", + json=BUILD_INFO.model_dump(), + status=200, + ) + + artifactory_build = ArtifactoryBuild(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_build, "create_build") + with pytest.raises(ArtifactoryError): + artifactory_build.create_build(BUILD_CREATE_REQUEST)