Repository: netbox-community/netbox-acls (language: python)

Issue:
[Housekeeping]: Develop Plugin Tests - Models
### Proposed Changes

Test each model included in the plugin

Examples:

- [NetBox SecretStore Plugin](https://github.com/DanSheps/netbox-secretstore/tree/develop/netbox_secretstore/tests)
- [NetBox BGP Plugin](https://github.com/k01ek/netbox-bgp/tree/main/netbox_bgp/tests)
- [NetBox Tunnels Plugin](https://github.com/jdrew82/netbox-tunnels-plugin/tree/master/tests)
- [NetBox Tenancy Model](https://github.com/netbox-community/netbox/tree/develop/netbox/tenancy/tests)

### Justification

Testing should be built into the development process.


Discussion / hints:

Fix patch:
diff --git a/.github/linters/.flake8 b/.github/linters/.flake8
deleted file mode 100644
index 55e13d2..0000000
--- a/.github/linters/.flake8
+++ /dev/null
@@ -1,3 +0,0 @@
-[flake8]
-max-line-length = 160
-extend-ignore = E203
diff --git a/.github/linters/.isort.cfg b/.github/linters/.isort.cfg
deleted file mode 100644
index a9a8d64..0000000
--- a/.github/linters/.isort.cfg
+++ /dev/null
@@ -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
diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json
deleted file mode 100644
index a474b30..0000000
--- a/.github/linters/.jscpd.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "threshold": 10
-}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4b580b7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml
deleted file mode 100644
index 49aa1ee..0000000
--- a/.github/workflows/super-linter.yml
+++ /dev/null
@@ -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 }}
diff --git a/.jscpd.json b/.jscpd.json
new file mode 100644
index 0000000..338acfe
--- /dev/null
+++ b/.jscpd.json
@@ -0,0 +1,4 @@
+{
+    "threshold": 10,
+    "ignore": ["**/tests/**"]
+}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9f31f9d..9ebc05c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,6 +9,8 @@ repos:
       - id: debug-statements
       - id: end-of-file-fixer
       - id: name-tests-test
+        args:
+          - "--django"
       - id: requirements-txt-fixer
       - id: trailing-whitespace
   - repo: https://github.com/PyCQA/isort
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..72b0fcd
--- /dev/null
+++ b/Dockerfile
@@ -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
diff --git a/MANIFEST.in b/MANIFEST.in
index beb736b..35c0df2 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,3 @@
 include README.md
 include LICENSE
 recursive-include netbox_acls/templates *
-recursive-include netbox_acls/static *
diff --git a/configuration/configuration.py b/configuration/configuration.py
new file mode 100644
index 0000000..1ebd734
--- /dev/null
+++ b/configuration/configuration.py
@@ -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
diff --git a/configuration/logging.py b/configuration/logging.py
new file mode 100644
index 0000000..86914ae
--- /dev/null
+++ b/configuration/logging.py
@@ -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,
+}
diff --git a/configuration/plugins.py b/configuration/plugins.py
new file mode 100644
index 0000000..eb7c714
--- /dev/null
+++ b/configuration/plugins.py
@@ -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": {},
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..0b78731
--- /dev/null
+++ b/docker-compose.yml
@@ -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
diff --git a/env/netbox.env b/env/netbox.env
new file mode 100644
index 0000000..ee71174
--- /dev/null
+++ b/env/netbox.env
@@ -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
diff --git a/env/postgres.env b/env/postgres.env
new file mode 100644
index 0000000..bb7b53c
--- /dev/null
+++ b/env/postgres.env
@@ -0,0 +1,3 @@
+POSTGRES_DB=netbox
+POSTGRES_PASSWORD=J5brHrAXFLQSif0K
+POSTGRES_USER=netbox
diff --git a/env/redis.env b/env/redis.env
new file mode 100644
index 0000000..44a1987
--- /dev/null
+++ b/env/redis.env
@@ -0,0 +1,1 @@
+REDIS_PASSWORD=H733Kdjndks81
diff --git a/netbox_acls/migrations/0003_netbox_acls.py b/netbox_acls/migrations/0003_netbox_acls.py
index ccfcfbe..562e0f3 100644
--- a/netbox_acls/migrations/0003_netbox_acls.py
+++ b/netbox_acls/migrations/0003_netbox_acls.py
@@ -15,28 +15,36 @@ class Migration(migrations.Migration):
             model_name="accesslist",
             name="custom_field_data",
             field=models.JSONField(
-                blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder
+                blank=True,
+                default=dict,
+                encoder=utilities.json.CustomFieldJSONEncoder,
             ),
         ),
         migrations.AlterField(
             model_name="aclextendedrule",
             name="custom_field_data",
             field=models.JSONField(
-                blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder
+                blank=True,
+                default=dict,
+                encoder=utilities.json.CustomFieldJSONEncoder,
             ),
         ),
         migrations.AlterField(
             model_name="aclinterfaceassignment",
             name="custom_field_data",
             field=models.JSONField(
-                blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder
+                blank=True,
+                default=dict,
+                encoder=utilities.json.CustomFieldJSONEncoder,
             ),
         ),
         migrations.AlterField(
             model_name="aclstandardrule",
             name="custom_field_data",
             field=models.JSONField(
-                blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder
+                blank=True,
+                default=dict,
+                encoder=utilities.json.CustomFieldJSONEncoder,
             ),
         ),
     ]
diff --git a/netbox_acls/urls.py b/netbox_acls/urls.py
index c5f07a1..5c70579 100644
--- a/netbox_acls/urls.py
+++ b/netbox_acls/urls.py
@@ -5,7 +5,7 @@
 from django.urls import include, path
 from utilities.urls import get_model_urls
 
-from . import models, views
+from . import views
 
 urlpatterns = (
     # Access Lists
diff --git a/setup.py b/setup.py
index 1b55f67..50d4055 100644
--- a/setup.py
+++ b/setup.py
@@ -3,12 +3,13 @@
 
 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()
 
 
 def read(rel_path):
-    here = os.path.abspath(os.path.dirname(__file__))
     with codecs.open(os.path.join(here, rel_path), "r") as fp:
         return fp.read()
 


Test patch:
diff --git a/netbox_acls/tests/__init__.py b/netbox_acls/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/netbox_acls/tests/test_api.py b/netbox_acls/tests/test_api.py
new file mode 100644
index 0000000..f01a687
--- /dev/null
+++ b/netbox_acls/tests/test_api.py
@@ -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,
+            },
+        ]
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..5fc2e06
--- /dev/null
+++ b/test.sh
@@ -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}'"