Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions biosimdb_interface/form/webform.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from werkzeug.datastructures import ImmutableMultiDict

from biosimdb_interface.login.community_invite import invite_user
from biosimdb_interface.schema.webform import WEBFORM_SCHEMA, get_simulation_metadata

from . import form_bp
Expand Down Expand Up @@ -125,8 +126,10 @@ def resume_submit():
@form_bp.route("/do_submit", methods=["POST"])
def do_submit():
"""Execute the deferred Invenio upload using session-stored form data.
Called automatically by the loading page after login. Clears pending
session data after upload and renders the success page with the record URL.
Called automatically by the loading page after login.
Automatically invite user to Invenio instance community, then submit.
Clears pending session data after upload and renders the success page
with the record URL.
"""
form_data = session.pop("pending_form_data", None)
tmpdir = session.pop("pending_files_dir", None)
Expand All @@ -140,6 +143,8 @@ def do_submit():
)

try:
token = session.get("access_token")
invite_user("biosimdb", token)
draft_id = prepare_for_invenio(flat_form, tmpdir)
except requests.HTTPError as exc:
status = exc.response.status_code if exc.response is not None else None
Expand Down
65 changes: 65 additions & 0 deletions biosimdb_interface/login/community_invite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python
"""Automatically invite logged in user to BioSimDB."""

import requests
from flask import current_app


def _fetch_user_id(access_token: str):
"""
Fetch the logged in user ID.
Args:
access_token (str): OAuth2 bearer token for the authenticated user.

Returns:
int | None: Invenio instance user ID if found, or None.

"""
api_base = current_app.config.get("API_BASE", "").rstrip("/")
url = f"{api_base}/me"
headers = {"Authorization": f"Bearer {access_token}"}
resp = requests.get(url, headers=headers, timeout=10)
data = resp.json()

if isinstance(data, dict):
if data.get("id"):
return data["id"]
else:
return None


def invite_user(slug: str, access_token: str):
"""
Check if a logged in user is a member of an Invenio instance community.
Add the user if they are not a member of biosimdb.

Args:
user_id (int): Invenio instance user ID.
slug (str): Name of the community in the Invenio instance.
access_token (str): OAuth2 bearer token for the authenticated user.
"""
user_id = _fetch_user_id(access_token)
api_base = current_app.config.get("API_BASE", "").rstrip("/")
headers = {"Authorization": f"Bearer {access_token}"}

r = requests.get(api_base + "/communities/" + slug, headers=headers)
community_id = r.json()["id"]

r = requests.get(
api_base + "/communities/" + community_id + "/members",
params={"size": 1000},
headers=headers,
)
found = False
for m in r.json()["hits"]["hits"]:
if m["member"]["id"] == str(user_id):
found = True
if found:
pass
else:
data = {"members": [{"id": user_id, "type": "user"}], "role": "reader"}
requests.post(
api_base + "/communities/" + community_id + "/invitations",
json=data,
headers=headers,
)
9 changes: 6 additions & 3 deletions tests/test_form/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ def test_do_submit_calls_invenio(client):
sess["pending_form_data"] = {"simulation_name": ["test"]}
sess["pending_files_dir"] = "/tmp/fake_pending"

with patch("biosimdb_interface.form.webform.prepare_for_invenio") as mock_prepare:
with (
patch("biosimdb_interface.form.webform.invite_user") as mock_invite,
patch("biosimdb_interface.form.webform.prepare_for_invenio") as mock_prepare,
):
mock_prepare.return_value = "draft-123"
response = client.post("/do_submit")
_response = client.post("/do_submit")
assert mock_invite.called
assert mock_prepare.called
assert response.status_code in (200, 302)
77 changes: 77 additions & 0 deletions tests/test_login/test_community_invite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from unittest.mock import Mock, patch

import pytest

from biosimdb_interface.login import community_invite as mod


def _resp(payload):
r = Mock()
r.json.return_value = payload
return r


def test_fetch_user_id_returns_id(app):
with (
app.app_context(),
patch("biosimdb_interface.login.community_invite.requests.get") as get,
):
get.return_value = _resp({"id": 7})
assert mod._fetch_user_id("tok") == 7
get.assert_called_once_with(
"http://localhost/api/me",
headers={"Authorization": "Bearer tok"},
timeout=10,
)


@pytest.mark.parametrize("payload", [[], {"username": "x"}])
def test_fetch_user_id_returns_none_for_non_id_payloads(app, payload):
with (
app.app_context(),
patch("biosimdb_interface.login.community_invite.requests.get") as get,
):
get.return_value = _resp(payload)
assert mod._fetch_user_id("tok") is None


def test_invite_user_does_not_post_if_member_exists(app):
with (
app.app_context(),
patch(
"biosimdb_interface.login.community_invite._fetch_user_id", return_value=42
),
patch("biosimdb_interface.login.community_invite.requests.get") as get,
patch("biosimdb_interface.login.community_invite.requests.post") as post,
):
get.side_effect = [
_resp({"id": "comm-1"}),
_resp({"hits": {"hits": [{"member": {"id": "42"}}]}}),
]

mod.invite_user("biosimdb", "tok")

post.assert_not_called()


def test_invite_user_posts_if_member_missing(app):
with (
app.app_context(),
patch(
"biosimdb_interface.login.community_invite._fetch_user_id", return_value=42
),
patch("biosimdb_interface.login.community_invite.requests.get") as get,
patch("biosimdb_interface.login.community_invite.requests.post") as post,
):
get.side_effect = [
_resp({"id": "comm-1"}),
_resp({"hits": {"hits": [{"member": {"id": "99"}}]}}),
]

mod.invite_user("biosimdb", "tok")

post.assert_called_once_with(
"http://localhost/api/communities/comm-1/invitations",
json={"members": [{"id": 42, "type": "user"}], "role": "reader"},
headers={"Authorization": "Bearer tok"},
)
Loading