diff --git a/backend/.dockerignore b/backend/.dockerignore
index 667375e7..5b8d1eac 100644
--- a/backend/.dockerignore
+++ b/backend/.dockerignore
@@ -39,6 +39,7 @@ var/
.installed.cfg
*.egg
.mypy_cache
+.pytest_cache
# PyInstaller
# Usually these files are written by a python script from a template
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index b32d2412..b41d3f68 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -34,7 +34,7 @@ metadata = "true"
tagged-metadata = "true"
[tool.ruff]
-select = ["E", "F", "B", "N", "UP", "ANN", "C4", "T20", "SIM", "ARG", "ERA", "PLE"]
+select = ["E", "F", "B", "N", "UP", "ANN", "C4", "T20", "SIM", "ARG", "ERA", "PLE", "TID252"]
ignore = ["ANN201", "ANN003", "N999", "ANN101", "ANN204"]
[tool.ruff.flake8-bugbear]
diff --git a/backend/test_observer/controllers/artefacts/artefacts.py b/backend/test_observer/controllers/artefacts/artefacts.py
index 4b4c4494..ccb75c91 100644
--- a/backend/test_observer/controllers/artefacts/artefacts.py
+++ b/backend/test_observer/controllers/artefacts/artefacts.py
@@ -15,191 +15,26 @@
# along with this program. If not, see .
#
# Written by:
-# Nadzeya Hutsko
# Omar Selo
-
-
-import logging
+# Nadzeya Hutsko
from fastapi import APIRouter, Depends
-from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
-
-from test_observer.data_access.repository import (
- get_stage_by_name,
- get_artefacts_by_family_name,
-)
-from test_observer.data_access.models import Artefact
-from test_observer.data_access.models_enums import FamilyName
+from test_observer.data_access.models import ArtefactBuild
from test_observer.data_access.setup import get_db
-from test_observer.external_apis.snapcraft import (
- get_channel_map_from_snapcraft,
-)
-from test_observer.external_apis.archive import ArchiveManager
-
-router = APIRouter()
-
-logger = logging.getLogger("test-observer-backend")
-
-CHANNEL_PROMOTION_MAP = {
- # channel -> next-channel
- "edge": "beta",
- "beta": "candidate",
- "candidate": "stable",
- "stable": "stable",
-}
-
-REPOSITORY_PROMOTION_MAP = {
- # repository -> next-repository
- "proposed": "updates",
- "updates": "updates",
-}
+from .models import ArtefactBuildDTO
-@router.put("/promote")
-def promote_artefacts(db: Session = Depends(get_db)):
- """
- Promote all the artefacts in all the families if it has been updated on the
- external source
- """
- try:
- processed_artefacts = promoter_controller(db)
- logger.info("INFO: Processed artefacts %s", processed_artefacts)
- if False in processed_artefacts.values():
- return JSONResponse(
- status_code=500,
- content={
- "detail": (
- "Got some errors while processing the next artefacts: "
- ", ".join(
- [k for k, v in processed_artefacts.items() if v is False]
- )
- )
- },
- )
- return JSONResponse(
- status_code=200,
- content={"detail": "All the artefacts have been processed successfully"},
- )
- except Exception as exc:
- return JSONResponse(status_code=500, content={"detail": str(exc)})
-
-
-def promoter_controller(session: Session) -> dict:
- """
- Orchestrate the snap promoter job
-
- :session: DB connection session
- :return: dict with the processed cards and the status of execution
- """
- family_mapping = {
- FamilyName.SNAP: run_snap_promoter,
- FamilyName.DEB: run_deb_promoter,
- }
- for family_name, promoter_function in family_mapping.items():
- artefacts = get_artefacts_by_family_name(session, family_name)
- processed_artefacts = {}
- for artefact in artefacts:
- try:
- processed_artefacts[
- f"{family_name} - {artefact.name} - {artefact.version}"
- ] = True
- promoter_function(session, artefact)
- except Exception as exc:
- processed_artefacts[
- f"{family_name} - {artefact.name} - {artefact.version}"
- ] = False
- logger.warning("WARNING: %s", str(exc), exc_info=True)
- return processed_artefacts
-
-
-def run_snap_promoter(session: Session, artefact: Artefact) -> None:
- """
- Check snap artefacts state and move/archive them if necessary
-
- :session: DB connection session
- :artefact_build: an ArtefactBuild object
- """
- for build in artefact.builds:
- arch = build.architecture
- channel_map = get_channel_map_from_snapcraft(
- arch=arch,
- snapstore=artefact.source["store"],
- snap_name=artefact.name,
- )
- track = artefact.source.get("track", "latest")
-
- for channel_info in channel_map:
- if not (
- channel_info.channel.track == track
- and channel_info.channel.architecture == arch
- ):
- continue
-
- risk = channel_info.channel.risk
- try:
- version = channel_info.version
- revision = channel_info.revision
- except KeyError as exc:
- logger.warning(
- "No key '%s' is found. Continue processing...",
- str(exc),
- )
- continue
-
- next_risk = CHANNEL_PROMOTION_MAP[artefact.stage.name]
- if (
- risk == next_risk != artefact.stage.name.lower()
- and version == artefact.version
- and revision == build.revision
- ):
- logger.info("Move artefact '%s' to the '%s' stage", artefact, next_risk)
- stage = get_stage_by_name(
- session, stage_name=next_risk, family=artefact.stage.family
- )
- if stage:
- artefact.stage = stage
- session.commit()
- # The artefact was promoted, so we're done
- return
+router = APIRouter()
-def run_deb_promoter(session: Session, artefact: Artefact) -> None:
- """
- Check deb artefacts state and move/archive them if necessary
- :session: DB connection session
- :artefact: an Artefact object
- """
- for build in artefact.builds:
- arch = build.architecture
- for repo in REPOSITORY_PROMOTION_MAP:
- with ArchiveManager(
- arch=arch,
- series=artefact.source["series"],
- pocket=repo,
- apt_repo=artefact.source["repo"],
- ) as archivemanager:
- deb_version = archivemanager.get_deb_version(artefact.name)
- if deb_version is None:
- logger.error(
- "Cannot find deb_version with deb %s in package data",
- artefact.name,
- )
- continue
- next_repo = REPOSITORY_PROMOTION_MAP.get(artefact.stage.name)
- logger.debug(
- "Artefact version: %s, deb version: %s", artefact.version, deb_version
- )
- if (
- repo == next_repo != artefact.stage.name
- and deb_version == artefact.version
- ):
- logger.info("Move artefact '%s' to the '%s' stage", artefact, next_repo)
- stage = get_stage_by_name(
- session, stage_name=next_repo, family=artefact.stage.family
- )
- if stage:
- artefact.stage = stage
- session.commit()
- # The artefact was promoted, so we're done
- return
+@router.get("/{artefact_id}/builds", response_model=list[ArtefactBuildDTO])
+def get_artefact_builds(artefact_id: int, db: Session = Depends(get_db)):
+ """Get latest artefact builds of an artefact together with their test executions"""
+ return (
+ db.query(ArtefactBuild)
+ .filter(ArtefactBuild.artefact_id == artefact_id)
+ .distinct(ArtefactBuild.architecture)
+ .order_by(ArtefactBuild.architecture, ArtefactBuild.revision.desc())
+ .all()
+ )
diff --git a/backend/test_observer/controllers/artefacts/models.py b/backend/test_observer/controllers/artefacts/models.py
new file mode 100644
index 00000000..2a89263d
--- /dev/null
+++ b/backend/test_observer/controllers/artefacts/models.py
@@ -0,0 +1,48 @@
+# Copyright 2023 Canonical Ltd.
+# All rights reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# Written by:
+# Omar Selo
+# Nadzeya Hutsko
+from pydantic import BaseModel
+
+
+class EnvironmentDTO(BaseModel):
+ id: int
+ name: str
+ architecture: str
+
+ class Config:
+ orm_mode = True
+
+
+class TestExecutionDTO(BaseModel):
+ id: int
+ jenkins_link: str | None
+ c3_link: str | None
+ environment: EnvironmentDTO
+
+ class Config:
+ orm_mode = True
+
+
+class ArtefactBuildDTO(BaseModel):
+ id: int
+ revision: int | None
+ test_executions: list[TestExecutionDTO]
+
+ class Config:
+ orm_mode = True
diff --git a/backend/tests/controllers/artefacts/__init__.py b/backend/test_observer/controllers/promoter/__init__.py
similarity index 100%
rename from backend/tests/controllers/artefacts/__init__.py
rename to backend/test_observer/controllers/promoter/__init__.py
diff --git a/backend/test_observer/controllers/promoter/promoter.py b/backend/test_observer/controllers/promoter/promoter.py
new file mode 100644
index 00000000..695eaf1e
--- /dev/null
+++ b/backend/test_observer/controllers/promoter/promoter.py
@@ -0,0 +1,205 @@
+# Copyright 2023 Canonical Ltd.
+# All rights reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# Written by:
+# Nadzeya Hutsko
+# Omar Selo
+
+
+import logging
+from fastapi import APIRouter, Depends
+from fastapi.responses import JSONResponse
+from sqlalchemy.orm import Session
+
+from test_observer.data_access.repository import (
+ get_stage_by_name,
+ get_artefacts_by_family_name,
+)
+from test_observer.data_access.models import Artefact
+from test_observer.data_access.models_enums import FamilyName
+from test_observer.data_access.setup import get_db
+from test_observer.external_apis.snapcraft import (
+ get_channel_map_from_snapcraft,
+)
+from test_observer.external_apis.archive import ArchiveManager
+
+router = APIRouter()
+
+logger = logging.getLogger("test-observer-backend")
+
+CHANNEL_PROMOTION_MAP = {
+ # channel -> next-channel
+ "edge": "beta",
+ "beta": "candidate",
+ "candidate": "stable",
+ "stable": "stable",
+}
+
+REPOSITORY_PROMOTION_MAP = {
+ # repository -> next-repository
+ "proposed": "updates",
+ "updates": "updates",
+}
+
+
+@router.put("/v0/artefacts/promote")
+def promote_artefacts(db: Session = Depends(get_db)):
+ """
+ Promote all the artefacts in all the families if it has been updated on the
+ external source
+ """
+ try:
+ processed_artefacts = promoter_controller(db)
+ logger.info("INFO: Processed artefacts %s", processed_artefacts)
+ if False in processed_artefacts.values():
+ return JSONResponse(
+ status_code=500,
+ content={
+ "detail": (
+ "Got some errors while processing the next artefacts: "
+ ", ".join(
+ [k for k, v in processed_artefacts.items() if v is False]
+ )
+ )
+ },
+ )
+ return JSONResponse(
+ status_code=200,
+ content={"detail": "All the artefacts have been processed successfully"},
+ )
+ except Exception as exc:
+ return JSONResponse(status_code=500, content={"detail": str(exc)})
+
+
+def promoter_controller(session: Session) -> dict:
+ """
+ Orchestrate the snap promoter job
+
+ :session: DB connection session
+ :return: dict with the processed cards and the status of execution
+ """
+ family_mapping = {
+ FamilyName.SNAP: run_snap_promoter,
+ FamilyName.DEB: run_deb_promoter,
+ }
+ for family_name, promoter_function in family_mapping.items():
+ artefacts = get_artefacts_by_family_name(session, family_name)
+ processed_artefacts = {}
+ for artefact in artefacts:
+ try:
+ processed_artefacts[
+ f"{family_name} - {artefact.name} - {artefact.version}"
+ ] = True
+ promoter_function(session, artefact)
+ except Exception as exc:
+ processed_artefacts[
+ f"{family_name} - {artefact.name} - {artefact.version}"
+ ] = False
+ logger.warning("WARNING: %s", str(exc), exc_info=True)
+ return processed_artefacts
+
+
+def run_snap_promoter(session: Session, artefact: Artefact) -> None:
+ """
+ Check snap artefacts state and move/archive them if necessary
+
+ :session: DB connection session
+ :artefact_build: an ArtefactBuild object
+ """
+ for build in artefact.builds:
+ arch = build.architecture
+ channel_map = get_channel_map_from_snapcraft(
+ arch=arch,
+ snapstore=artefact.source["store"],
+ snap_name=artefact.name,
+ )
+ track = artefact.source.get("track", "latest")
+
+ for channel_info in channel_map:
+ if not (
+ channel_info.channel.track == track
+ and channel_info.channel.architecture == arch
+ ):
+ continue
+
+ risk = channel_info.channel.risk
+ try:
+ version = channel_info.version
+ revision = channel_info.revision
+ except KeyError as exc:
+ logger.warning(
+ "No key '%s' is found. Continue processing...",
+ str(exc),
+ )
+ continue
+
+ next_risk = CHANNEL_PROMOTION_MAP[artefact.stage.name]
+ if (
+ risk == next_risk != artefact.stage.name.lower()
+ and version == artefact.version
+ and revision == build.revision
+ ):
+ logger.info("Move artefact '%s' to the '%s' stage", artefact, next_risk)
+ stage = get_stage_by_name(
+ session, stage_name=next_risk, family=artefact.stage.family
+ )
+ if stage:
+ artefact.stage = stage
+ session.commit()
+ # The artefact was promoted, so we're done
+ return
+
+
+def run_deb_promoter(session: Session, artefact: Artefact) -> None:
+ """
+ Check deb artefacts state and move/archive them if necessary
+
+ :session: DB connection session
+ :artefact: an Artefact object
+ """
+ for build in artefact.builds:
+ arch = build.architecture
+ for repo in REPOSITORY_PROMOTION_MAP:
+ with ArchiveManager(
+ arch=arch,
+ series=artefact.source["series"],
+ pocket=repo,
+ apt_repo=artefact.source["repo"],
+ ) as archivemanager:
+ deb_version = archivemanager.get_deb_version(artefact.name)
+ if deb_version is None:
+ logger.error(
+ "Cannot find deb_version with deb %s in package data",
+ artefact.name,
+ )
+ continue
+ next_repo = REPOSITORY_PROMOTION_MAP.get(artefact.stage.name)
+ logger.debug(
+ "Artefact version: %s, deb version: %s", artefact.version, deb_version
+ )
+ if (
+ repo == next_repo != artefact.stage.name
+ and deb_version == artefact.version
+ ):
+ logger.info("Move artefact '%s' to the '%s' stage", artefact, next_repo)
+ stage = get_stage_by_name(
+ session, stage_name=next_repo, family=artefact.stage.family
+ )
+ if stage:
+ artefact.stage = stage
+ session.commit()
+ # The artefact was promoted, so we're done
+ return
diff --git a/backend/test_observer/controllers/router.py b/backend/test_observer/controllers/router.py
index 60848046..474279fd 100644
--- a/backend/test_observer/controllers/router.py
+++ b/backend/test_observer/controllers/router.py
@@ -1,20 +1,42 @@
+# Copyright 2023 Canonical Ltd.
+# All rights reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# Written by:
+# Omar Selo
+# Nadzeya Hutsko
from fastapi import APIRouter, Depends
from sqlalchemy import text
from sqlalchemy.orm import Session
+
from test_observer.data_access.setup import get_db
from .application import version
from .artefacts import artefacts
from .families import families
+from .promoter import promoter
from .test_execution import test_execution
from .test_executions import test_executions
router = APIRouter()
-router.include_router(artefacts.router, prefix="/v0/artefacts")
+router.include_router(promoter.router)
router.include_router(test_execution.router, prefix="/v1/test-execution")
router.include_router(families.router, prefix="/v1/families")
router.include_router(version.router, prefix="/v1/version")
router.include_router(test_executions.router, prefix="/v1/test-executions")
+router.include_router(artefacts.router, prefix="/v1/artefacts")
@router.get("/")
diff --git a/backend/test_observer/data_access/repository.py b/backend/test_observer/data_access/repository.py
index b0eb75f6..d0f0824c 100644
--- a/backend/test_observer/data_access/repository.py
+++ b/backend/test_observer/data_access/repository.py
@@ -20,9 +20,9 @@
"""Services for working with objects from DB"""
-from sqlalchemy import func, and_
+from sqlalchemy import and_, func
from sqlalchemy.dialects.postgresql import insert
-from sqlalchemy.orm import joinedload, Session
+from sqlalchemy.orm import Session, joinedload
from .models_enums import FamilyName
diff --git a/backend/tests/controllers/artefacts/test_artefacts.py b/backend/tests/controllers/artefacts/test_artefacts.py
index 7667d160..d67b5018 100644
--- a/backend/tests/controllers/artefacts/test_artefacts.py
+++ b/backend/tests/controllers/artefacts/test_artefacts.py
@@ -15,128 +15,68 @@
# along with this program. If not, see .
#
# Written by:
-# Nadzeya Hutsko
# Omar Selo
-"""Test snap manager API"""
-
-
-from datetime import datetime, timedelta
-
+# Nadzeya Hutsko
from fastapi.testclient import TestClient
-from requests_mock import Mocker
from sqlalchemy.orm import Session
+from test_observer.data_access.models import ArtefactBuild, Environment, TestExecution
-from ...helpers import create_artefact, create_artefact_builds
+from tests.helpers import create_artefact, create_artefact_builds
-def test_run_to_move_artefact_snap(
- db_session: Session, test_client: TestClient, requests_mock: Mocker
-):
- """
- If artefact's current stage name is different to its stage name on
- snapcraft, the artefact is moved to the next stage
- """
- # Arrange
- artefact = create_artefact(
- db_session,
- "edge",
- name="core20",
- version="1.1.1",
- source={"store": "ubuntu"},
- created_at=datetime.utcnow(),
+def test_get_artefact_builds(db_session: Session, test_client: TestClient):
+ artefact = create_artefact(db_session, "beta")
+ artefact_build = create_artefact_builds(db_session, artefact, 1)[0]
+ environment = Environment(
+ name="some-environment", architecture=artefact_build.architecture
)
- create_artefact_builds(db_session, artefact)
- create_artefact(
- db_session,
- "edge",
- name="core20",
- version="1.1.0",
- source={"store": "ubuntu"},
- created_at=datetime.utcnow() - timedelta(days=1),
- )
- requests_mock.get(
- "https://api.snapcraft.io/v2/snaps/info/core20",
- json={
- "channel-map": [
- {
- "channel": {
- "architecture": artefact.builds[0].architecture,
- "name": "beta",
- "released-at": "2023-05-17T12:39:07.471800+00:00",
- "risk": "beta",
- "track": "latest",
- },
- "created-at": "2023-04-10T09:59:22.309277+00:00",
- "download": {
- "deltas": [],
- "sha3-384": "70f0",
- "size": 130830336,
- "url": "https://api.snapcraft.io/api/v1/snaps/download/...",
- },
- "revision": artefact.builds[0].revision,
- "type": "app",
- "version": "1.1.1",
- },
- ]
- },
+ test_execution = TestExecution(
+ artefact_build=artefact_build, environment=environment
)
+ db_session.add_all([environment, test_execution])
+ db_session.commit()
- # Act
- test_client.put("/v0/artefacts/promote")
+ response = test_client.get(f"/v1/artefacts/{artefact.id}/builds")
- db_session.refresh(artefact)
-
- # Assert
- assert artefact.stage.name == "beta"
+ assert response.status_code == 200
+ assert response.json() == [
+ {
+ "id": artefact_build.id,
+ "revision": artefact_build.revision,
+ "test_executions": [
+ {
+ "id": test_execution.id,
+ "jenkins_link": test_execution.jenkins_link,
+ "c3_link": test_execution.c3_link,
+ "environment": {
+ "id": environment.id,
+ "name": environment.name,
+ "architecture": environment.architecture,
+ },
+ }
+ ],
+ }
+ ]
-def test_run_to_move_artefact_deb(
- db_session: Session, test_client: TestClient, requests_mock: Mocker
-):
- """
- If artefact's current stage name is different to its stage name on
- deb archive, the artefact is moved to the next stage
- """
- # Arrange
- artefact = create_artefact(
- db_session,
- "proposed",
- name="linux-generic",
- version="5.19.0.43.39",
- source={"series": "kinetic", "repo": "main"},
- created_at=datetime.utcnow(),
+def test_get_artefact_builds_only_latest(db_session: Session, test_client: TestClient):
+ artefact = create_artefact(db_session, "beta")
+ artefact_build1 = ArtefactBuild(
+ architecture="amd64", revision="1", artefact=artefact
)
- create_artefact_builds(db_session, artefact)
- create_artefact(
- db_session,
- "proposed",
- name="linux-generic",
- version="5.19.0.43.38",
- source={"series": "kinetic", "repo": "main"},
- created_at=datetime.utcnow() - timedelta(days=1),
+ artefact_build2 = ArtefactBuild(
+ architecture="amd64", revision="2", artefact=artefact
)
-
- with open("tests/test_data/Packages-proposed.gz", "rb") as f:
- proposed_content = f.read()
- with open("tests/test_data/Packages-updates.gz", "rb") as f:
- updates_content = f.read()
-
- for build in artefact.builds:
- requests_mock.get(
- "http://us.archive.ubuntu.com/ubuntu/dists/kinetic-proposed/main/"
- f"binary-{build.architecture}/Packages.gz",
- content=proposed_content,
- )
- requests_mock.get(
- "http://us.archive.ubuntu.com/ubuntu/dists/kinetic-updates/main/"
- f"binary-{build.architecture}/Packages.gz",
- content=updates_content,
- )
-
- # Act
- test_client.put("/v0/artefacts/promote")
-
- db_session.refresh(artefact)
-
- # Assert
- assert artefact.stage.name == "updates"
+ db_session.add_all([artefact_build1, artefact_build2])
+ db_session.commit()
+
+ response = test_client.get(f"/v1/artefacts/{artefact.id}/builds")
+
+ assert response.status_code == 200
+ assert response.json() == [
+ {
+ "id": artefact_build2.id,
+ "revision": artefact_build2.revision,
+ "test_executions": [],
+ }
+ ]
diff --git a/backend/tests/controllers/promoter/__init__.py b/backend/tests/controllers/promoter/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/tests/controllers/promoter/test_promoter.py b/backend/tests/controllers/promoter/test_promoter.py
new file mode 100644
index 00000000..bb90c175
--- /dev/null
+++ b/backend/tests/controllers/promoter/test_promoter.py
@@ -0,0 +1,142 @@
+# Copyright 2023 Canonical Ltd.
+# All rights reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# Written by:
+# Nadzeya Hutsko
+# Omar Selo
+"""Test promoter API"""
+
+
+from datetime import datetime, timedelta
+
+from fastapi.testclient import TestClient
+from requests_mock import Mocker
+from sqlalchemy.orm import Session
+
+from tests.helpers import create_artefact, create_artefact_builds
+
+
+def test_run_to_move_artefact_snap(
+ db_session: Session, test_client: TestClient, requests_mock: Mocker
+):
+ """
+ If artefact's current stage name is different to its stage name on
+ snapcraft, the artefact is moved to the next stage
+ """
+ # Arrange
+ artefact = create_artefact(
+ db_session,
+ "edge",
+ name="core20",
+ version="1.1.1",
+ source={"store": "ubuntu"},
+ created_at=datetime.utcnow(),
+ )
+ create_artefact_builds(db_session, artefact)
+ create_artefact(
+ db_session,
+ "edge",
+ name="core20",
+ version="1.1.0",
+ source={"store": "ubuntu"},
+ created_at=datetime.utcnow() - timedelta(days=1),
+ )
+ requests_mock.get(
+ "https://api.snapcraft.io/v2/snaps/info/core20",
+ json={
+ "channel-map": [
+ {
+ "channel": {
+ "architecture": artefact.builds[0].architecture,
+ "name": "beta",
+ "released-at": "2023-05-17T12:39:07.471800+00:00",
+ "risk": "beta",
+ "track": "latest",
+ },
+ "created-at": "2023-04-10T09:59:22.309277+00:00",
+ "download": {
+ "deltas": [],
+ "sha3-384": "70f0",
+ "size": 130830336,
+ "url": "https://api.snapcraft.io/api/v1/snaps/download/...",
+ },
+ "revision": artefact.builds[0].revision,
+ "type": "app",
+ "version": "1.1.1",
+ },
+ ]
+ },
+ )
+
+ # Act
+ test_client.put("/v0/artefacts/promote")
+
+ db_session.refresh(artefact)
+
+ # Assert
+ assert artefact.stage.name == "beta"
+
+
+def test_run_to_move_artefact_deb(
+ db_session: Session, test_client: TestClient, requests_mock: Mocker
+):
+ """
+ If artefact's current stage name is different to its stage name on
+ deb archive, the artefact is moved to the next stage
+ """
+ # Arrange
+ artefact = create_artefact(
+ db_session,
+ "proposed",
+ name="linux-generic",
+ version="5.19.0.43.39",
+ source={"series": "kinetic", "repo": "main"},
+ created_at=datetime.utcnow(),
+ )
+ create_artefact_builds(db_session, artefact)
+ create_artefact(
+ db_session,
+ "proposed",
+ name="linux-generic",
+ version="5.19.0.43.38",
+ source={"series": "kinetic", "repo": "main"},
+ created_at=datetime.utcnow() - timedelta(days=1),
+ )
+
+ with open("tests/test_data/Packages-proposed.gz", "rb") as f:
+ proposed_content = f.read()
+ with open("tests/test_data/Packages-updates.gz", "rb") as f:
+ updates_content = f.read()
+
+ for build in artefact.builds:
+ requests_mock.get(
+ "http://us.archive.ubuntu.com/ubuntu/dists/kinetic-proposed/main/"
+ f"binary-{build.architecture}/Packages.gz",
+ content=proposed_content,
+ )
+ requests_mock.get(
+ "http://us.archive.ubuntu.com/ubuntu/dists/kinetic-updates/main/"
+ f"binary-{build.architecture}/Packages.gz",
+ content=updates_content,
+ )
+
+ # Act
+ test_client.put("/v0/artefacts/promote")
+
+ db_session.refresh(artefact)
+
+ # Assert
+ assert artefact.stage.name == "updates"
diff --git a/backend/tests/data_access/test_repository.py b/backend/tests/data_access/test_repository.py
index 73ef9110..56267b99 100644
--- a/backend/tests/data_access/test_repository.py
+++ b/backend/tests/data_access/test_repository.py
@@ -31,7 +31,7 @@
get_stage_by_name,
)
-from ..helpers import create_artefact
+from tests.helpers import create_artefact
def test_get_stage_by_name(db_session: Session):
diff --git a/backend/tests/helpers.py b/backend/tests/helpers.py
index 4443e034..6dbbae76 100644
--- a/backend/tests/helpers.py
+++ b/backend/tests/helpers.py
@@ -23,7 +23,7 @@ def create_artefact(db_session: Session, stage_name: str, **kwargs) -> Artefact:
def create_artefact_builds(
db_session: Session, artefact: Artefact, num_builds: int = 5
-):
+) -> list[ArtefactBuild]:
"""
Create a number of ArtefactBuild instances for an Artefact and add them
to the database