Implement multiple fields

This commit is contained in:
Éloi Rivard 2023-06-22 11:39:50 +02:00
parent 42730f72d3
commit 8617fc0f2b
8 changed files with 477 additions and 87 deletions

View file

@ -1,5 +1,6 @@
import datetime import datetime
import math import math
import re
import pytz import pytz
import wtforms import wtforms
@ -23,6 +24,17 @@ def is_uri(form, field):
raise wtforms.ValidationError(_("This is not a valid URL")) raise wtforms.ValidationError(_("This is not a valid URL"))
def unique_values(form, field):
values = set()
for subfield in field:
if subfield.data in values:
subfield.errors.append(_("This value is a duplicate"))
raise wtforms.ValidationError(_("This value is a duplicate"))
if subfield.data:
values.add(subfield.data)
meta = DefaultMeta() meta = DefaultMeta()
@ -36,6 +48,26 @@ class I18NFormMixin:
class HTMXFormMixin: class HTMXFormMixin:
SEPARATOR = "-"
def field_from_name(self, field_name):
"""
Returns a tuple containing a field and its rendering context
"""
if self.SEPARATOR not in field_name:
field = self[field_name] if field_name in self else None
return field, {}
parts = field_name.split(self.SEPARATOR)
fieldlist_name = self.SEPARATOR.join(parts[:-1])
try:
indice = int(parts[-1])
except ValueError:
return None, {}
fieldlist, _ = self.field_from_name(fieldlist_name)
context = {"parent_list": fieldlist, "parent_indice": indice}
return fieldlist[indice], context
def validate(self, *args, **kwargs): def validate(self, *args, **kwargs):
""" """
If the request is a HTMX request, this will only render the field If the request is a HTMX request, this will only render the field
@ -46,21 +78,68 @@ class HTMXFormMixin:
return super().validate(*args, **kwargs) return super().validate(*args, **kwargs)
field_name = request.headers.get("HX-Trigger-Name") field_name = request.headers.get("HX-Trigger-Name")
if field_name in self: field, context = self.field_from_name(field_name)
self.validate_field(field_name, *args, **kwargs) if field:
self.render_field(field_name) self.validate_field(field, *args, **kwargs)
abort(400) self.render_field(field, **context)
def validate_field(self, field_name, *args, **kwargs): abort(400, f"{field_name} is not a valid field for inline validation")
self[field_name].widget.hide_value = False
def validate_field(self, field, *args, **kwargs):
field.widget.hide_value = False
self.process(request.form) self.process(request.form)
super().validate(*args, **kwargs) return field.validate(self, *args, **kwargs)
def render_field(self, field_name, *args, **kwargs): def render_field(self, field, *args, **kwargs):
form_macro = current_app.jinja_env.get_template("macro/form.html") form_macro = current_app.jinja_env.get_template("macro/form.html")
response = make_response(form_macro.module.render_field(self[field_name])) response = make_response(form_macro.module.render_field(field, *args, **kwargs))
abort(response) abort(response)
def form_control(self):
"""
Checks wether the current request is the result of the users
adding or removing a field from a FieldList.
"""
FIELDLIST_ADD_BUTTON = "fieldlist_add"
FIELDLIST_REMOVE_BUTTON = "fieldlist_remove"
fieldlist_suffix = rf"{self.SEPARATOR}(\d+)$"
if field_name := request.form.get(FIELDLIST_ADD_BUTTON):
fieldlist_name = re.sub(fieldlist_suffix, "", field_name)
fieldlist, context = self.field_from_name(fieldlist_name)
if not fieldlist or not isinstance(fieldlist, wtforms.FieldList):
abort(400)
if request_is_htmx():
self.validate_field(fieldlist)
fieldlist.append_entry()
if request_is_htmx():
self.render_field(fieldlist, **context)
return True
if field_name := request.form.get(FIELDLIST_REMOVE_BUTTON):
fieldlist_name = re.sub(fieldlist_suffix, "", field_name)
fieldlist, context = self.field_from_name(fieldlist_name)
if not fieldlist or not isinstance(fieldlist, wtforms.FieldList):
abort(400)
if request_is_htmx():
self.validate_field(fieldlist)
fieldlist.pop_entry()
if request_is_htmx():
self.render_field(fieldlist, **context)
return True
return False
class HTMXForm(HTMXFormMixin, I18NFormMixin, FlaskForm): class HTMXForm(HTMXFormMixin, I18NFormMixin, FlaskForm):
pass pass

View file

@ -3,7 +3,6 @@ import datetime
from canaille.app import models from canaille.app import models
from canaille.app.flask import permissions_needed from canaille.app.flask import permissions_needed
from canaille.app.flask import render_htmx_template from canaille.app.flask import render_htmx_template
from canaille.app.flask import request_is_htmx
from canaille.app.forms import TableForm from canaille.app.forms import TableForm
from canaille.oidc.forms import ClientAddForm from canaille.oidc.forms import ClientAddForm
from flask import abort from flask import abort
@ -37,7 +36,7 @@ def index(user):
def add(user): def add(user):
form = ClientAddForm(request.form or None) form = ClientAddForm(request.form or None)
if not request.form: if not request.form or form.form_control():
return render_template( return render_template(
"oidc/admin/client_add.html", form=form, menuitem="admin" "oidc/admin/client_add.html", form=form, menuitem="admin"
) )
@ -57,11 +56,11 @@ def add(user):
client_id=client_id, client_id=client_id,
client_id_issued_at=client_id_issued_at, client_id_issued_at=client_id_issued_at,
client_name=form["client_name"].data, client_name=form["client_name"].data,
contacts=[form["contacts"].data], contacts=form["contacts"].data,
client_uri=form["client_uri"].data, client_uri=form["client_uri"].data,
grant_types=form["grant_types"].data, grant_types=form["grant_types"].data,
redirect_uris=[form["redirect_uris"].data], redirect_uris=form["redirect_uris"].data,
post_logout_redirect_uris=[form["post_logout_redirect_uris"].data], post_logout_redirect_uris=form["post_logout_redirect_uris"].data,
response_types=form["response_types"].data, response_types=form["response_types"].data,
scope=form["scope"].data.split(" "), scope=form["scope"].data.split(" "),
token_endpoint_auth_method=form["token_endpoint_auth_method"].data, token_endpoint_auth_method=form["token_endpoint_auth_method"].data,
@ -90,17 +89,9 @@ def add(user):
@bp.route("/edit/<client_id>", methods=["GET", "POST"]) @bp.route("/edit/<client_id>", methods=["GET", "POST"])
@permissions_needed("manage_oidc") @permissions_needed("manage_oidc")
def edit(user, client_id): def edit(user, client_id):
if ( if request.form and request.form.get("action") == "delete":
request.method == "GET"
or request.form.get("action") == "edit"
or request_is_htmx()
):
return client_edit(client_id)
if request.form.get("action") == "delete":
return client_delete(client_id) return client_delete(client_id)
return client_edit(client_id)
abort(400)
def client_edit(client_id): def client_edit(client_id):
@ -111,17 +102,10 @@ def client_edit(client_id):
data = {attribute: getattr(client, attribute) for attribute in client.attributes} data = {attribute: getattr(client, attribute) for attribute in client.attributes}
data["scope"] = " ".join(data["scope"]) data["scope"] = " ".join(data["scope"])
data["redirect_uris"] = data["redirect_uris"][0] if data["redirect_uris"] else ""
data["contacts"] = data["contacts"][0] if data["contacts"] else ""
data["post_logout_redirect_uris"] = (
data["post_logout_redirect_uris"][0]
if data["post_logout_redirect_uris"]
else ""
)
data["preconsent"] = client.preconsent data["preconsent"] = client.preconsent
form = ClientAddForm(request.form or None, data=data, client=client) form = ClientAddForm(request.form or None, data=data, client=client)
if not request.form: if not request.form or form.form_control():
return render_template( return render_template(
"oidc/admin/client_edit.html", form=form, client=client, menuitem="admin" "oidc/admin/client_edit.html", form=form, client=client, menuitem="admin"
) )
@ -137,11 +121,11 @@ def client_edit(client_id):
client.update( client.update(
client_name=form["client_name"].data, client_name=form["client_name"].data,
contacts=[form["contacts"].data], contacts=form["contacts"].data,
client_uri=form["client_uri"].data, client_uri=form["client_uri"].data,
grant_types=form["grant_types"].data, grant_types=form["grant_types"].data,
redirect_uris=[form["redirect_uris"].data], redirect_uris=form["redirect_uris"].data,
post_logout_redirect_uris=[form["post_logout_redirect_uris"].data], post_logout_redirect_uris=form["post_logout_redirect_uris"].data,
response_types=form["response_types"].data, response_types=form["response_types"].data,
scope=form["scope"].data.split(" "), scope=form["scope"].data.split(" "),
token_endpoint_auth_method=form["token_endpoint_auth_method"].data, token_endpoint_auth_method=form["token_endpoint_auth_method"].data,

View file

@ -2,6 +2,7 @@ import wtforms
from canaille.app import models from canaille.app import models
from canaille.app.forms import HTMXForm from canaille.app.forms import HTMXForm
from canaille.app.forms import is_uri from canaille.app.forms import is_uri
from canaille.app.forms import unique_values
from flask_babel import lazy_gettext as _ from flask_babel import lazy_gettext as _
@ -23,10 +24,14 @@ class ClientAddForm(HTMXForm):
validators=[wtforms.validators.DataRequired()], validators=[wtforms.validators.DataRequired()],
render_kw={"placeholder": "Client Name"}, render_kw={"placeholder": "Client Name"},
) )
contacts = wtforms.EmailField( contacts = wtforms.FieldList(
_("Contact"), wtforms.EmailField(
validators=[wtforms.validators.Optional(), wtforms.validators.Email()], _("Contact"),
render_kw={"placeholder": "admin@mydomain.tld"}, validators=[wtforms.validators.Optional(), wtforms.validators.Email()],
render_kw={"placeholder": "admin@mydomain.tld"},
),
min_entries=1,
validators=[unique_values],
) )
client_uri = wtforms.URLField( client_uri = wtforms.URLField(
_("URI"), _("URI"),
@ -36,21 +41,31 @@ class ClientAddForm(HTMXForm):
], ],
render_kw={"placeholder": "https://mydomain.tld"}, render_kw={"placeholder": "https://mydomain.tld"},
) )
redirect_uris = wtforms.URLField( redirect_uris = wtforms.FieldList(
_("Redirect URIs"), wtforms.URLField(
validators=[ _("Redirect URIs"),
wtforms.validators.DataRequired(), validators=[
is_uri, wtforms.validators.DataRequired(),
], is_uri,
render_kw={"placeholder": "https://mydomain.tld/callback"}, ],
render_kw={"placeholder": "https://mydomain.tld/callback"},
),
min_entries=1,
validators=[unique_values],
) )
post_logout_redirect_uris = wtforms.URLField( post_logout_redirect_uris = wtforms.FieldList(
_("Post logout redirect URIs"), wtforms.URLField(
validators=[ _("Post logout redirect URIs"),
wtforms.validators.Optional(), validators=[
is_uri, wtforms.validators.Optional(),
], is_uri,
render_kw={"placeholder": "https://mydomain.tld/you-have-been-disconnected"}, ],
render_kw={
"placeholder": "https://mydomain.tld/you-have-been-disconnected"
},
),
min_entries=1,
validators=[unique_values],
) )
grant_types = wtforms.SelectMultipleField( grant_types = wtforms.SelectMultipleField(
_("Grant types"), _("Grant types"),

View file

@ -100,6 +100,19 @@ i.massive.massive.massive.portrait.icon, i.massive.massive.massive.portrait.icon
background: rgba(0,0,0,.05) !important; background: rgba(0,0,0,.05) !important;
} }
/**
* Workaround for
* https://github.com/fomantic/Fomantic-UI/issues/2829
*/
.ui.corner.labeled.action.input .ui.corner.label {
right:40px;
z-index: 5;
}
.ui.corner.labeled.action.input .ui.button {
z-index: 99;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.logo img { .logo img {
filter: invert(.8) !important; filter: invert(.8) !important;

View file

@ -6,7 +6,9 @@ container=true,
noindicator=false, noindicator=false,
indicator_icon=none, indicator_icon=none,
indicator_text=none, indicator_text=none,
display=true display=true,
add_button=false,
del_button=false
) -%} ) -%}
{% set field_visible = field.type != 'HiddenField' and field.type !='CSRFTokenField' %} {% set field_visible = field.type != 'HiddenField' and field.type !='CSRFTokenField' %}
{% if container and field_visible %} {% if container and field_visible %}
@ -33,6 +35,7 @@ display=true
<div class="ui <div class="ui
{%- if corner_indicator %} corner labeled{% endif -%} {%- if corner_indicator %} corner labeled{% endif -%}
{%- if icon or field.description %} left icon{% endif -%} {%- if icon or field.description %} left icon{% endif -%}
{%- if add_button or del_button %} action{% endif -%}
{%- if field.type not in ("BooleanField", "RadioField") %} input{% endif -%} {%- if field.type not in ("BooleanField", "RadioField") %} input{% endif -%}
"> ">
{% endif %} {% endif %}
@ -66,7 +69,38 @@ display=true
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if field_visible %} {% if field_visible %}
{% if del_button %}
<button
class="ui teal icon button"
title="{{ _("Remove this field") }}"
type="submit"
name="fieldlist_remove"
value="{{ field.name }}"
hx-post=""
{# Workaround for https://github.com/bigskysoftware/htmx/issues/1506 #}
hx-vals='{"fieldlist_remove": "{{ field.name }}"}'
hx-target="closest .fieldlist"
formnovalidate>
<i class="minus icon"></i>
</button>
{% endif %}
{% if add_button %}
<button
class="ui teal icon button"
title="{{ _("Add another field") }}"
type="submit"
name="fieldlist_add"
value="{{ field.name }}"
hx-post=""
{# Workaround for https://github.com/bigskysoftware/htmx/issues/1506 #}
hx-vals='{"fieldlist_add": "{{ field.name }}"}'
hx-target="closest .fieldlist"
formnovalidate>
<i class="plus icon"></i>
</button>
{% endif %}
</div> </div>
{% endif %} {% endif %}
@ -90,14 +124,39 @@ display=true
{% endfor %} {% endfor %}
{% endmacro %} {% endmacro %}
{% macro render_field(field) -%} {% macro render_field(field, parent_list=none, parent_indice=none) -%}
{% if field.type == "BooleanField" %} {% if parent_list %}
{% set last = parent_indice >= parent_list.entries|len -1 %}
{% set ignore_me = kwargs.update({
"label_visible": false,
"add_button": (last and (not parent_list.max_entries or parent_indice < parent_list.max_entries)),
"del_button": (last and parent_list.min_entries and parent_indice >= parent_list.min_entries),
}) %}
{% endif %}
{% if field.type == "FieldList" %}
{{ render_list(field, **kwargs) }}
{% elif field.type == "BooleanField" %}
{{ render_checkbox(field, **kwargs) }} {{ render_checkbox(field, **kwargs) }}
{% else %} {% else %}
{{ render_input(field, **kwargs) }} {{ render_input(field, **kwargs) }}
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}
{% macro render_list(field) -%}
<div class="field fieldlist" id="{{ field.name }}">
{# Strangely enough, translations are not rendered when using field.label() #}
{{ field[0].label() }}
{% for subfield in field %}
{{ render_field(
subfield,
parent_list=field,
parent_indice=loop.index0,
**kwargs
) }}
{% endfor %}
</div>
{%- endmacro %}
{% macro render_checkbox(field, display=true) -%} {% macro render_checkbox(field, display=true) -%}
<div class="field" <div class="field"
{% if not display %}style="display: none"{% endif %} {% if not display %}style="display: none"{% endif %}

View file

@ -2,6 +2,7 @@ import datetime
import wtforms import wtforms
from babel.dates import LOCALTZ from babel.dates import LOCALTZ
from canaille.app import models
from canaille.app.forms import DateTimeUTCField from canaille.app.forms import DateTimeUTCField
from flask import current_app from flask import current_app
from werkzeug.datastructures import ImmutableMultiDict from werkzeug.datastructures import ImmutableMultiDict
@ -184,3 +185,262 @@ def test_datetime_utc_field_invalid_timezone(testclient):
form.dt() form.dt()
== f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">' == f'<input id="dt" name="dt" type="datetime-local" value="{rendered_locale_date}">'
) )
def test_fieldlist_add(testclient, logged_admin):
assert not models.Client.query()
res = testclient.get("/admin/client/add")
assert "redirect_uris-1" not in res.form.fields
data = {
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "https://foo.bar/callback",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
}
for k, v in data.items():
res.form[k].force_value(v)
res = res.form.submit(status=200, name="fieldlist_add", value="redirect_uris-0")
assert not models.Client.query()
data["redirect_uris-1"] = "https://foo.bar/callback2"
for k, v in data.items():
res.form[k].force_value(v)
res = res.form.submit(status=302, name="action", value="edit")
res = res.follow(status=200)
client_id = res.forms["readonly"]["client_id"].value
client = models.Client.get(client_id=client_id)
assert client.redirect_uris == [
"https://foo.bar/callback",
"https://foo.bar/callback2",
]
client.delete()
def test_fieldlist_delete(testclient, logged_admin):
assert not models.Client.query()
res = testclient.get("/admin/client/add")
data = {
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "https://foo.bar/callback1",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
}
for k, v in data.items():
res.form[k].force_value(v)
res = res.form.submit(status=200, name="fieldlist_add", value="redirect_uris-0")
res.form["redirect_uris-1"] = "https://foo.bar/callback2"
res = res.form.submit(status=200, name="fieldlist_remove", value="redirect_uris-1")
assert not models.Client.query()
assert "redirect_uris-1" not in res.form.fields
res = res.form.submit(status=302, name="action", value="edit")
res = res.follow(status=200)
client_id = res.forms["readonly"]["client_id"].value
client = models.Client.get(client_id=client_id)
assert client.redirect_uris == [
"https://foo.bar/callback1",
]
client.delete()
def test_fieldlist_add_invalid_field(testclient, logged_admin):
res = testclient.get("/admin/client/add")
data = {
"csrf_token": res.form["csrf_token"].value,
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "https://foo.bar/callback",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
"fieldlist_add": "invalid",
}
testclient.post("/admin/client/add", data, status=400)
def test_fieldlist_delete_invalid_field(testclient, logged_admin):
assert not models.Client.query()
res = testclient.get("/admin/client/add")
data = {
"csrf_token": res.form["csrf_token"].value,
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "https://foo.bar/callback1",
"redirect_uris-1": "https://foo.bar/callback2",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
"fieldlist_remove": "invalid",
}
testclient.post("/admin/client/add", data, status=400)
def test_fieldlist_duplicate_value(testclient, logged_admin, client):
res = testclient.get("/admin/client/add")
data = {
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "https://foo.bar/samecallback",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
}
for k, v in data.items():
res.form[k].force_value(v)
res = res.form.submit(status=200, name="fieldlist_add", value="redirect_uris-0")
res.form["redirect_uris-1"] = "https://foo.bar/samecallback"
res = res.form.submit(status=200, name="action", value="edit")
res.mustcontain("This value is a duplicate")
def test_fieldlist_empty_value(testclient, logged_admin, client):
res = testclient.get("/admin/client/add")
data = {
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "https://foo.bar/samecallback",
"post_logout_redirect_uris-0": "https://foo.bar/callback1",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
}
for k, v in data.items():
res.form[k].force_value(v)
res = res.form.submit(
status=200, name="fieldlist_add", value="post_logout_redirect_uris-0"
)
res.form.submit(status=302, name="action", value="edit")
client = models.Client.get()
client.delete()
def test_fieldlist_add_field_htmx(testclient, logged_admin):
res = testclient.get("/admin/client/add")
data = {
"csrf_token": res.form["csrf_token"].value,
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "https://foo.bar/callback",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
"fieldlist_add": "redirect_uris-0",
}
response = testclient.post(
"/admin/client/add",
data,
headers={
"HX-Request": "true",
"HX-Trigger-Name": "listfield_add",
},
)
assert 'name="redirect_uris-0' in response.text
assert 'name="redirect_uris-1' in response.text
def test_fieldlist_add_field_htmx_validation(testclient, logged_admin):
res = testclient.get("/admin/client/add")
data = {
"csrf_token": res.form["csrf_token"].value,
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "not-a-valid-uri",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
"fieldlist_add": "redirect_uris-0",
}
response = testclient.post(
"/admin/client/add",
data,
headers={
"HX-Request": "true",
"HX-Trigger-Name": "listfield_add",
},
)
assert 'name="redirect_uris-0' in response.text
assert 'name="redirect_uris-1' in response.text
assert "This is not a valid URL" in response.text
def test_fieldlist_remove_field_htmx(testclient, logged_admin):
res = testclient.get("/admin/client/add")
data = {
"csrf_token": res.form["csrf_token"].value,
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "https://foo.bar/callback1",
"redirect_uris-1": "https://foo.bar/callback2",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
"fieldlist_remove": "redirect_uris-1",
}
response = testclient.post(
"/admin/client/add",
data,
headers={
"HX-Request": "true",
"HX-Trigger-Name": "listfield_remove",
},
)
assert 'name="redirect_uris-0' in response.text
assert 'name="redirect_uris-1' not in response.text
def test_fieldlist_inline_validation(testclient, logged_admin):
res = testclient.get("/admin/client/add")
data = {
"csrf_token": res.form["csrf_token"].value,
"client_name": "foobar",
"client_uri": "https://foo.bar",
"redirect_uris-0": "invalid-url",
"redirect_uris-1": "https://foo.bar/callback2",
"grant_types": ["password", "authorization_code"],
"response_types": ["code", "token"],
"token_endpoint_auth_method": "none",
}
response = testclient.post(
"/admin/client/add",
data,
headers={
"HX-Request": "true",
"HX-Trigger-Name": "redirect_uris-0",
},
)
assert 'name="redirect_uris-0' in response.text
assert 'name="redirect_uris-1' not in response.text
assert "This is not a valid URL" in response.text
def test_inline_validation_invalid_field(testclient, logged_admin, user):
res = testclient.get("/profile")
testclient.post(
"/profile",
{
"csrf_token": res.form["csrf_token"].value,
"email": "john@doe.com",
},
headers={
"HX-Request": "true",
"HX-Trigger-Name": "invalid-field",
},
status=400,
)

View file

@ -80,24 +80,6 @@ def test_profile_creation_dynamic_validation(testclient, logged_admin, user):
res.mustcontain("The email &#39;john@doe.com&#39; is already used") res.mustcontain("The email &#39;john@doe.com&#39; is already used")
def test_profile_creation_dynamic_validation_invalid_field(
testclient, logged_admin, user
):
res = testclient.get("/profile")
testclient.post(
"/profile",
{
"csrf_token": res.form["csrf_token"].value,
"email": "john@doe.com",
},
headers={
"HX-Request": "true",
"HX-Trigger-Name": "invalid-field",
},
status=400,
)
def test_user_creation_without_password(testclient, logged_moderator): def test_user_creation_without_password(testclient, logged_moderator):
res = testclient.get("/profile", status=200) res = testclient.get("/profile", status=200)
res.form["user_name"] = "george" res.form["user_name"] = "george"

View file

@ -87,9 +87,9 @@ def test_client_add(testclient, logged_admin):
res = testclient.get("/admin/client/add") res = testclient.get("/admin/client/add")
data = { data = {
"client_name": "foobar", "client_name": "foobar",
"contacts": "foo@bar.com", "contacts-0": "foo@bar.com",
"client_uri": "https://foo.bar", "client_uri": "https://foo.bar",
"redirect_uris": ["https://foo.bar/callback"], "redirect_uris-0": "https://foo.bar/callback",
"grant_types": ["password", "authorization_code"], "grant_types": ["password", "authorization_code"],
"scope": "openid profile", "scope": "openid profile",
"response_types": ["code", "token"], "response_types": ["code", "token"],
@ -103,12 +103,12 @@ def test_client_add(testclient, logged_admin):
"jwks_uri": "https://foo.bar/jwks.json", "jwks_uri": "https://foo.bar/jwks.json",
"audience": [], "audience": [],
"preconsent": False, "preconsent": False,
"post_logout_redirect_uris": ["https://foo.bar/disconnected"], "post_logout_redirect_uris-0": "https://foo.bar/disconnected",
} }
for k, v in data.items(): for k, v in data.items():
res.form[k].force_value(v) res.form[k].force_value(v)
res = res.form.submit(status=302, name="action", value="edit") res = res.form.submit(status=302, name="action", value="add")
res = res.follow(status=200) res = res.follow(status=200)
client_id = res.forms["readonly"]["client_id"].value client_id = res.forms["readonly"]["client_id"].value
@ -149,9 +149,9 @@ def test_client_edit(testclient, client, logged_admin, other_client):
res = testclient.get("/admin/client/edit/" + client.client_id) res = testclient.get("/admin/client/edit/" + client.client_id)
data = { data = {
"client_name": "foobar", "client_name": "foobar",
"contacts": "foo@bar.com", "contacts-0": "foo@bar.com",
"client_uri": "https://foo.bar", "client_uri": "https://foo.bar",
"redirect_uris": ["https://foo.bar/callback"], "redirect_uris-0": "https://foo.bar/callback",
"grant_types": ["password", "authorization_code"], "grant_types": ["password", "authorization_code"],
"scope": "openid profile", "scope": "openid profile",
"response_types": ["code", "token"], "response_types": ["code", "token"],
@ -165,7 +165,7 @@ def test_client_edit(testclient, client, logged_admin, other_client):
"jwks_uri": "https://foo.bar/jwks.json", "jwks_uri": "https://foo.bar/jwks.json",
"audience": [client.id, other_client.id], "audience": [client.id, other_client.id],
"preconsent": True, "preconsent": True,
"post_logout_redirect_uris": ["https://foo.bar/disconnected"], "post_logout_redirect_uris-0": "https://foo.bar/disconnected",
} }
for k, v in data.items(): for k, v in data.items():
res.forms["clientaddform"][k].force_value(v) res.forms["clientaddform"][k].force_value(v)
@ -182,7 +182,10 @@ def test_client_edit(testclient, client, logged_admin, other_client):
assert client.client_name == "foobar" assert client.client_name == "foobar"
assert client.contacts == ["foo@bar.com"] assert client.contacts == ["foo@bar.com"]
assert client.client_uri == "https://foo.bar" assert client.client_uri == "https://foo.bar"
assert client.redirect_uris == ["https://foo.bar/callback"] assert client.redirect_uris == [
"https://foo.bar/callback",
"https://mydomain.tld/redirect2",
]
assert client.grant_types == ["password", "authorization_code"] assert client.grant_types == ["password", "authorization_code"]
assert client.scope == ["openid", "profile"] assert client.scope == ["openid", "profile"]
assert client.response_types == ["code", "token"] assert client.response_types == ["code", "token"]
@ -247,11 +250,6 @@ def test_client_delete_invalid_client(testclient, logged_admin, client):
) )
def test_invalid_request(testclient, logged_admin, client):
res = testclient.get("/admin/client/edit/" + client.client_id)
res = res.forms["clientaddform"].submit(name="action", value="invalid", status=400)
def test_client_edit_preauth(testclient, client, logged_admin, other_client): def test_client_edit_preauth(testclient, client, logged_admin, other_client):
assert not client.preconsent assert not client.preconsent