Skip to content

Commit

Permalink
chore: Implement TUF Interface and RSTUF Service
Browse files Browse the repository at this point in the history
- Implements the tuf/interfaces.py with ITUFService
- Implements the tuf/services.py with RSTUFService
- Refactor the tuf/tasks.py metadata_update to use the Service
- Add unit tests

Signed-off-by: Kairo de Araujo <[email protected]>
  • Loading branch information
kairoaraujo committed Oct 7, 2024
1 parent 422de27 commit 86803ac
Show file tree
Hide file tree
Showing 9 changed files with 452 additions and 216 deletions.
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
from warehouse.packaging.interfaces import IProjectService
from warehouse.subscriptions import services as subscription_services
from warehouse.subscriptions.interfaces import IBillingService, ISubscriptionService
from warehouse.tuf import services as tuf_services
from warehouse.tuf.interfaces import ITUFService

from .common.db import Session
from .common.db.accounts import EmailFactory, UserFactory
Expand Down Expand Up @@ -179,6 +181,7 @@ def pyramid_services(
integrity_service,
macaroon_service,
helpdesk_service,
tuf_service,
):
services = _Services()

Expand All @@ -201,6 +204,7 @@ def pyramid_services(
services.register_service(integrity_service, IIntegrityService, None)
services.register_service(macaroon_service, IMacaroonService, None, name="")
services.register_service(helpdesk_service, IHelpDeskService, None)
services.register_service(tuf_service, ITUFService, None)

return services

Expand Down Expand Up @@ -611,6 +615,11 @@ def helpdesk_service():
return helpdesk_services.ConsoleHelpDeskService()


@pytest.fixture
def tuf_service(db_session):
return tuf_services.RSTUFService(db_session)


class QueryRecorder:
def __init__(self):
self.queries = []
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/tuf/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pretend

from warehouse import tuf
from warehouse.tuf.interfaces import ITUFService
from warehouse.tuf.services import rstuf_factory


def test_includeme():
config = pretend.stub(
register_service_factory=pretend.call_recorder(
lambda factory, iface, name=None: None
),
maybe_dotted=pretend.call_recorder(lambda *a: "http://rstuf"),
)

tuf.includeme(config)

assert config.register_service_factory.calls == [
pretend.call(rstuf_factory, ITUFService),
]
267 changes: 267 additions & 0 deletions tests/unit/tuf/test_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pretend
import pytest

from requests import Session
from zope.interface.verify import verifyClass

from warehouse.tuf import services
from warehouse.tuf.interfaces import ITUFService
from warehouse.tuf.services import RSTUFService


class TestRSTUFService:

def test_verify_service(self):
assert verifyClass(ITUFService, RSTUFService)

def basic_init(self, db_request):
db_request.registry.settings = {"rstuf.api_url": "http://rstuf"}

rstuf = RSTUFService(db_request)

assert rstuf is not None
assert rstuf.api_url == "http://rstuf"
assert isinstance(rstuf.requests, Session)

def test_create_service(self, db_request):
db_request.registry.settings = {"rstuf.api_url": "http://rstuf"}

rstuf = RSTUFService.create_service(db_request)

assert rstuf is not None
assert rstuf.api_url == "http://rstuf"
assert isinstance(rstuf.requests, Session)

def test_get_task_state(self, monkeypatch, db_request):
db_request.registry.settings = {"rstuf.api_url": "http://rstuf"}

response = pretend.stub(
raise_for_status=pretend.call_recorder(lambda: None),
json=pretend.call_recorder(lambda: {"data": {"state": "SUCCESS"}}),
)
test_session = pretend.stub(
get=pretend.call_recorder(lambda *a, **kw: response)
)
fake_session = pretend.call_recorder(lambda: test_session)
monkeypatch.setattr(services, "Session", fake_session)

rstuf = RSTUFService.create_service(db_request)

state = rstuf.get_task_state("123456")

assert state == "SUCCESS"

assert test_session.get.calls == [
pretend.call("http://rstuf/api/v1/task?task_id=123456"),
]
assert response.raise_for_status.calls == [pretend.call()]
assert response.json.calls == [pretend.call()]

def test_post_artifacts(self, monkeypatch, db_request):
db_request.registry.settings = {"rstuf.api_url": "http://rstuf"}

response = pretend.stub(
raise_for_status=pretend.call_recorder(lambda: None),
json=pretend.call_recorder(lambda: {"data": {"task_id": "123456"}}),
)
test_session = pretend.stub(
post=pretend.call_recorder(lambda *a, **kw: response)
)
fake_session = pretend.call_recorder(lambda: test_session)
monkeypatch.setattr(services, "Session", fake_session)

rstuf = RSTUFService.create_service(db_request)

task_id = rstuf.post_artifacts({"targets": [{"path": "name"}]})

assert task_id == "123456"

assert test_session.post.calls == [
pretend.call(
"http://rstuf/api/v1/artifacts", json={"targets": [{"path": "name"}]}
),
]
assert response.raise_for_status.calls == [pretend.call()]
assert response.json.calls == [pretend.call()]

def test_post_artifacts_no_data_from_rstuf(self, monkeypatch, db_request):
db_request.registry.settings = {"rstuf.api_url": "http://rstuf"}

response = pretend.stub(
raise_for_status=pretend.call_recorder(lambda: None),
json=pretend.call_recorder(lambda: {"data": None}),
)
test_session = pretend.stub(
post=pretend.call_recorder(lambda *a, **kw: response)
)
fake_session = pretend.call_recorder(lambda: test_session)
monkeypatch.setattr(services, "Session", fake_session)

rstuf = RSTUFService.create_service(db_request)

with pytest.raises(services.RSTUFError) as e:
rstuf.post_artifacts({"targets": [{"path": "name"}]})

assert "Error in RSTUF job: {'data': None}" in str(e)

assert fake_session.calls == [pretend.call()]
assert test_session.post.calls == [
pretend.call(
"http://rstuf/api/v1/artifacts", json={"targets": [{"path": "name"}]}
),
]
assert response.raise_for_status.calls == [pretend.call()]
assert response.json.calls == [pretend.call()]

@pytest.mark.parametrize(
("states", "exception", "message"),
[
(
[
{"data": {"state": "PENDING"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "RECEIVED"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "SUCCESS"}},
],
None,
"",
),
(
[
{"data": {"state": "PENDING"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "RECEIVED"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "FAILURE"}},
],
services.RSTUFError,
"RSTUF job failed, please check payload and retry",
),
(
[
{"data": {"state": "PENDING"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "RECEIVED"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "ERRORED"}},
],
services.RSTUFError,
"RSTUF internal problem, please check RSTUF health",
),
(
[
{"data": {"state": "PENDING"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "RECEIVED"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "REVOKED"}},
],
services.RSTUFError,
"RSTUF internal problem, please check RSTUF health",
),
(
[
{"data": {"state": "PENDING"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "RECEIVED"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "REJECTED"}},
],
services.RSTUFError,
"RSTUF internal problem, please check RSTUF health",
),
(
[
{"data": {"state": "PENDING"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "RECEIVED"}},
{"data": {"state": "STARTED"}},
{"data": {"state": "INVALID_STATE"}},
],
services.RSTUFError,
"RSTUF job returned unexpected state: INVALID_STATE",
),
(
[
{"data": {"state": "PENDING"}},
{"data": {"state": "PENDING"}},
{"data": {"state": "PENDING"}},
{"data": {"state": "PENDING"}},
{"data": {"state": "PENDING"}},
],
services.RSTUFError,
"RSTUF job failed, please check payload and retry",
),
],
)
def test_wait_for_pending_than_success(
self, monkeypatch, db_request, states, exception, message
):
db_request.registry.settings = {"rstuf.api_url": "http://rstuf"}

# generate iter of responses
responses = iter(states)
response = pretend.stub(
raise_for_status=pretend.call_recorder(lambda: None),
json=pretend.call_recorder(lambda: next(responses)),
)
test_session = pretend.stub(
get=pretend.call_recorder(lambda *a, **kw: response)
)
fake_session = pretend.call_recorder(lambda: test_session)
monkeypatch.setattr(services, "Session", fake_session)

rstuf = RSTUFService.create_service(db_request)
rstuf.delay = 0.1 # speed up the test
if message == "RSTUF job failed, please check payload and retry":
rstuf.retries = 5 # simulate failure by limiting retries

result = None
if exception is not None:
with pytest.raises(exception) as e:
rstuf.wait_for_success("123456")

assert message in str(e)
else:
result = rstuf.wait_for_success("123456")

assert result is None

assert test_session.get.calls == [
pretend.call("http://rstuf/api/v1/task?task_id=123456"),
pretend.call("http://rstuf/api/v1/task?task_id=123456"),
pretend.call("http://rstuf/api/v1/task?task_id=123456"),
pretend.call("http://rstuf/api/v1/task?task_id=123456"),
pretend.call("http://rstuf/api/v1/task?task_id=123456"),
]
assert response.raise_for_status.calls == [
pretend.call(),
pretend.call(),
pretend.call(),
pretend.call(),
pretend.call(),
]
assert response.json.calls == [
pretend.call(),
pretend.call(),
pretend.call(),
pretend.call(),
pretend.call(),
]

def test_rstuf_factory(self):
rstuf = services.rstuf_factory(pretend.stub(), pretend.stub())

assert isinstance(rstuf, RSTUFService)
15 changes: 7 additions & 8 deletions tests/unit/tuf/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,16 @@ def test_update_metadata(self, db_request, monkeypatch):
render = call_recorder(lambda *a, **kw: (index_digest, None, index_size))
tuf.tasks.render_simple_detail = render

post = call_recorder(lambda *a: self.task_id)
monkeypatch.setattr(tuf.tasks, "post_artifacts", post)

wait = call_recorder(lambda *a: None)
monkeypatch.setattr(tuf.tasks, "wait_for_success", wait)
rstuf = tuf.services.RSTUFService.create_service(db_request)
rstuf.post_artifacts = call_recorder(lambda *a: self.task_id)
rstuf.wait_for_success = call_recorder(lambda *a: None)
db_request.find_service = call_recorder(lambda *a, **kw: rstuf)

tuf.tasks.update_metadata(db_request, project_id)
assert one.calls == [call()]
assert render.calls == [call(project, db_request, store=True)]
assert post.calls == [
assert rstuf.post_artifacts.calls == [
call(
rstuf_url,
{
"targets": [
{
Expand All @@ -60,7 +58,7 @@ def test_update_metadata(self, db_request, monkeypatch):
},
)
]
assert wait.calls == [call(rstuf_url, self.task_id)]
assert rstuf.wait_for_success.calls == [call(self.task_id)]

def test_update_metadata_no_rstuf_api_url(self, db_request):
project_id = "id"
Expand All @@ -73,6 +71,7 @@ def test_update_metadata_no_rstuf_api_url(self, db_request):

# Test early return, if no RSTUF API URL configured
db_request.registry.settings = {"rstuf.api_url": None}

tuf.tasks.update_metadata(db_request, project_id)

assert not one.calls
Loading

0 comments on commit 86803ac

Please sign in to comment.