Repository: nautobot/nautobot

Issue:
Swagger UI broken (extra `/api` in URLs)
### Environment
* Python version:
* Nautobot version: 1.4.0

### Steps to Reproduce
1. Navigate to /api/docs/
2. Attempt any "Try it out" operation

### Expected Behavior

Valid API request, e.g. to `http://localhost:8080/api/circuits/circuit-terminations/`

### Observed Behavior

Invalid API request due to an extra `/api` in the URL, e.g. `http://localhost:8080/api/api/circuits/circuit-terminations/'`

Additionally, there's an extra `/circuits` section at the end of the listed endpoints:

![image](https://user-images.githubusercontent.com/5603551/185177652-5f3fa71d-9796-4463-a941-a57badde3792.png)

This appears to have been broken by #1902.
Swagger API Docs Appears to Have Lost Sectional Headings
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version: 3.9
* Nautobot version: 1.4.0

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Install Nautobot
2. Navigate to https://test.nautobot.com/api/docs/#/ page
3. Expect to see headers for circuits, dcim, ipam, etc...

<!-- What did you expect to happen? -->
### Expected Behavior

Expect to see headers for circuits, dcim, ipam, etc...

<!-- What happened instead? -->
### Observed Behavior

See only section headers of `API` and `Circuits`


Discussion:
I did some testing with pynautobot to test against what the impact may be on this. Pynautobot did handle API requests just fine for many of the items including creating devices, sites, and IPAM.
Yeah, pynautobot *should* be using the REST API directly, not going through Swagger, so I'd expect it to be unaffected - thanks for checking though!
Possibly same root cause as #2232 

Resolving patch:
diff --git a/nautobot/core/settings.py b/nautobot/core/settings.py
--- a/nautobot/core/settings.py
+++ b/nautobot/core/settings.py
@@ -202,6 +202,7 @@
     # trim it from all of the individual paths correspondingly.
     # See also https://github.com/nautobot/nautobot-ansible/pull/135 for an example of why this is desirable.
     "SERVERS": [{"url": "/api"}],
+    "SCHEMA_PATH_PREFIX": "/api",
     "SCHEMA_PATH_PREFIX_TRIM": True,
     # use sidecar - locally packaged UI files, not CDN
     "SWAGGER_UI_DIST": "SIDECAR",
diff --git a/nautobot/core/views/mixins.py b/nautobot/core/views/mixins.py
--- a/nautobot/core/views/mixins.py
+++ b/nautobot/core/views/mixins.py
@@ -23,6 +23,8 @@
 from rest_framework.response import Response
 from rest_framework.viewsets import GenericViewSet
 
+from drf_spectacular.utils import extend_schema
+
 from nautobot.core.api.views import BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin
 from nautobot.extras.models import CustomField, ExportTemplate
 from nautobot.utilities.error_handlers import handle_protectederror
@@ -53,6 +55,7 @@
 }
 
 
+@extend_schema(exclude=True)
 class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormView):
     """
     NautobotViewSetMixin is an aggregation of various mixins from DRF, Django and Nautobot to acheive the desired behavior pattern for NautobotUIViewSet


Test patch:
diff --git a/nautobot/core/tests/integration/test_swagger.py b/nautobot/core/tests/integration/test_swagger.py
new file mode 100644
--- /dev/null
+++ b/nautobot/core/tests/integration/test_swagger.py
@@ -0,0 +1,38 @@
+from django.urls import reverse
+
+from nautobot.utilities.testing.integration import SeleniumTestCase
+
+
+class SwaggerUITestCase(SeleniumTestCase):
+    """Integration tests for the Swagger UI."""
+
+    def setUp(self):
+        super().setUp()
+        self.user.is_superuser = True
+        self.user.save()
+        self.login(self.user.username, self.password)
+
+    def tearDown(self):
+        self.logout()
+        super().tearDown()
+
+    def test_endpoint_render(self):
+        """Check that the dcim.site API endpoints are rendered correctly."""
+        self.browser.visit(self.live_server_url + reverse("api_docs"))
+        # Wait for Swagger UI to load, look for the site-list endpoint in the UI and click on it to expand it.
+        dcim_sites_list = self.browser.find_by_id("operations-dcim-dcim_sites_list", wait_time=20).first
+        dcim_sites_list.find_by_tag("button").first.click()
+
+        # Look for the "Try it out" button and click it
+        dcim_sites_list.find_by_xpath("//button[contains(text(), 'Try it out')]").first.click()
+
+        # Look for the "Execute" button and click it
+        dcim_sites_list.find_by_xpath("//button[contains(text(), 'Execute')]").first.click()
+
+        # Look at the response status code
+        response_code = dcim_sites_list.find_by_xpath(
+            "//table[@class[contains(.,'live-responses-table')]]/tbody//td[@class[contains(.,'response-col_status')]]",
+            wait_time=20,
+        ).first.text
+        self.assertEqual(response_code, "200")
+        # The response body is wrapped in a bunch of <span>s to apply syntax-highlighting, so it's not worth inspecting.