Added testing framework (#121)

* added api test case for access list

* initial testing framework
This commit is contained in:
Abhimanyu Saharan 2023-01-26 06:31:05 +05:30 committed by GitHub
parent 9611816fe8
commit e43b5e41f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 394 additions and 52 deletions

View File

@ -1,3 +0,0 @@
[flake8]
max-line-length = 160
extend-ignore = E203

View File

@ -1,8 +0,0 @@
[settings]
profile = black
; vertical hanging indent mode also used in black configuration
multi_line_output = 3
; necessary because black expect the trailing comma
include_trailing_comma = true

View File

@ -1,3 +0,0 @@
{
"threshold": 10
}

46
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: CI
on:
push:
pull_request:
# This ensures that previous jobs for the workflow are canceled when the ref is
# updated.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
run-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0
- name: Lint Code Base
uses: github/super-linter/slim@v4
env:
DEFAULT_BRANCH: dev
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUPPRESS_POSSUM: true
LINTER_RULES_PATH: /
VALIDATE_ALL_CODEBASE: false
VALIDATE_DOCKERFILE: false
VALIDATE_JSCPD: true
FILTER_REGEX_EXCLUDE: (.*/)?(configuration/.*)
test:
runs-on: ubuntu-latest
name: Runs plugin tests
needs: run-lint
steps:
- id: git-checkout
name: Checkout
uses: actions/checkout@v3
- id: docker-test
name: Test the image
run: ./test.sh snapshot

View File

@ -1,30 +0,0 @@
---
# This workflow executes several linters on changed files based on languages used in your code base whenever
# you push a code or open a pull request.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/github/super-linter
name: Lint Code Base
on:
push:
branches: ["dev"]
pull_request:
branches: ["dev"]
jobs:
run-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0
- name: Lint Code Base
uses: github/super-linter/slim@v4
env:
VALIDATE_ALL_CODEBASE: false
DEFAULT_BRANCH: "dev"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.jscpd.json Normal file
View File

@ -0,0 +1,4 @@
{
"threshold": 10,
"ignore": ["**/tests/**"]
}

View File

@ -9,6 +9,8 @@ repos:
- id: debug-statements - id: debug-statements
- id: end-of-file-fixer - id: end-of-file-fixer
- id: name-tests-test - id: name-tests-test
args:
- "--django"
- id: requirements-txt-fixer - id: requirements-txt-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
ARG NETBOX_VARIANT=v3.4
FROM netboxcommunity/netbox:${NETBOX_VARIANT}
RUN mkdir -pv /plugins/netbox-acls
COPY . /plugins/netbox-acls
RUN /opt/netbox/venv/bin/python3 /plugins/netbox-acls/setup.py develop
RUN cp -rf /plugins/netbox-acls/netbox_acls/ /opt/netbox/venv/lib/python3.10/site-packages/netbox_acls

View File

@ -1,4 +1,3 @@
include README.md include README.md
include LICENSE include LICENSE
recursive-include netbox_acls/templates * recursive-include netbox_acls/templates *
recursive-include netbox_acls/static *

View File

@ -0,0 +1,99 @@
####
## We recommend to not edit this file.
## Create separate files to overwrite the settings.
## See `extra.py` as an example.
####
from os import environ
from os.path import abspath, dirname
# For reference see https://netbox.readthedocs.io/en/stable/configuration/
# Based on https://github.com/netbox-community/netbox/blob/master/netbox/netbox/configuration.example.py
# Read secret from file
def _read_secret(secret_name, default=None):
try:
f = open("/run/secrets/" + secret_name, encoding="utf-8")
except OSError:
return default
else:
with f:
return f.readline().strip()
_BASE_DIR = dirname(dirname(abspath(__file__)))
#########################
# #
# Required settings #
# #
#########################
# 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.
#
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
ALLOWED_HOSTS = environ.get("ALLOWED_HOSTS", "*").split(" ")
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
DATABASE = {
"NAME": environ.get("DB_NAME", "netbox"), # Database name
"USER": environ.get("DB_USER", ""), # PostgreSQL username
"PASSWORD": _read_secret("db_password", environ.get("DB_PASSWORD", "")),
# PostgreSQL password
"HOST": environ.get("DB_HOST", "localhost"), # Database server
"PORT": environ.get("DB_PORT", ""), # Database port (leave blank for default)
"OPTIONS": {"sslmode": environ.get("DB_SSLMODE", "prefer")},
# Database connection SSLMODE
"CONN_MAX_AGE": int(environ.get("DB_CONN_MAX_AGE", "300")),
# Max database connection age
"DISABLE_SERVER_SIDE_CURSORS": environ.get(
"DB_DISABLE_SERVER_SIDE_CURSORS",
"False",
).lower()
== "true",
# Disable the use of server-side cursors transaction pooling
}
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
# to use two separate database IDs.
REDIS = {
"tasks": {
"HOST": environ.get("REDIS_HOST", "localhost"),
"PORT": int(environ.get("REDIS_PORT", 6379)),
"PASSWORD": _read_secret("redis_password", environ.get("REDIS_PASSWORD", "")),
"DATABASE": int(environ.get("REDIS_DATABASE", 0)),
"SSL": environ.get("REDIS_SSL", "False").lower() == "true",
"INSECURE_SKIP_TLS_VERIFY": environ.get(
"REDIS_INSECURE_SKIP_TLS_VERIFY",
"False",
).lower()
== "true",
},
"caching": {
"HOST": environ.get("REDIS_CACHE_HOST", environ.get("REDIS_HOST", "localhost")),
"PORT": int(environ.get("REDIS_CACHE_PORT", environ.get("REDIS_PORT", 6379))),
"PASSWORD": _read_secret(
"redis_cache_password",
environ.get("REDIS_CACHE_PASSWORD", environ.get("REDIS_PASSWORD", "")),
),
"DATABASE": int(environ.get("REDIS_CACHE_DATABASE", 1)),
"SSL": environ.get("REDIS_CACHE_SSL", environ.get("REDIS_SSL", "False")).lower()
== "true",
"INSECURE_SKIP_TLS_VERIFY": environ.get(
"REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY",
environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False"),
).lower()
== "true",
},
}
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
# symbols. NetBox will not run without this defined. For more information, see
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", ""))
DEVELOPER = True

11
configuration/logging.py Normal file
View File

@ -0,0 +1,11 @@
# Remove first comment(#) on each line to implement this working logging example.
# Add LOGLEVEL environment variable to netbox if you use this example & want a different log level.
from os import environ
# Set LOGLEVEL in netbox.env or docker-compose.overide.yml to override a logging level of INFO.
LOGLEVEL = environ.get("LOGLEVEL", "INFO")
LOGGING = {
"version": 1,
"disable_existing_loggers": True,
}

13
configuration/plugins.py Normal file
View File

@ -0,0 +1,13 @@
# Add your plugins and plugin settings here.
# Of course uncomment this file out.
# To learn how to build images with your required plugins
# See https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins
PLUGINS = [
"netbox_acls",
]
PLUGINS_CONFIG = { # type: ignore
"netbox_acls": {},
}

29
docker-compose.yml Normal file
View File

@ -0,0 +1,29 @@
version: '3.4'
services:
netbox:
build:
dockerfile: Dockerfile
context: .
args:
NETBOX_VARIANT: ${NETBOX_VARIANT}
depends_on:
- postgres
- redis
env_file: env/netbox.env
volumes:
- ./configuration:/etc/netbox/config:z,ro
# postgres
postgres:
image: postgres:14-alpine
env_file: env/postgres.env
# redis
redis:
image: redis:6-alpine
command:
- sh
- -c # this is to evaluate the $REDIS_PASSWORD from the env
- redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
env_file: env/redis.env

23
env/netbox.env vendored Normal file
View File

@ -0,0 +1,23 @@
ALLOWED_HOSTS=*
CORS_ORIGIN_ALLOW_ALL=true
DB_HOST=postgres
DB_NAME=netbox
DB_PASSWORD=J5brHrAXFLQSif0K
DB_USER=netbox
DEBUG=true
ENFORCE_GLOBAL_UNIQUE=true
LOGIN_REQUIRED=false
GRAPHQL_ENABLED=true
MAX_PAGE_SIZE=1000
MEDIA_ROOT=/opt/netbox/netbox/media
REDIS_DATABASE=0
REDIS_HOST=redis
REDIS_INSECURE_SKIP_TLS_VERIFY=false
REDIS_PASSWORD=H733Kdjndks81
SECRET_KEY=r8OwDznj!!dciP9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
SUPERUSER_EMAIL=admin@example.com
SUPERUSER_NAME=admin
SUPERUSER_PASSWORD=admin
STARTUP_SCRIPTS=false
WEBHOOKS_ENABLED=true

3
env/postgres.env vendored Normal file
View File

@ -0,0 +1,3 @@
POSTGRES_DB=netbox
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
POSTGRES_USER=netbox

1
env/redis.env vendored Normal file
View File

@ -0,0 +1 @@
REDIS_PASSWORD=H733Kdjndks81

View File

@ -15,28 +15,36 @@ class Migration(migrations.Migration):
model_name="accesslist", model_name="accesslist",
name="custom_field_data", name="custom_field_data",
field=models.JSONField( field=models.JSONField(
blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder,
), ),
), ),
migrations.AlterField( migrations.AlterField(
model_name="aclextendedrule", model_name="aclextendedrule",
name="custom_field_data", name="custom_field_data",
field=models.JSONField( field=models.JSONField(
blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder,
), ),
), ),
migrations.AlterField( migrations.AlterField(
model_name="aclinterfaceassignment", model_name="aclinterfaceassignment",
name="custom_field_data", name="custom_field_data",
field=models.JSONField( field=models.JSONField(
blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder,
), ),
), ),
migrations.AlterField( migrations.AlterField(
model_name="aclstandardrule", model_name="aclstandardrule",
name="custom_field_data", name="custom_field_data",
field=models.JSONField( field=models.JSONField(
blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder,
), ),
), ),
] ]

View File

View File

@ -0,0 +1,100 @@
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from netbox_acls.choices import *
from netbox_acls.models import *
from rest_framework import status
from utilities.testing import APITestCase, APIViewTestCases
class AppTest(APITestCase):
def test_root(self):
url = reverse("plugins-api:netbox_acls-api:api-root")
response = self.client.get(f"{url}?format=api", **self.header)
self.assertEqual(response.status_code, status.HTTP_200_OK)
class ACLTestCase(
APIViewTestCases.GetObjectViewTestCase,
APIViewTestCases.ListObjectsViewTestCase,
APIViewTestCases.CreateObjectViewTestCase,
APIViewTestCases.UpdateObjectViewTestCase,
APIViewTestCases.DeleteObjectViewTestCase,
):
"""Test the AccessList Test"""
model = AccessList
view_namespace = "plugins-api:netbox_acls"
brief_fields = ["display", "id", "name", "url"]
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name="Site 1", slug="site-1")
manufacturer = Manufacturer.objects.create(
name="Manufacturer 1",
slug="manufacturer-1",
)
devicetype = DeviceType.objects.create(
manufacturer=manufacturer,
model="Device Type 1",
)
devicerole = DeviceRole.objects.create(
name="Device Role 1",
slug="device-role-1",
)
device = Device.objects.create(
name="Device 1",
site=site,
device_type=devicetype,
device_role=devicerole,
)
access_lists = (
AccessList(
name="testacl1",
assigned_object_type=ContentType.objects.get_for_model(Device),
assigned_object_id=device.id,
type=ACLTypeChoices.TYPE_STANDARD,
default_action=ACLActionChoices.ACTION_DENY,
),
AccessList(
name="testacl2",
assigned_object_type=ContentType.objects.get_for_model(Device),
assigned_object_id=device.id,
type=ACLTypeChoices.TYPE_STANDARD,
default_action=ACLActionChoices.ACTION_DENY,
),
AccessList(
name="testacl3",
assigned_object_type=ContentType.objects.get_for_model(Device),
assigned_object_id=device.id,
type=ACLTypeChoices.TYPE_STANDARD,
default_action=ACLActionChoices.ACTION_DENY,
),
)
AccessList.objects.bulk_create(access_lists)
cls.create_data = [
{
"name": "testacl4",
"assigned_object_type": "dcim.device",
"assigned_object_id": device.id,
"type": ACLTypeChoices.TYPE_STANDARD,
"default_action": ACLActionChoices.ACTION_DENY,
},
{
"name": "testacl5",
"assigned_object_type": "dcim.device",
"assigned_object_id": device.id,
"type": ACLTypeChoices.TYPE_EXTENDED,
"default_action": ACLActionChoices.ACTION_DENY,
},
{
"name": "testacl6",
"assigned_object_type": "dcim.device",
"assigned_object_id": device.id,
"type": ACLTypeChoices.TYPE_STANDARD,
"default_action": ACLActionChoices.ACTION_DENY,
},
]

View File

@ -5,7 +5,7 @@ Map Views to URLs.
from django.urls import include, path from django.urls import include, path
from utilities.urls import get_model_urls from utilities.urls import get_model_urls
from . import models, views from . import views
urlpatterns = ( urlpatterns = (
# Access Lists # Access Lists

View File

@ -3,12 +3,13 @@ import os.path
from setuptools import find_packages, setup from setuptools import find_packages, setup
with open("README.md", encoding="utf-8") as fh: here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, "README.md"), encoding="utf-8") as fh:
long_description = fh.read() long_description = fh.read()
def read(rel_path): def read(rel_path):
here = os.path.abspath(os.path.dirname(__file__))
with codecs.open(os.path.join(here, rel_path), "r") as fp: with codecs.open(os.path.join(here, rel_path), "r") as fp:
return fp.read() return fp.read()

38
test.sh Executable file
View File

@ -0,0 +1,38 @@
#!/bin/bash
# Runs the NetBox plugin unit tests
# Usage:
# ./test.sh latest
# ./test.sh v2.9.7
# ./test.sh develop-2.10
# exit when a command exits with an exit code != 0
set -e
# NETBOX_VARIANT is used by `Dockerfile` to determine the tag
NETBOX_VARIANT="${1-latest}"
# The docker compose command to use
doco="docker compose --file docker-compose.yml"
test_netbox_unit_tests() {
echo "⏱ Running NetBox Unit Tests"
$doco run --rm netbox python manage.py makemigrations netbox_acls --check
$doco run --rm netbox python manage.py test netbox_acls
}
test_cleanup() {
echo "💣 Cleaning Up"
$doco down -v
$doco rm -fsv
docker image rm docker.io/library/netbox-acls-netbox || echo ''
}
export NETBOX_VARIANT=${NETBOX_VARIANT}
echo "🐳🐳🐳 Start testing '${NETBOX_VARIANT}'"
# Make sure the cleanup script is executed
trap test_cleanup EXIT ERR
test_netbox_unit_tests
echo "🐳🐳🐳 Done testing '${NETBOX_VARIANT}'"