ProvenPin Install

Flask-WTF forms with custom validators, CSRF on and off in tests: working versions (tested 2026-10-15)

A Flask-WTF form with a reusable custom WTForms validator, rendered errors, and CSRF protection, tested offline with the Flask test client: CSRF disabled in the test config, then enforced.

passed4 passed · 0 failed2026-10-15 13:30 UTC
Status
passed
Last passed
2026-10-15 13:30 UTC
Tests
4 / 4
Language
Python
Runtime
Python 3.13.16
pinned versions4
flask@3.1.3flask-wtf@1.3.0wtforms@3.2.2pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38742087302

The code

Exactly the files that passed. Your agent can pull them with get_recipe("flask-wtforms-csrf-validation") (install), or see them on GitHub.

requirements.txt
flask==3.1.3
flask-wtf==1.3.0
wtforms==3.2.2
pytest==9.1.1
app.py
import re

from flask import Flask, render_template_string, request
from flask_wtf import FlaskForm
from wtforms import IntegerField, StringField, SubmitField
from wtforms.validators import DataRequired, NumberRange, ValidationError

SLUG = re.compile(r"^[a-z0-9-]+$")


def slug_format(form, field):
    # Reusable custom validator: raise ValidationError to attach a message to the field.
    if not SLUG.match(field.data or ""):
        raise ValidationError("Use lowercase letters, digits and dashes only.")


class ProjectForm(FlaskForm):
    # filters run before validators, so whitespace-only names fail DataRequired.
    name = StringField("Name", filters=[lambda s: s.strip() if s else s],
                       validators=[DataRequired()])
    slug = StringField("Slug", validators=[DataRequired(), slug_format])
    seats = IntegerField("Seats", validators=[NumberRange(min=1, max=50)])
    submit = SubmitField("Create")


TEMPLATE = """
<form method="post">
  {{ form.csrf_token }}
  {% for field in [form.name, form.slug, form.seats] %}
  <label>{{ field.label }} {{ field() }}</label>
  {% endfor %}
  {{ form.submit() }}
  {% for msgs in form.errors.values() %}{% for e in msgs %}<p class="error">{{ e }}</p>{% endfor %}{% endfor %}
</form>
"""


def create_app(config=None):
    app = Flask(__name__)
    # Flask-WTF reads WTF_CSRF_ENABLED per form instance, so set it before requests.
    # Note: TESTING=True does NOT disable CSRF; only WTF_CSRF_ENABLED does.
    app.config.update(SECRET_KEY="dev", TESTING=True)
    app.config.update(config or {})

    @app.route("/projects/new", methods=["GET", "POST"])
    def new_project():
        form = ProjectForm()
        if form.validate_on_submit():
            return f"created {form.slug.data}", 201
        status = 422 if request.method == "POST" else 200
        return render_template_string(TEMPLATE, form=form), status

    return app
tests/test_forms.py
import re

import pytest

from app import create_app

VALID = {"name": "Demo", "slug": "demo-1", "seats": "5"}
TOKEN = re.compile(r'name="csrf_token"[^>]*value="([^"]+)"')


@pytest.fixture
def client():
    # CSRF disabled for this app only; the form is validated without a token.
    return create_app({"WTF_CSRF_ENABLED": False}).test_client()


@pytest.fixture
def app():
    # Default config: CSRF enforced.
    return create_app()


def test_csrf_disabled_in_test_config(client):
    r = client.post("/projects/new", data=VALID)
    assert r.status_code == 201
    assert b"demo-1" in r.data


def test_csrf_enforced_rejects_missing_token(app):
    r = app.test_client().post("/projects/new", data=VALID)
    assert r.status_code == 422
    assert b"CSRF" in r.data


def test_token_is_session_bound(app):
    # The token is tied to the session cookie: the same client succeeds,
    # a fresh client presenting the same token is rejected.
    c = app.test_client()
    page = c.get("/projects/new").get_data(as_text=True)
    token = TOKEN.search(page).group(1)
    assert c.post("/projects/new", data={**VALID, "csrf_token": token}).status_code == 201
    other = app.test_client()
    assert other.post("/projects/new", data={**VALID, "csrf_token": token}).status_code == 422


def test_custom_validator_errors_rendered(client):
    # Whitespace-only name is stripped by the filter, then fails DataRequired.
    r = client.post("/projects/new", data={"name": "   ", "slug": "Bad Slug", "seats": "5"})
    assert r.status_code == 422
    assert b"lowercase letters" in r.data
    assert b"This field is required." in r.data
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.