Repository: Pylons/pyramid_openapi3

Issue:
endpoint validation completely fails if the app is mounted at a subpath
I have an `openapi.yaml` defined with endpoints like `/foo`, `/bar`, etc. I then invoke 

```python
    config.pyramid_openapi3_spec(spec_path, route='/api/openapi.yaml')
    config.pyramid_openapi3_add_explorer('/api/')
```

and I mount my routes at `/api/foo`, `/api/bar`, etc. There doesn't seem to be a way to coordinate these such that everyone is happy because pyramid_openapi3 is expecting the api document paths to be relative to the root of the application and there's no way to tell pyramid_openapi3 that I'm actually mounting the routes/spec at a subpath inside the application.

Discussion:

Solution patch:
diff --git a/pyramid_openapi3/wrappers.py b/pyramid_openapi3/wrappers.py
index 3762d36..6c77211 100644
--- a/pyramid_openapi3/wrappers.py
+++ b/pyramid_openapi3/wrappers.py
@@ -41,7 +41,7 @@ class PyramidOpenAPIRequest:
             if self.request.matched_route
             else self.request.path_info
         )
-        return path_pattern
+        return self.request.script_name + path_pattern
 
     @property
     def method(self) -> str:


Test patch:
diff --git a/pyramid_openapi3/tests/test_validation.py b/pyramid_openapi3/tests/test_validation.py
index 540b625..02a9909 100644
--- a/pyramid_openapi3/tests/test_validation.py
+++ b/pyramid_openapi3/tests/test_validation.py
@@ -363,6 +363,110 @@ class TestRequestValidation(RequestValidationBase):  # noqa: D101
         self.assertEqual(start_response.status, "500 Internal Server Error")
 
 
+class TestServerRequestValidation(RequestValidationBase):  # noqa: D101
+
+    openapi_spec = (
+        b"openapi: '3.1.0'\n"
+        b"info:\n"
+        b"  version: '1.0.0'\n"
+        b"  title: Foo API\n"
+        b"servers:\n"
+        b"  - url: /prefix/v1\n"
+        b"  - url: http://example.com/prefix\n"
+        b"paths:\n"
+        b"  /foo:\n"
+        b"    get:\n"
+        b"      responses:\n"
+        b"        200:\n"
+        b"          description: A foo\n"
+        b"          content:\n"
+        b"            application/json:\n"
+        b"              schema:\n"
+        b"                type: object\n"
+        b"                properties:\n"
+        b"                  test:\n"
+        b"                    type: string\n"
+        b"        400:\n"
+        b"          description: Bad Request\n"
+    )
+
+    def test_server_validation_works_with_script_name(self) -> None:
+        """Expect to find a match for http://localhost:8080/prefix/v1/foo."""
+        self._add_view(lambda *arg: {"test": "correct"})
+        # run request through router
+        router = Router(self.config.registry)
+        environ = {
+            "wsgi.url_scheme": "http",
+            "SERVER_NAME": "localhost",
+            "SERVER_PORT": "8080",
+            "REQUEST_METHOD": "GET",
+            "SCRIPT_NAME": "/prefix/v1",
+            "PATH_INFO": "/foo",
+            "HTTP_ACCEPT": "application/json",
+        }
+        start_response = DummyStartResponse()
+        response = router(environ, start_response)
+
+        self.assertEqual(start_response.status, "200 OK")
+        self.assertEqual(json.loads(response[0]), {"test": "correct"})
+
+    def test_server_validation_works_with_script_name_and_hostname(self) -> None:
+        """Expect to find a match for http://example.com/prefix/foo."""
+        self._add_view(lambda *arg: {"test": "correct"})
+        # run request through router
+        router = Router(self.config.registry)
+        environ = {
+            "wsgi.url_scheme": "http",
+            "SERVER_NAME": "localhost",
+            "SERVER_PORT": "8080",
+            "REQUEST_METHOD": "GET",
+            "SCRIPT_NAME": "/prefix",
+            "PATH_INFO": "/foo",
+            "HTTP_HOST": "example.com",
+            "HTTP_ACCEPT": "application/json",
+        }
+        start_response = DummyStartResponse()
+        response = router(environ, start_response)
+
+        self.assertEqual(start_response.status, "200 OK")
+        self.assertEqual(json.loads(response[0]), {"test": "correct"})
+
+    def test_server_validation_fails_with_bad_hostname(self) -> None:
+        """Expect to fail for /prefix/v2/foo."""
+        self._add_view()
+        # run request through router
+        router = Router(self.config.registry)
+        environ = {
+            "wsgi.url_scheme": "http",
+            "SERVER_NAME": "localhost",
+            "SERVER_PORT": "8080",
+            "REQUEST_METHOD": "GET",
+            "SCRIPT_NAME": "/prefix/v2",
+            "PATH_INFO": "/foo",
+            "HTTP_HOST": "example.com",
+            "HTTP_ACCEPT": "application/json",
+        }
+        start_response = DummyStartResponse()
+        with self.assertLogs(level="ERROR") as cm:
+            response = router(environ, start_response)
+        self.assertEqual(start_response.status, "500 Internal Server Error")
+        self.assertEqual(
+            json.loads(response[0]),
+            [
+                {
+                    "exception": "ServerNotFound",
+                    "message": "Server not found for http://example.com/prefix/v2/foo",
+                }
+            ],
+        )
+        self.assertEqual(
+            cm.output,
+            [
+                "ERROR:pyramid_openapi3:Server not found for http://example.com/prefix/v2/foo"
+            ],
+        )
+
+
 class TestImproperAPISpecValidation(RequestValidationBase):  # noqa: D101
 
     openapi_spec = (
diff --git a/pyramid_openapi3/tests/test_wrappers.py b/pyramid_openapi3/tests/test_wrappers.py
index 5bae1ba..87d46b3 100644
--- a/pyramid_openapi3/tests/test_wrappers.py
+++ b/pyramid_openapi3/tests/test_wrappers.py
@@ -69,7 +69,7 @@ def test_relative_app_request() -> None:
     )
     assert openapi_request.host_url == "http://example.com"
     assert openapi_request.path == "/subpath/foo"
-    assert openapi_request.path_pattern == "/foo"
+    assert openapi_request.path_pattern == "/subpath/foo"
     assert openapi_request.method == "get"
     assert openapi_request.body == b""
     assert openapi_request.mimetype == "text/html"