Repository: jupyterhub/tmpauthenticator (language: python)

Issue:
Add basic tests
It would be good to have basic tests to verify function with JupyterHub of different versions etc.

### Todo

- [ ] Add pytest based tests
- [ ] Add .github/workflows/test.yaml



Discussion / hints:
Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! :hugs:
<br>If you haven't done so already, check out [Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). Also, please try to follow the issue template as it helps other other community members to contribute more effectively.
![welcome](https://raw.githubusercontent.com/jupyterhub/.github/master/images/welcome.jpg)
You can meet the other [Jovyans](https://jupyter.readthedocs.io/en/latest/community/content-community.html?highlight=jovyan#what-is-a-jovyan) by joining our [Discourse forum](http://discourse.jupyter.org/). There is also an intro thread there where you can stop by and say Hi! :wave:
<br>Welcome to the Jupyter community! :tada:


Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index 9eea84b..437c342 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,6 @@ remove-unused-variables = true
 [tool.black]
 skip-string-normalization = true
 target_version = [
-    "py37",
     "py38",
     "py39",
     "py310",
diff --git a/setup.py b/setup.py
index 73f33a8..2152ee0 100644
--- a/setup.py
+++ b/setup.py
@@ -20,15 +20,18 @@
         ],
     },
     packages=find_packages(),
-    python_requires=">=3.7",
+    python_requires=">=3.8",
     install_require={
-        "jupyterhub>=1.3.0",
+        "jupyterhub>=2.3.0",
+        "traitlets",
     },
     extras_require={
         "test": [
+            "aiohttp",
             "pytest",
             "pytest-asyncio",
             "pytest-cov",
+            "pytest-jupyterhub",
         ],
     },
 )


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index f48277b..4fa9d06 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -7,11 +7,13 @@ on:
   pull_request:
     paths-ignore:
       - "docs/**"
+      - "**.md"
       - ".github/workflows/*.yaml"
       - "!.github/workflows/test.yaml"
   push:
     paths-ignore:
       - "docs/**"
+      - "**.md"
       - ".github/workflows/*.yaml"
       - "!.github/workflows/test.yaml"
     branches-ignore:
@@ -29,37 +31,41 @@ jobs:
       matrix:
         include:
           # oldest supported python and jupyterhub version
-          - python-version: "3.7"
-            jupyterhub-version: "1.3.0"
-
           - python-version: "3.8"
-            jupyterhub-version: "1.*"
+            pip-install-spec: "jupyterhub==2.3.0 sqlalchemy==1.*"
           - python-version: "3.9"
-            jupyterhub-version: "2.*"
+            pip-install-spec: "jupyterhub==2.* sqlalchemy==1.*"
           - python-version: "3.10"
-            jupyterhub-version: "3.*"
+            pip-install-spec: "jupyterhub==3.*"
           - python-version: "3.11"
-            jupyterhub-version: "4.*"
+            pip-install-spec: "jupyterhub==4.*"
 
           # latest version of python and jupyterhub (including pre-releases)
           - python-version: "3.x"
-            jupyterhub-version: "*"
-            pip-install-flags: --pre
+            pip-install-spec: "--pre jupyterhub"
 
     steps:
       - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: "18"
       - uses: actions/setup-python@v4
         with:
           python-version: "${{ matrix.python-version }}"
 
+      - name: Install Node dependencies
+        run: |
+          npm install -g configurable-http-proxy
+
       - name: Install Python dependencies
         run: |
-          pip install ${{ matrix.pip-install-flags }} "jupyterhub==${{ matrix.jupyterhub-version }}"
+          pip install ${{ matrix.pip-install-spec }}
           pip install ".[test]"
           pip freeze
 
       - name: Run tests
         run: |
-          pytest --cov=jupyterhub-tmpauthenticator
+          pytest --cov=tmpauthenticator
 
+      # GitHub action reference: https://github.com/codecov/codecov-action
       - uses: codecov/codecov-action@v3
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..1f55586
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,42 @@
+import aiohttp
+import pytest
+from traitlets.config import Config
+
+# pytest-jupyterhub provides a pytest-plugin, and from it we get various
+# fixtures, where we make use of hub_app that builds on MockHub, which defaults
+# to providing a MockSpawner.
+#
+# ref: https://github.com/jupyterhub/pytest-jupyterhub
+# ref: https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/tests/mocking.py#L224
+#
+pytest_plugins = [
+    "jupyterhub-spawners-plugin",
+]
+
+
+@pytest.fixture
+async def hub_config():
+    """
+    Represents the base configuration of relevance to test TmpAuthenticator.
+    """
+    config = Config()
+    config.JupyterHub.authenticator_class = "tmp"
+
+    # yarl or aiohttp used in tests doesn't handle escaped URLs correctly, so
+    # the MockHub prefix of "/@/space%20word/" must be updated to workaround it.
+    config.JupyterHub.base_url = "/prefix/"
+
+    return config
+
+
+@pytest.fixture
+async def web_client_session():
+    """
+    Returns a ClientSession object from aiohttp, allowing cookies to be stored
+    in between requests etc, allowing us to simulate a browser.
+
+    ref: https://docs.aiohttp.org/en/stable/client_reference.html#client-session
+    ref: https://docs.aiohttp.org/en/stable/client_reference.html#response-object
+    """
+    async with aiohttp.ClientSession() as web_client_session:
+        yield web_client_session
diff --git a/tests/test_tmpauthenticator.py b/tests/test_tmpauthenticator.py
index f7e9a38..eea4b73 100644
--- a/tests/test_tmpauthenticator.py
+++ b/tests/test_tmpauthenticator.py
@@ -1,9 +1,159 @@
-"""
-Test ideas:
-
-- Validate that TmpAuthenticateHandler.get() creates a new user.
-- Validate that TmpAuthenticateHandler.get() creates a new user and updates the
-  login cookie even if it was already set.
-- Validate that TmpAuthenticateHandler.get() redirects to something sensible.
-- Validate that a configured post_auth_hook is executed.
-"""
+import pytest
+from yarl import URL
+
+
+async def _get_username(web_client_session, app_url):
+    """
+    Visits /hub/home to get an _xsrf token set to cookies, that we can then
+    pass as a X-XSRFToken header when accessing /hub/api, then the function
+    visit /hub/api/user to get the username as recognized by JupyterHub
+    based on cookies passed.
+    """
+    hub_url = str(app_url / "hub/")
+    home_url = str(app_url / "hub/home")
+    api_user_url = str(app_url / "hub/api/user")
+
+    r = await web_client_session.get(home_url)
+    assert r.status == 200
+
+    hub_cookies = web_client_session.cookie_jar.filter_cookies(home_url)
+    if "_xsrf" in hub_cookies:
+        _xsrf = hub_cookies["_xsrf"].value
+    else:
+        _xsrf = ""
+    headers = {
+        "X-XSRFToken": _xsrf,  # required for jupyterhub>=4
+        "Referer": hub_url,  # required for jupyterhub<4
+        "Accept": "application/json",
+    }
+
+    r = await web_client_session.get(api_user_url, headers=headers)
+    assert r.status == 200
+    user_api_response = await r.json()
+    return user_api_response["name"]
+
+
+@pytest.mark.parametrize(
+    "test_config, test_status, test_location, test_url",
+    [
+        ({}, 302, "hub/tmplogin", None),
+        ({"TmpAuthenticator": {"auto_login": True}}, 302, "hub/tmplogin", None),
+        ({"TmpAuthenticator": {"auto_login": False}}, 200, None, "hub/login"),
+    ],
+)
+async def test_auto_login_config(
+    hub_app,
+    hub_config,
+    web_client_session,
+    test_config,
+    test_status,
+    test_location,
+    test_url,
+):
+    """
+    Tests TmpAuthenticator.auto_login's behavior when its default value is used,
+    and when its explicitly set.
+    """
+    hub_config.merge(test_config)
+
+    app = await hub_app(hub_config)
+    app_port = URL(app.bind_url).port
+    app_url = URL(f"http://localhost:{app_port}{app.base_url}")
+
+    login_url = str(app_url / "hub/login")
+    r = await web_client_session.get(login_url, allow_redirects=False)
+
+    assert r.status == test_status
+    if test_url:
+        assert test_url in str(r.url)
+    if test_location:
+        assert test_location in r.headers["Location"]
+
+
+async def test_login(
+    hub_app,
+    hub_config,
+    web_client_session,
+):
+    """
+    Tests that the user is redirected and finally authorized for /hub/home.
+    """
+    app = await hub_app(hub_config)
+    app_port = URL(app.bind_url).port
+    app_url = URL(f"http://localhost:{app_port}{app.base_url}")
+
+    home_url = str(app_url / "hub/home")
+    r = await web_client_session.get(home_url)
+
+    assert r.status == 200
+
+
+@pytest.mark.parametrize(
+    "test_setting_admin_to, test_status",
+    [
+        (True, 200),
+        (False, 403),
+    ],
+)
+async def test_post_auth_hook_config(
+    hub_app,
+    hub_config,
+    web_client_session,
+    test_setting_admin_to,
+    test_status,
+):
+    """
+    Tests that the inherited Authenticator.post_auth_hook is respected by
+    updating the authentication dictionary's admin key and accessing /hub/admin,
+    which should result in a forbidden response if not configured as admin via
+    the post_auth_hook.
+    """
+
+    def set_admin_post_auth_hook(authenticator, handler, authentication):
+        authentication["admin"] = test_setting_admin_to
+        return authentication
+
+    hub_config.TmpAuthenticator.post_auth_hook = set_admin_post_auth_hook
+
+    app = await hub_app(hub_config)
+    app_port = URL(app.bind_url).port
+    app_url = URL(f"http://localhost:{app_port}{app.base_url}")
+
+    admin_url = str(app_url / "hub/admin")
+    r = await web_client_session.get(admin_url)
+
+    assert r.status == test_status
+
+
+async def test_revisit_tmplogin(
+    hub_app,
+    hub_config,
+    web_client_session,
+):
+    """
+    Tests that we get a new user if visiting /hub/tmplogin, even if we already
+    were authenticated as one as recognized by cookies.
+
+    This is done by first visiting /hub/home which should get us logged in and
+    inspecting the user via a cookie, and then /hub/tmplogin to again inspect
+    the user via a cookie.
+    """
+    app = await hub_app(hub_config)
+    app_port = URL(app.bind_url).port
+    app_url = URL(f"http://localhost:{app_port}{app.base_url}")
+
+    # first access, so we receive a new user
+    first_username = await _get_username(web_client_session, app_url)
+    assert first_username
+
+    # when we visit /hub/home again, we are recognized and that doesn't make us
+    # arrive at /hub/login -> /hub/tmplogin, and therefore we shouldn't get a
+    # new user
+    assert first_username == await _get_username(web_client_session, app_url)
+
+    # we visit /hub/tmplogin and should get a _new_ user
+    tmplogin_url = str(app_url / "hub/tmplogin")
+    r = await web_client_session.get(tmplogin_url)
+    assert r.status == 200
+    second_username = await _get_username(web_client_session, app_url)
+    assert first_username != second_username