Skip to content

Commit

Permalink
First try at an integration test
Browse files Browse the repository at this point in the history
  • Loading branch information
javierdelapuente committed Feb 28, 2024
1 parent c6c22bc commit 8a4262b
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 9 deletions.
2 changes: 2 additions & 0 deletions charm/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.
16 changes: 16 additions & 0 deletions charm/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""Fixtures for NetBox charm tests."""

from pytest import Parser

DJANGO_APP_IMAGE_PARAM = "--django-app-image"

def pytest_addoption(parser: Parser) -> None:
"""Parse additional pytest options.
Args:
parser: Pytest parser.
"""
parser.addoption(DJANGO_APP_IMAGE_PARAM, action="store", help="Django app image to be deployed")
113 changes: 113 additions & 0 deletions charm/tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""Fixtures for NetBox charm integration tests."""

import json

import pytest
import pytest_asyncio
from juju.action import Action
from juju.model import Model
from pytest_operator.plugin import OpsTest
from pytest import Config

from tests.conftest import DJANGO_APP_IMAGE_PARAM

@pytest_asyncio.fixture(scope="module", name="get_unit_ips")
async def get_unit_ips_fixture(ops_test: OpsTest):
"""Return an async function to retrieve unit ip addresses of a certain application."""

async def get_unit_ips(application_name: str):
"""Retrieve unit ip addresses of a certain application.
Args:
application_name: application name.
Returns:
a list containing unit ip addresses.
"""
_, status, _ = await ops_test.juju("status", "--format", "json")
status = json.loads(status)
units = status["applications"][application_name]["units"]
return tuple(
unit_status["address"]
for _, unit_status in sorted(units.items(), key=lambda kv: int(kv[0].split("/")[-1]))
)

return get_unit_ips

@pytest.fixture(scope="module", name="postgresql_app_name")
def postgresql_app_name_fixture() -> str:
"""Return the name of the postgresql application deployed for tests."""
return "postgresql-k8s"


# @pytest.mark.skip_if_deployed
@pytest_asyncio.fixture(scope="module", name="postgresql_app")
async def postgresql_app_fixture(
ops_test: OpsTest, postgresql_app_name: str, pytestconfig: Config
):
async with ops_test.fast_forward():
await ops_test.model.deploy(postgresql_app_name, channel="14/stable", trust=True)
await ops_test.model.wait_for_idle(status='active')


@pytest_asyncio.fixture(scope="module", name="django_app_image")
def django_app_image_fixture(pytestconfig: Config):
"""Get value from parameter django-app--image."""
django_app_image = pytestconfig.getoption(DJANGO_APP_IMAGE_PARAM)
assert django_app_image, f"{DJANGO_APP_IMAGE_PARAM} must be set"
return django_app_image


@pytest_asyncio.fixture(scope="module", name="netbox_app")
async def netbox_app_fixture(
ops_test: OpsTest,
django_app_image,
postgresql_app_name,
get_unit_ips,
redis_password,
postgresql_app, # do not use
):
charm = await ops_test.build_charm(".")

resources = {
"django-app-image": django_app_image,
}
redis_ips = await get_unit_ips("redis-k8s")
app = await ops_test.model.deploy(
str(charm),
resources=resources,
config={
"redis_hostname" : redis_ips[0],
"redis_password": redis_password,
"django_debug": False,
"django_allowed_hosts": '*',
}
)
async with ops_test.fast_forward():
await ops_test.model.wait_for_idle(apps=["netbox"], status='waiting')

await ops_test.model.relate(f"netbox:postgresql", f"{postgresql_app_name}")
await ops_test.model.wait_for_idle(status='active')
return app

@pytest_asyncio.fixture(scope="module", name="redis_app")
async def redis_app_fixture(ops_test: OpsTest):
app = await ops_test.model.deploy("redis-k8s", channel="edge")
await ops_test.model.wait_for_idle(apps=["redis-k8s"], status='active')
return app

# @pytest.mark.skip_if_deployed
@pytest_asyncio.fixture(scope="module", name="redis_password")
async def redis_password_fixture(
ops_test: OpsTest,
redis_app, # do not use
):
password_action: Action = await ops_test.model.applications["redis-k8s"].units[0].run_action( # type: ignore
"get-initial-admin-password",
)
await password_action.wait()
assert password_action.status == "completed"
return password_action.results["redis-password"]
28 changes: 28 additions & 0 deletions charm/tests/integration/test_charm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

import requests

from pytest_operator.plugin import OpsTest

async def test_broken(ops_test: OpsTest, netbox_app, get_unit_ips):

unit_ips = await get_unit_ips("netbox")
breakpoint()
for unit_ip in unit_ips:
sess = requests.session()

url = f"http://{unit_ip}:8000"
res = sess.get(url, timeout=20,)
print("response", res)
print("response content", res.content)
assert res.status_code == 200
assert b"<title>Home | NetBox</title>" in res.content

url = f"http://{unit_ip}:8000/"
res = sess.get(url, timeout=20,)

# Also some random thing from the static dir.
url = f"http://{unit_ip}:8000/static/netbox.ico"
res = sess.get(url, timeout=20,)
assert res.status_code == 200
113 changes: 104 additions & 9 deletions charm_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

import json
import os
import pathlib
import pprint
import urllib.parse

print("OS ENVIRONMENT VARIABLES", os.environ)

# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
#
Expand All @@ -19,7 +19,8 @@
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases

db_url = os.environ["POSTGRESQL_DB_CONNECT_STRING"]
# TODO BE CAREFUL, THIS WILL ALSO WILL BE RUN IN THE MIGRATE, WITHOUT AN ENV VARIABLE!
db_url = os.environ.get("POSTGRESQL_DB_CONNECT_STRING", "")
parsed_db_url = urllib.parse.urlparse(db_url)

DATABASE = {
Expand Down Expand Up @@ -82,6 +83,7 @@
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY

# It is less than 50 characters in the 12 factor. Double the size.
# TODO FIX THIS. It is ugly.
SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] * 2


Expand Down Expand Up @@ -114,22 +116,115 @@
# Base URL path if accessing NetBox within a directory. For example, if installed at https://example.com/netbox/, set:
# BASE_PATH = 'netbox/'
BASE_PATH = os.environ["DJANGO_BASE_PATH"]

# API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be
# allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or
# CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers
CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = [
# 'https://hostname.example.com',
]
CORS_ORIGIN_REGEX_WHITELIST = [
# r'^(https?://)?(\w+\.)?example\.com$',
]

# The name to use for the CSRF token cookie.
CSRF_COOKIE_NAME = 'csrftoken'

# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
# on a production system.
DEBUG = os.environ.get("DJANGO_DEBUG", False)

# Set the default preferred language/locale
DEFAULT_LANGUAGE = 'en-us'

# Email settings
# EMAIL = {
# 'SERVER': 'localhost',
# 'PORT': 25,
# 'USERNAME': '',
# 'PASSWORD': '',
# 'USE_SSL': False,
# 'USE_TLS': False,
# 'TIMEOUT': 10, # seconds
# 'FROM_EMAIL': '',
# }

# Localization
ENABLE_LOCALIZATION = False

# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and
# by anonymous users. List models in the form `<app>.<model>`. Add '*' to this list to exempt all models.
EXEMPT_VIEW_PERMISSIONS = [
# 'dcim.site',
# 'dcim.region',
# 'ipam.prefix',
]

# HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks).
# HTTP_PROXIES = {
# 'http': 'http://10.10.1.10:3128',
# 'https': 'http://10.10.1.10:1080',
# }

# IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing
# NetBox from an internal IP.
INTERNAL_IPS = ('127.0.0.1', '::1')

# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
# https://docs.djangoproject.com/en/stable/topics/logging/
# LOGGING = {}

# LOGGING = {
# "version": 1,
# "disable_existing_loggers": False,
# "handlers": {
# "console": {
# "class": "logging.StreamHandler",
# },
# "file": {
# 'level': 'DEBUG',
# 'class': 'logging.FileHandler',
# 'filename': '/tmp/netbox.log',
# },
# },
# "root": {
# "handlers": ["console"],
# "level": "DEBUG",
# },
# }

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'normal': {
'format': '%(asctime)s %(name)s %(levelname)s: %(message)s'
},
},
'handlers': {
"console": {
"class": "logging.StreamHandler",
'level': 'DEBUG',
'formatter': 'normal',
},
# 'file': {
# 'level': 'DEBUG',
# 'class': 'logging.handlers.WatchedFileHandler',
# 'filename': '/tmp/netbox.log', # this is problematic, as migrate is run as root :(
# 'formatter': 'normal',
# },
},
"root": {
"handlers": ["console"],
"level": "DEBUG",
'loggers': {
'django': {
'handlers': ['console'],
'level': 'DEBUG',
},
'netbox': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}

Expand Down
35 changes: 35 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

[tox]
skipsdist=True
skip_missing_interpreters = True

[vars]
src_path = {toxinidir}/charm/
tst_path = {toxinidir}/charm/tests/

[testenv]
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path}
PYTHONBREAKPOINT=ipdb.set_trace
PY_COLORS=1
passenv =
PYTHONPATH
CHARM_BUILD_DIR
MODEL_SETTINGS


[testenv:integration]
description = Run integration tests
deps =
juju >=3.0
pytest
pytest-asyncio
pytest-operator
-r{[vars]src_path}/requirements.txt
changedir = {[vars]src_path}
setenv =
CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true
commands =
pytest -v -x --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs}

0 comments on commit 8a4262b

Please sign in to comment.