Skip to content

Commit

Permalink
ci: basic integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-hagemann committed Oct 5, 2023
1 parent 32ac5c9 commit 266e3cd
Show file tree
Hide file tree
Showing 11 changed files with 369 additions and 9 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/build-charm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,25 @@ jobs:
run: |
cd vm_operator
tox -e unit
integration-test:
name: Integration tests
runs-on: ubuntu-22.04
needs:
- lint
- unit-test
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup hosts for tests
run: |
echo "10.64.140.43 testing-ratings.foo.bar" | sudo tee -a /etc/hosts
- name: Setup operator environment
uses: charmed-kubernetes/actions-operator@main
with:
provider: lxd
juju-channel: 3.2/stable
- name: Run integration tests
run: |
cd vm_operator
tox -e integration -- --model=testing
1 change: 1 addition & 0 deletions vm_operator/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ parts:
charm:
charm-binary-python-packages:
- psycopg[binary]
- PyYAML
2 changes: 1 addition & 1 deletion vm_operator/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ options:
default: "https://github.com/matthew-hagemann/app-center-ratings"
squid-proxy-url:
type: string
default: "http://proxy.example.com"
default: ""
43 changes: 43 additions & 0 deletions vm_operator/lib/ratings_api/ratings_features_user_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

199 changes: 199 additions & 0 deletions vm_operator/lib/ratings_api/ratings_features_user_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
import ratings_features_user_pb2 as ratings__features__user__pb2


class UserStub(object):
"""Missing associated documentation comment in .proto file."""

def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.Authenticate = channel.unary_unary(
'/ratings.features.user.User/Authenticate',
request_serializer=ratings__features__user__pb2.AuthenticateRequest.SerializeToString,
response_deserializer=ratings__features__user__pb2.AuthenticateResponse.FromString,
)
self.Delete = channel.unary_unary(
'/ratings.features.user.User/Delete',
request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
)
self.Vote = channel.unary_unary(
'/ratings.features.user.User/Vote',
request_serializer=ratings__features__user__pb2.VoteRequest.SerializeToString,
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
)
self.ListMyVotes = channel.unary_unary(
'/ratings.features.user.User/ListMyVotes',
request_serializer=ratings__features__user__pb2.ListMyVotesRequest.SerializeToString,
response_deserializer=ratings__features__user__pb2.ListMyVotesResponse.FromString,
)
self.GetSnapVotes = channel.unary_unary(
'/ratings.features.user.User/GetSnapVotes',
request_serializer=ratings__features__user__pb2.GetSnapVotesRequest.SerializeToString,
response_deserializer=ratings__features__user__pb2.GetSnapVotesResponse.FromString,
)


class UserServicer(object):
"""Missing associated documentation comment in .proto file."""

def Authenticate(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def Delete(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def Vote(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def ListMyVotes(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def GetSnapVotes(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')


def add_UserServicer_to_server(servicer, server):
rpc_method_handlers = {
'Authenticate': grpc.unary_unary_rpc_method_handler(
servicer.Authenticate,
request_deserializer=ratings__features__user__pb2.AuthenticateRequest.FromString,
response_serializer=ratings__features__user__pb2.AuthenticateResponse.SerializeToString,
),
'Delete': grpc.unary_unary_rpc_method_handler(
servicer.Delete,
request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
),
'Vote': grpc.unary_unary_rpc_method_handler(
servicer.Vote,
request_deserializer=ratings__features__user__pb2.VoteRequest.FromString,
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
),
'ListMyVotes': grpc.unary_unary_rpc_method_handler(
servicer.ListMyVotes,
request_deserializer=ratings__features__user__pb2.ListMyVotesRequest.FromString,
response_serializer=ratings__features__user__pb2.ListMyVotesResponse.SerializeToString,
),
'GetSnapVotes': grpc.unary_unary_rpc_method_handler(
servicer.GetSnapVotes,
request_deserializer=ratings__features__user__pb2.GetSnapVotesRequest.FromString,
response_serializer=ratings__features__user__pb2.GetSnapVotesResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'ratings.features.user.User', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))


# This class is part of an EXPERIMENTAL API.
class User(object):
"""Missing associated documentation comment in .proto file."""

@staticmethod
def Authenticate(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/Authenticate',
ratings__features__user__pb2.AuthenticateRequest.SerializeToString,
ratings__features__user__pb2.AuthenticateResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

@staticmethod
def Delete(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/Delete',
google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
google_dot_protobuf_dot_empty__pb2.Empty.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

@staticmethod
def Vote(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/Vote',
ratings__features__user__pb2.VoteRequest.SerializeToString,
google_dot_protobuf_dot_empty__pb2.Empty.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

@staticmethod
def ListMyVotes(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/ListMyVotes',
ratings__features__user__pb2.ListMyVotesRequest.SerializeToString,
ratings__features__user__pb2.ListMyVotesResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

@staticmethod
def GetSnapVotes(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/GetSnapVotes',
ratings__features__user__pb2.GetSnapVotesRequest.SerializeToString,
ratings__features__user__pb2.GetSnapVotesResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
5 changes: 5 additions & 0 deletions vm_operator/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
UNIT_PATH = Path("/etc/systemd/system/ratings.service")
CARGO_PATH = Path(environ.get("HOME", "/root")) / ".cargo/bin/cargo"
APP_PORT = 443
APP_NAME = "ratings"
APP_HOST = "0.0.0.0"


class RatingsCharm(ops.CharmBase):
Expand Down Expand Up @@ -94,8 +96,11 @@ def _render_systemd_unit(self):
rendered = template.render(
project_root=APP_PATH,
app_env=self.config["app-env"],
app_host=APP_HOST,
app_jwt_secret=jwt_secret,
app_log_level=self.config["app-log-level"],
app_name=APP_NAME,
app_port=APP_PORT,
app_postgres_uri=connection_string,
app_migration_postgres_uri=connection_string,
)
Expand Down
3 changes: 3 additions & 0 deletions vm_operator/templates/ratings-service.j2
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ After=network.target

[Service]
Environment="APP_ENV={{ app_env }}"
Environment="APP_HOST={{ app_host }}"
Environment="APP_JWT_SECRET={{ app_jwt_secret }}"
Environment="APP_LOG_LEVEL={{ app_log_level }}"
Environment="APP_NAME={{ app_name }}"
Environment="APP_PORT={{ app_port }}"
Environment="APP_POSTGRES_URI={{ app_postgres_uri }}"
Environment="APP_MIGRATION_POSTGRES_URI={{ app_migration_postgres_uri }}"
WorkingDirectory = {{ project_root }}
Expand Down
12 changes: 12 additions & 0 deletions vm_operator/tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2021 Canonical Ltd.
# See LICENSE file for licensing details.

from pytest import fixture
from pytest_operator.plugin import OpsTest


@fixture(scope="module")
async def ratings_charm(ops_test: OpsTest):
"""Ratings charm used for integration testing."""
charm = await ops_test.build_charm(".")
return charm
63 changes: 63 additions & 0 deletions vm_operator/tests/integration/test_charm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.

import asyncio
import secrets

import grpc
import ratings_api.ratings_features_user_pb2 as pb2
import ratings_api.ratings_features_user_pb2_grpc as pb2_grpc
from pytest import mark
from pytest_operator.plugin import OpsTest

RATINGS = "ratings"
UNIT_0 = f"{RATINGS}/0"
DB = "db"


@mark.abort_on_fail
@mark.skip_if_deployed
async def test_deploy(ops_test: OpsTest, ratings_charm):
await ops_test.model.deploy(await ratings_charm, application_name=RATINGS)
# issuing dummy update_status just to trigger an event
async with ops_test.fast_forward():
await ops_test.model.wait_for_idle(apps=[RATINGS], status="active", timeout=1000)
assert ops_test.model.applications[RATINGS].units[0].workload_status == "active"


@mark.abort_on_fail
async def test_database_relation(ops_test: OpsTest):
"""Test that the charm can be successfully related to PostgreSQL."""
await asyncio.gather(
ops_test.model.deploy("postgresql", channel="edge", application_name=DB, trust=True),
ops_test.model.wait_for_idle(
apps=[DB], status="active", raise_on_blocked=True, timeout=1000
),
)

await asyncio.gather(
ops_test.model.integrate(RATINGS, DB),
ops_test.model.wait_for_idle(
apps=[RATINGS], status="active", raise_on_blocked=True, timeout=1000
),
)


@mark.abort_on_fail
async def test_ratings_register_user(ops_test: OpsTest):
"""End-to-end test to ensure the app can interact with the database."""
# Introduce a wait (e.g., 300 seconds or 5 minutes)
status = await ops_test.model.get_status() # noqa: F821
unit = list(status.applications[RATINGS].units)[0]
print(f"Connecting to address: {status}")
address = status["applications"][RATINGS]["units"][unit]["public-address"]
print(f"Connecting to address: {address}")
connection_string = f"{address}:443"

channel = grpc.insecure_channel(connection_string)
stub = pb2_grpc.UserStub(channel)
message = pb2.AuthenticateRequest(id=secrets.token_hex(32))
print(f"Message sent: {message}")
response = stub.Authenticate(message)
assert response.token
Loading

0 comments on commit 266e3cd

Please sign in to comment.