Browse Source

Improve general architecture (#46)

pull/53/head
Reinhard Müller 2 months ago
parent
commit
1592fe3163

+ 39
- 0
Dockerfile View File

@@ -0,0 +1,39 @@
# =============================================================================
# Build instructions for the Docker container
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

FROM fsfe/alpine-pipenv:latest

EXPOSE 8080

WORKDIR /root

# Install Python packages
COPY Pipfile Pipfile.lock ./
RUN pipenv install --system --deploy

# Install the actual application
COPY . .
RUN ./setup.py install

# Switch to non-root user
RUN adduser -g "FSFE" -s "/sbin/nologin" -D fsfe
USER fsfe
WORKDIR /home/fsfe

# Run the WSGI server
CMD gunicorn --bind 0.0.0.0:8080 "fsfe_forms:create_app()"

+ 0
- 11
Dockerfile-web View File

@@ -1,11 +0,0 @@
FROM python:3.6
EXPOSE 8080

RUN pip install "pipenv==2018.11.26"
COPY Pipfile Pipfile.lock ./
RUN pipenv install --system --deploy

COPY . /var/share/forms
WORKDIR /var/share/forms

CMD gunicorn -b 0.0.0.0:8080 fsfe_forms.wsgi:application

+ 1
- 0
MANIFEST.in View File

@@ -0,0 +1 @@
recursive-include fsfe_forms/configuration *

+ 6
- 5
Pipfile View File

@@ -3,17 +3,18 @@ url = "https://pypi.org/simple"
verify_ssl = true

[packages]
bottle = "==0.12.13"
gunicorn = "==19.7.1"
filelock = "*"
flask = "*"
flask-limiter = "*"
gunicorn = "*"
redis = "*"
filelock = "==2.0.12"
Jinja2 = "==2.9.6"
webargs = "*"

[dev-packages]
fakeredis = "*"
pytest-cov = "*"
pytest-flask = "*"
pytest-mock = "*"
webtest = "*"

[requires]
python_version = "3.6"

+ 174
- 49
Pipfile.lock View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "0cfd50577d7ea22df7a35b523db1ee8b38570df4daa2befa27896d29e27c648b"
"sha256": "eda7fc40dfa733958d50f1d3f49afe99e23a0238033cd0037ba4d9b7a09b36ad"
},
"pipfile-spec": 6,
"requires": {
@@ -15,31 +15,61 @@
]
},
"default": {
"bottle": {
"click": {
"hashes": [
"sha256:39b751aee0b167be8dffb63ca81b735bbf1dd0905b3bc42761efedee8f123355"
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==0.12.13"
"version": "==7.0"
},
"filelock": {
"hashes": [
"sha256:eb4314a9a032707a914b037433ce866d4ed363fce8605d45f0c9d2cd6ac52f98"
"sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
"sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
],
"version": "==2.0.12"
"version": "==3.0.12"
},
"flask": {
"hashes": [
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"version": "==1.1.1"
},
"flask-limiter": {
"hashes": [
"sha256:473aa5bc97310406aa8c12ab3dc080697bcfa8cd21a6d0aba30916911bbc673c",
"sha256:8cce98dcf25bf2ddbb824c2b503b4fc8e1a139154240fd2c60d9306bad8a0db8"
],
"version": "==1.0.1"
},
"gunicorn": {
"hashes": [
"sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6",
"sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"
"sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
"sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
],
"version": "==19.9.0"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==19.7.1"
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:2231bace0dfd8d2bf1e5d7e41239c06c9e0ded46e70cc1094a0aa64b0afeb054",
"sha256:ddaa01a212cd6d641401cb01b605f4a4d9f37bfc93043d7f760ec70fb99ff9ff"
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
],
"version": "==2.9.6"
"version": "==2.10.1"
},
"limits": {
"hashes": [
"sha256:9df578f4161017d79f5188609f1d65f6b639f8aad2914c3960c9252e56a0ff95",
"sha256:a017b8d9e9da6761f4574642149c337f8f540d4edfe573fb91ad2c4001a2bc76"
],
"version": "==1.3"
},
"markupsafe": {
"hashes": [
@@ -74,12 +104,41 @@
],
"version": "==1.1.1"
},
"marshmallow": {
"hashes": [
"sha256:9cedfc5b6f568d57e8a2cf3d293fbd81b05e5ef557854008d03e25660a39ccfd",
"sha256:a4d99922116a76e5abd8f997ec0519086e24814b7e1e1344bebe2a312ba50235"
],
"version": "==2.19.5"
},
"redis": {
"hashes": [
"sha256:6946b5dca72e86103edc8033019cc3814c031232d339d5f4533b02ea85685175",
"sha256:8ca418d2ddca1b1a850afa1680a7d2fd1f3322739271de4b704e0d4668449273"
],
"version": "==3.2.1"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"webargs": {
"hashes": [
"sha256:6b81ce44572d4f345104aa41c734fdc01165f054a061a8ebb1b46e89851e1170",
"sha256:713bd63440ee078ce48ca953d254d51e5f1a6fa0c76fb521fc596306c78d95a5",
"sha256:e2394ea7e422c1e795681cee5e8b1c6083bab7db6d7a380841130cbbae173d29"
],
"version": "==5.3.2"
},
"werkzeug": {
"hashes": [
"sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c",
"sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"
],
"version": "==0.15.4"
}
},
"develop": {
@@ -97,13 +156,12 @@
],
"version": "==19.1.0"
},
"beautifulsoup4": {
"click": {
"hashes": [
"sha256:034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858",
"sha256:945065979fb8529dd2f37dbb58f00b661bdbcbebf954f93b32fdf5263ef35348",
"sha256:ba6d5c59906a85ac23dadfe5c88deaf3e179ef565f4898671253e50a78680718"
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==4.7.1"
"version": "==7.0"
},
"coverage": {
"hashes": [
@@ -158,20 +216,87 @@
],
"version": "==1.0.3"
},
"flask": {
"hashes": [
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"version": "==1.1.1"
},
"importlib-metadata": {
"hashes": [
"sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7",
"sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db"
],
"version": "==0.18"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
],
"version": "==2.10.1"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
],
"version": "==1.1.1"
},
"more-itertools": {
"hashes": [
"sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7",
"sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"
"sha256:3ad685ff8512bf6dc5a8b82ebf73543999b657eded8c11803d9ba6b648986f4d",
"sha256:8bb43d1f51ecef60d81854af61a3a880555a14643691cc4b64a6ee269c78f09a"
],
"version": "==7.1.0"
},
"packaging": {
"hashes": [
"sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
"sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
],
"markers": "python_version > '2.7'",
"version": "==7.0.0"
"version": "==19.0"
},
"pluggy": {
"hashes": [
"sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180",
"sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a"
"sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc",
"sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"
],
"version": "==0.11.0"
"version": "==0.12.0"
},
"py": {
"hashes": [
@@ -180,12 +305,19 @@
],
"version": "==1.8.0"
},
"pyparsing": {
"hashes": [
"sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
"sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
],
"version": "==2.4.0"
},
"pytest": {
"hashes": [
"sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24",
"sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6"
"sha256:6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d",
"sha256:a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77"
],
"version": "==4.5.0"
"version": "==5.0.1"
},
"pytest-cov": {
"hashes": [
@@ -194,6 +326,13 @@
],
"version": "==2.7.1"
},
"pytest-flask": {
"hashes": [
"sha256:283730b469604ecb94caac28df99a40b7c785b828dd8d3323596718b51dfaeb2",
"sha256:d874781b622210d8c5d8061cdb091cb059fcb12203125110bd8e6f9256ccbf49"
],
"version": "==0.15.0"
},
"pytest-mock": {
"hashes": [
"sha256:43ce4e9dd5074993e7c021bb1c22cbb5363e612a2b5a76bc6d956775b10758b7",
@@ -222,20 +361,6 @@
],
"version": "==2.1.0"
},
"soupsieve": {
"hashes": [
"sha256:6898e82ecb03772a0d82bd0d0a10c0d6dcc342f77e0701d0ec4a8271be465ece",
"sha256:b20eff5e564529711544066d7dc0f7661df41232ae263619dede5059799cdfca"
],
"version": "==1.9.1"
},
"waitress": {
"hashes": [
"sha256:4e2a6e6fca56d6d3c279f68a2b2cc9b4798d834ea3c3a9db3e2b76b6d66f4526",
"sha256:90fe750cd40b282fae877d3c866255d485de18e8a232e93de42ebd9fb750eebb"
],
"version": "==1.3.0"
},
"wcwidth": {
"hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
@@ -243,19 +368,19 @@
],
"version": "==0.1.7"
},
"webob": {
"werkzeug": {
"hashes": [
"sha256:05aaab7975e0ee8af2026325d656e5ce14a71f1883c52276181821d6d5bf7086",
"sha256:36db8203c67023d68c1b00208a7bf55e3b10de2aa317555740add29c619de12b"
"sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c",
"sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"
],
"version": "==1.8.5"
"version": "==0.15.4"
},
"webtest": {
"zipp": {
"hashes": [
"sha256:41348efe4323a647a239c31cde84e5e440d726ca4f449859264e538d39037fd0",
"sha256:f3a603b8f1dd873b9710cd5a7dd0889cf758d7e1c133b1dae971c04f567e566e"
"sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a",
"sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec"
],
"version": "==2.0.33"
"version": "==0.5.2"
}
}
}

+ 19
- 75
README.md View File

@@ -22,26 +22,7 @@ the API configuration, which is available in `fsfe_forms/configuration/applicati

## Install

It is not expected that you install the forms API code yourself, but if you
choose to do so, you will also need access to a Redis instance.

To setup the environment:

```
$ export REDIS_HOST=redishostname
$ export REDIS_PORT=redispost
$ export REDIS_PASSWORD=redispassword # if required
$ pip install -r requirements.txt
```

You can then run the web app with:

```
$ gunicorn -b 0.0.0.0:8080 fsfe_forms.wsgi:application
```

This will run the web application on port 8080 on the default network
interface.
See the file `doc/install.md`.

## Usage

@@ -56,31 +37,21 @@ The application configuration could look like this:

```json
"totick2": {
"ratelimit": 500,
"required_vars": ["country", "message", "participant_name"],
"to": [ "contact@fsfe.org" ],
"subject": "Registration of event from {{ participant_name }}",
"include_vars": true,
"redirect": "http://fsfe.org",
"template": "totick2-template",
"template": {
"plain": {
"filename": "totick2-template.txt"
},
}
"required_vars": ["participant_name"],
"headers": {
"X-OTRS-Queue": "Promo"
}
},
```

The template configuration could look like this:

```json
"totick2-template": {
"plain": {
"filename": "totick2-template.txt"
},
"required_vars": ["country", "message", "participant_name"],
"headers": {
"X-PARTICIPANT-NAME": "{{ participant_name }}"
}
}
},
```

The HTML form could look like this:
@@ -108,6 +79,7 @@ I'm from {{ country }} and would like you to know:
```

### Signing an open letter

In this case, we're publishing an open letter which we invite people to
sign. We want to store information about who has signed the open letter,
and we want a double opt-in of their email address so we know we have
@@ -118,30 +90,22 @@ The configuration could look like this:

```json
"tosign": {
"ratelimit": 500,
"required_vars": ["name", "confirm", "country"]
"from": "admin@fsfe.org",
"confirmation-from": "admin@fsfe.org",
"to": [ "campaignowner@fsfe.org" ],
"subject": "New signatory to open letter",
"include_vars": true,
"redirect": "http://fsfe.org",
"template": "tosign-template",
"template": {
"plain": {
"filename": "tosign-template.txt",
},
}
"store": "/store/campaign2.json",
"confirm": true,
},
```

The template configuration could look like this:

```json
"tosign-template": {
"plain": {
"filename": "tosign-template.txt",
},
"required_vars": ["name", "confirm", "country"]
}
```

The HTML form could look like this:

```html
@@ -236,10 +200,10 @@ according to the configuration. The following parameters are supported:
This will confirm an e-mail address if using double opt-in. The following
parameters are supported:

* confirm (required)
* id (required)

The value for confirm is generated automatically by the forms system. You
should never need to generate this URL yourself.
The id is generated automatically by the forms system. You should never need to
generate this URL yourself.

### Supported parameters for each registered application user

@@ -265,8 +229,6 @@ instead.

The following parameters are available only in the API configuration file:

* **ratelimit**: controls the number of emails allowed to be sent per hour
* **include_vars**: if set to true, then any extra variables provided in a GET request will be made available to the template when rendering an email
* **store**: if set to a filename, then information about emails sent will be stored in this file. This will not inclue emails which have not been confirmed (if double opt-in is in use).
* **confirm**: if set to true, then no email is sent without an explicit confirmation of a suitable e-mail address. The email to confirm should be passed in the **confirm** parameter of the GET request (see later)
* **redirect**: address to redirect the user to after having accepted and processed a request
@@ -281,24 +243,7 @@ The following parameters are available only in the API configuration file:

## Contribute

### Testing in the git checkout directory

The repository contains automatic functional tests which can be run from the
git checkout directory, without installing anything.

When you have checked out the git repository, you can use the command
```bash
make virtualenv
```
to set up a virtual environment for this project, where all dependencies will
be contained. After you have done that, you can at any time run
```bash
make pytest
```
to run the automatic functional tests.

All these tests work against simulated Redis and email servers, so there is no
need for a real Redis instance or a connection to an email server.
See the file `doc/hack.md`.

### Testing in a local docker container

@@ -328,4 +273,3 @@ On `http://localhost:1080` you can then see the sent emails.
This software is copyright 2019 by the Free Software Foundation Europe e.V.
and licensed under the GPLv3 license. For details see the "LICENSE" file in
the top level directory of https://git.fsfe.org/fsfe-system-hackers/forms/


+ 75
- 0
doc/configure.md View File

@@ -0,0 +1,75 @@
# How to configure fsfe-forms

Configuration parameters for fsfe-forms must be set through environment
variables.

The configuration for the production instance of fsfe-forms is set in
[`docker-compose.yml`]. On the other hand, the file [`.env`], which is read
automatically when entering the “pipenv” virtual environment, contains settings
suitable for testing and debugging.


## Flask server settings

These settings are only relevant for the Flask builtin web server, which is
very nice for testing and debugging, but not recommended for production.


### `FLASK_SKIP_DOTENV`

In the virtual environment setup used for the development of fsfe-forms, the
.env file is parsed by `pipenv`, so we always set this to `1`.


### `FLASK_APP`

This tells the Flask server where to find the application object. Always set to
`fsfe_cd_front`.


### `FLASK_ENV`

Since we don't use the Flask server for production, this is always set to
`development`.


### `FLASK_RUN_HOST`

The hostname to listen on.


### `FLASK_RUN_PORT`

The TCP port to listen on.


## Email settings

### `SMTP_HOST` and `SMTP_PORT`

The SMTP server and port to use for sending out all kinds of emails. Defaults
to `localhost` and `25`.


### `SMTP_USERNAME` and `SMTP_PASSWORD`

The credentials for the SMTP server. Only needed if the SMTP server requires
authentication.


### `LOG_EMAIL_FROM` and `LOG_EMAIL_TO`

In a production environment, fsfe-forms sends log messages of severity
“ERROR” or worse by email. These are the “From” and “To“ address for these
emails.


## Parameters for the connection to the Redis server

### `REDIS_HOST` and `REDIS_PORT`

Hostname and TCP port of the Redis server.


[`docker-compose.yml`]: ../docker-compose.yml
[`.env`]: ../.env

+ 54
- 0
doc/hack.md View File

@@ -0,0 +1,54 @@
# How to hack on fsfe-forms

## Development environment setup

The (strongly) recommended way of developing, testing and debugging fsfe-forms
is to set up an isolated Python environment, called a *virtual environment* or
*venv*, to make development independent from the operating system provided
version of the required Python libraries. To make this as easy as possible,
fsfe-forms uses [Pipenv](https://docs.pipenv.org/en/latest/).

After cloning the git repository, just run `make virtualenv` in the git
checkout directory and the virtual environment will be completely set up.


## Coding style

fsfe-forms follows [PEP 8](https://pep8.org/). Additionally, imports are sorted
alphabetically; you can run `make applyisort` to let
[isort](https://pypi.org/project/isort/) do that for you.


## Testing and debugging environment

fsfe-forms can be run from the git checkout directory for testing and
debugging.

Please note that fsfe-forms requires access to a number of external systems
to run properly, most notably a mail server and a redis server.

When you have set up all that, you can run `make flask` to run fsfe-forms
with Flask's built-in web server in debug mode. Alternatively, you can run
`make gunicorn` to use the gunicorn web server, which is the variant used in
production.

For all the above commands, the [configuration variables](configure.md) defined
in the file [`.env`](../.env) are used. They should work for most development
setups, but if you need to do any modification, make sure that you don't
accidentally commit your changes.


## Automatic quality checks

The following commands are available for automatic quality checks:

* `make isort` to verify the correct sorting of imports.
* `make lint` to verify the compliance with coding standards.
* `make pytest` to run the functional tests defined in the [test](../test)
directory, again using the [configuration variables](configure.md) defined in
the file [`.env`](../.env).
* `make quality` to run all of the above tests.

All these tests are also run during the deployment process, and updating the
code on the production server is refused if any of the tests fails, so it is
strongly recommended that you run `make quality` before committing any change.

+ 69
- 0
doc/install.md View File

@@ -0,0 +1,69 @@
# How to install fsfe-forms

## Requirements

The file [`Pipfile`] lists all Python dependencies of fsfe-forms, and
[`Pipfile.lock`] contains information about the actual versions of these
dependencies recommended for use. You can use `pipenv install --system` to
download and install all these dependencies on your computer.

Please note that fsfe-forms requires access to a number of external systems
to run properly, most notably a mail server and a redis server.


## Local install

Run `./setup.py install` in the git checkout directory to install fsfe-forms
on the local machine. There are a number of options to select the installation
target, for example installing with a specific prefix, or installing in a home
directory to be able to install without root permissions. Run `./setup.py
install --help` for more information. Run `./setup.py --help-commands` for a
list of other tasks you can do with `setup.py`.

[`setup.py`] installs all Python files and uses [`MANIFEST.in`] to determine
which additional files to install.


## Docker image build

The [`Dockerfile`] contains build instructions for a Docker container in which
fsfe-forms can run. After installing the requirements, it installs fsfe-forms
using `setup.py install`, all as described in the previous sections.

Within the Docker container, fsfe-forms runs as non-privilleged user “fsfe” for
security reasons.


## Automatic deployment

fsfe-forms uses [drone](https://drone.fsfe.org) to automatically deploy updates
to the production server.

Upon each push to the master branch of the git repository, drone creates a
temporary clone of the repository and then sequentially executes the following
steps defined in [`.drone.yml`]:

1. *build-quality*: use [`docker-compose`] with [`docker-compose-quality.yml`]
as a wrapper around [`Dockerfile-quality`] to create a docker image for
automatic quality checks.

2. *quality*: in a container with the previously created image, run a number of
quality checks to ensure no obviously broken code is deployed to the
production server.

3. *deploy*: again, use [`docker-compose`], this time to create the actual
docker image and start the corresponding container. The file
[`docker-compose.yml`] defines the parameters for this step, referring to
the [`Dockerfile`] described in the previous section.


[`Pipfile`]: ../Pipfile
[`Pipfile.lock`]: ../Pipfile.lock
[`setup.py`]: ../setup.py
[`MANIFEST.in`]: ../MANIFEST.in
[`Dockerfile`]: ../Dockerfile
[`Dockerfile-quality`]: ../Dockerfile-quality
[`docker-compose`]: https://docs.docker.com/compose/
[`docker-compose.yml`]: ../docker-compose.yml
[`docker-compose-quality.yml`]: ../docker-compose-quality.yml
[`.drone.yml`]: ../.drone.yml

+ 28
- 9
docker-compose.dev.yml View File

@@ -1,19 +1,38 @@
# =============================================================================
# Deployment instructions for the developer's Docker container
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

version: '3'
services:

forms-web:
forms:
ports:
- "8080:8080"
- "8080:8080"
environment:
- SMTP_HOST=forms-fakesmtp
- SMTP_PORT=1025
"SMTP_HOST": "forms-fakesmtp"
"SMTP_PORT": "1025"
"LOG_EMAIL_FROM": "contact@fsfe.org"
"LOG_EMAIL_TO": "contact@fsfe.org"

forms-fakesmtp:
container_name: forms-fakesmtp
image: forms-fakesmtp
build:
context: ./fake-smtp-server
dockerfile: Dockerfile-smtp
image: forms-fakesmtp
container_name: forms-fakesmtp
ports:
- "1025:1025"
- "1080:1080"
- "1025:1025"
- "1080:1080"

+ 43
- 23
docker-compose.yml View File

@@ -1,38 +1,58 @@
# =============================================================================
# Deployment instructions for the Docker container
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

version: '3'
services:
forms-redis:
container_name: forms-redis
image: redis:3.2
restart: always
container_name: forms-redis
labels:
fsfe-monitoring: "true"
expose:
- 6379

forms-web:
container_name: forms-web
build:
context: .
dockerfile: Dockerfile-web
image: forms-web
restart: always
expose:
- 8080

forms:
depends_on:
- forms-redis
image: forms
build: .
container_name: forms
labels:
fsfe-monitoring: "true"
environment:
- VIRTUAL_HOST=forms.fsfe.org
- LETSENCRYPT_HOST=forms.fsfe.org
- LETSENCRYPT_EMAIL=contact@fsfe.org
- REDIS_HOST=forms-redis
- REDIS_PORT=6379
- SMTP_HOST=mail.fsfe.org
- SMTP_PORT=25
VIRTUAL_HOST: "forms.fsfe.org"
LETSENCRYPT_HOST: "forms.fsfe.org"
LETSENCRYPT_EMAIL: "contact@fsfe.org"
RATELIMIT_DEFAULT: "1 per second, 5 per minute, 20 per hour"
SMTP_HOST: "mail.fsfe.org"
LOG_EMAIL_FROM: "contact@fsfe.org"
LOG_EMAIL_TO: "contact@fsfe.org"
REDIS_HOST: "forms-redis"
REDIS_PORT: "6379"
volumes:
- "/srv/forms:/store:rw"
restart: always

# Connect the container which exposes the service to the 'bridge' network as
# this is where the reverse proxy is
connect-bridge:
depends_on:
- forms
image: docker:dind
volumes:
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- forms-web
command: /bin/sh -c 'docker network connect bridge forms-web'
command: docker network connect bridge forms

+ 21
- 0
fsfe_forms/__init__.py View File

@@ -0,0 +1,21 @@
# =============================================================================
# Main module
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

# pylama:ignore=W0611

from fsfe_forms.app import create_app

+ 81
- 0
fsfe_forms/app.py View File

@@ -0,0 +1,81 @@
# =============================================================================
# WSGI application
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

from logging import ERROR, INFO, Formatter, getLogger
from logging.handlers import SMTPHandler

from flask import Flask
from flask.logging import default_handler
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from werkzeug.middleware.proxy_fix import ProxyFix

from fsfe_forms.common import config
from fsfe_forms.views import email, confirm


# =============================================================================
# Main application factory
# =============================================================================

def create_app(testing=False):
app = Flask(__name__.split('.')[0])

# This enables Flask-Limiter to detect the real remote address even though
# fsfe-forms runs behind a proxy.
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)

# Read configuration
app.config.from_object(config)
if testing:
app.config['TESTING'] = True

# Configure the root logger
root_logger = getLogger()
root_logger.setLevel(INFO)

# Add the flask default log handler
default_handler.setFormatter(Formatter(
'[%(asctime)s] (%(name)s) %(levelname)s: %(message)s'))
root_logger.addHandler(default_handler)

# Add a log handler which forwards errors by email
if not (app.debug or app.testing): # pragma: no cover
if app.config['SMTP_USERNAME'] is not None:
credentials = (
app.config['SMTP_USERNAME'],
app.config['SMTP_PASSWORD'])
else:
credentials = None
handler = SMTPHandler(
mailhost=app.config['SMTP_HOST'],
fromaddr=app.config['LOG_EMAIL_FROM'],
toaddrs=[app.config['LOG_EMAIL_TO']],
subject="Log message from fsfe-forms",
credentials=credentials)
handler.setLevel(ERROR)
root_logger.addHandler(handler)

# Initialize Flask-Limiter
app.limiter = Limiter(app, key_func=get_remote_address)

# Register views
app.add_url_rule(rule="/email", view_func=email, methods=["GET", "POST"])
app.add_url_rule(rule="/confirm", view_func=confirm)

return app

+ 17
- 0
fsfe_forms/background/__init__.py View File

@@ -0,0 +1,17 @@
# =============================================================================
# Subpackage main file
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

+ 0
- 1
fsfe_forms/background/tasks.py View File

@@ -1,7 +1,6 @@
import uuid

from typing import Union
from fsfe_forms.common import exceptions
from fsfe_forms.common.config import DEFAULT_SUBJECT_LANG
from fsfe_forms.common.configurator import configuration, AppConfig
from fsfe_forms.common.services import DeliveryService, SenderStorageService, TemplateService

+ 17
- 0
fsfe_forms/common/__init__.py View File

@@ -0,0 +1,17 @@
# =============================================================================
# Subpackage main file
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

+ 10
- 3
fsfe_forms/common/config.py View File

@@ -1,11 +1,18 @@
import os

REDIS_HOST = os.environ.get('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379))
REDIS_PASSWORD = os.environ.get('REDIS_PASSWORD', None)
RATELIMIT_DEFAULT = os.environ.get('RATELIMIT_DEFAULT')

SMTP_HOST = os.environ.get('SMTP_HOST', 'localhost')
SMTP_PORT = int(os.environ.get('SMTP_PORT', 25))
SMTP_USERNAME = os.environ.get('SMTP_USERNAME')
SMTP_PASSWORD = os.environ.get('SMTP_PASSWORD')

LOG_EMAIL_FROM = os.environ.get('LOG_EMAIL_FROM')
LOG_EMAIL_TO = os.environ.get('LOG_EMAIL_TO')

REDIS_HOST = os.environ.get('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379))
REDIS_PASSWORD = os.environ.get('REDIS_PASSWORD', None)

LOCK_FILENAME = os.environ.get('LOCK_FILENAME', '/tmp/forms.lock')


+ 0
- 13
fsfe_forms/common/exceptions.py View File

@@ -1,13 +0,0 @@
class SomethingWrong(Exception):
def __init__(self, message='Something wrong'):
self.message = message


class BadRequest(SomethingWrong):
def __init__(self, message='Bad Request'):
self.message = message


class NotFound(SomethingWrong):
def __init__(self, message='Not Found'):
self.message = message

+ 2
- 2
fsfe_forms/common/models.py View File

@@ -41,7 +41,7 @@ class SendData(Serializable):
for name in data:
if name in ['from', 'to', 'replyto', 'subject', 'template', 'appid']:
continue
request_data[name] = getattr(data, name)
request_data[name] = data[name]
return cls(appid, send_from, send_to, reply_to, subject, template, confirm, False, request_data, url, lang)

def toJSON(self):
@@ -66,4 +66,4 @@ class SendData(Serializable):
def get_secure_lang(lang):
if lang and re.match('[a-z]{2}$', lang):
return lang
return None
return None

+ 11
- 10
fsfe_forms/common/services/SenderService.py View File

@@ -1,8 +1,9 @@
import uuid
from typing import Optional

from flask import abort

from fsfe_forms.background.tasks import schedule_confirmation, schedule_email
from fsfe_forms.common import exceptions
from fsfe_forms.common.config import CONFIRMATION_EXPIRATION_SECS
from fsfe_forms.common.configurator import AppConfig, configuration
from fsfe_forms.common.models import SendData
@@ -12,22 +13,22 @@ from fsfe_forms.common.services import SenderStorageService
def validate_and_send_email(data: SendData):
config = configuration.get_config(data)
if config is None:
raise exceptions.NotFound('Configuration not found for this AppId')
abort(404, 'Configuration not found for this AppId')
if config.send_from is None:
raise exceptions.BadRequest('\"From\" is required')
abort(400, '\"From\" is required')
if config.send_to is None or not config.send_to:
raise exceptions.BadRequest('\"To\" is required')
abort(400, '\"To\" is required')
if config.subject is None:
raise exceptions.BadRequest('\"Subject\" is required')
abort(400, '\"Subject\" is required')
if config.template is None:
raise exceptions.BadRequest('\"Template\" is required')
abort(400, '\"Template\" is required')
for field in config.required_vars:
if field not in data.request_data:
raise exceptions.BadRequest('\"%s\" is required' % field)
raise abort(400, '\"%s\" is required' % field)

if config.confirm:
if data.confirm is None:
raise exceptions.BadRequest('\"Confirm\" address is required')
abort(400, '\"Confirm\" address is required')
id = SenderStorageService.store_data(data, CONFIRMATION_EXPIRATION_SECS)
schedule_confirmation(id, data, config)
else:
@@ -40,10 +41,10 @@ def confirm_email(id: str) -> Optional[AppConfig]:
id = uuid.UUID(id)
data = SenderStorageService.resolve_data(id)
if data is None:
raise exceptions.NotFound('Confirmation ID is Not Found')
abort(404, 'Confirmation ID is Not Found')
config = configuration.get_config(data)
if config is None:
raise exceptions.NotFound('Configuration not found for this AppId')
abort(404, 'Configuration not found for this AppId')
if not data.confirmed:
data.confirmed = True
SenderStorageService.update_data(id, data)

+ 2
- 8
fsfe_forms/common/services/TemplateRenderService.py View File

@@ -1,11 +1,5 @@
import re
from bottle import jinja2_template as template

containing_variables_pattern = re.compile('{{.+?}}', re.MULTILINE)
from jinja2 import Template


def render_content(contents, data: dict):
if containing_variables_pattern.search(contents) is None:
return contents
else:
return template(contents, data)
return Template(contents).render(data)

+ 17
- 0
fsfe_forms/common/services/__init__.py View File

@@ -0,0 +1,17 @@
# =============================================================================
# Subpackage main file
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

+ 0
- 5
fsfe_forms/debug_server.py View File

@@ -1,5 +0,0 @@
from fsfe_forms.web.controller import *
from bottle import run

if __name__ == '__main__':
run(host='localhost', port=8080, debug=True)

+ 53
- 0
fsfe_forms/views.py View File

@@ -0,0 +1,53 @@
# =============================================================================
# Endpoints for the WSGI server
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

from flask import redirect, request
from webargs.fields import String
from webargs.flaskparser import use_kwargs

from fsfe_forms.common.models import SendData
from fsfe_forms.common.services import SenderService


# =============================================================================
# Registration endpoint
# =============================================================================

email_parameters = {
"appid": String(required=True)}


@use_kwargs(email_parameters)
def email(appid):
send_data = SendData.from_request(appid, request.values, request.url)
config = SenderService.validate_and_send_email(send_data)
return redirect(config.redirect)


# =============================================================================
# Confirmation endpoint
# =============================================================================

confirm_parameters = {
"id": String(required=True)}


@use_kwargs(confirm_parameters)
def confirm(id):
config = SenderService.confirm_email(id)
return redirect(config.redirect_confirmed or config.redirect)

+ 0
- 0
fsfe_forms/web/__init__.py View File


+ 0
- 56
fsfe_forms/web/controller.py View File

@@ -1,56 +0,0 @@
import redis
from bottle import route, request, redirect, abort
from fsfe_forms.common import exceptions
from fsfe_forms.common.models import SendData
from fsfe_forms.common.services import SenderService


def error_handler(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except exceptions.BadRequest as e:
abort(400, e.message)
except exceptions.NotFound as e:
abort(404, e.message)
except redis.exceptions.ConnectionError as e:
abort(500, 'Database connection failed')

return wrapper


def id_extractor(id_name, method):
def decorator(function):
def wrapper(*args, **kwargs):
_id = getattr(request, method).get(id_name)
if _id:
return function(_id, *args, **kwargs)
raise exceptions.BadRequest
return wrapper
return decorator


@route('/email', method='GET')
@error_handler
@id_extractor('appid', 'GET')
def email_get(appid):
send_data = SendData.from_request(appid, request.GET, request.url)
config = SenderService.validate_and_send_email(send_data)
return redirect(config.redirect)


@route('/email', method='POST')
@error_handler
@id_extractor('appid', 'POST')
def email_post(appid):
send_data = SendData.from_request(appid, request.forms, request.url)
config = SenderService.validate_and_send_email(send_data)
return redirect(config.redirect)


@route('/confirm', method='GET')
@error_handler
@id_extractor('id', 'GET')
def confirmation(id):
config = SenderService.confirm_email(id)
return redirect(config.redirect_confirmed or config.redirect)

+ 0
- 4
fsfe_forms/wsgi.py View File

@@ -1,4 +0,0 @@
from fsfe_forms.web.controller import *
from bottle import default_app, run

application = default_app()

+ 33
- 0
setup.py View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
# =============================================================================
# Install the FSFE Form Server locally
# =============================================================================
# This file is part of the FSFE Form Server.
#
# Copyright © 2017-2019 Free Software Foundation Europe <contact@fsfe.org>
#
# The FSFE Form Server is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# The FSFE Form Server is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details <http://www.gnu.org/licenses/>.
# =============================================================================

from setuptools import find_packages, setup


setup(
name="fsfe-forms",
description="FSFE Form Server",
url="https://git.fsfe.org/fsfe-system-hackers/forms",
author="Free Software Foundation Europe",
author_email="contact@fsfe.org",
license="GPL",
packages=find_packages(exclude=["test"]),
include_package_data=True,
zip_safe=False
)

+ 6
- 7
test/conftest.py View File

@@ -17,7 +17,6 @@
# =============================================================================

import pytest
import webtest
from fakeredis import FakeRedis


@@ -59,8 +58,8 @@ def file_mock(mocker):
def app(redis_mock):
# Redis must be patched before we import the app, since the connection is
# created in module init code.
from fsfe_forms import wsgi
return webtest.TestApp(wsgi.application)
from fsfe_forms import create_app
return create_app(testing=True)


# -----------------------------------------------------------------------------
@@ -68,10 +67,10 @@ def app(redis_mock):
# -----------------------------------------------------------------------------

@pytest.fixture
def signed_up(app):
app.get(
url='/email',
params={
def signed_up(client):
client.get(
path='/email',
data={
'appid': 'pmpc-sign',
'name': "THE NAME",
'confirm': 'EMAIL@example.com'})

+ 13
- 13
test/test_confirm.py View File

@@ -17,10 +17,10 @@
# =============================================================================


def test_confirm(app, smtp_mock, redis_mock, file_mock, signed_up):
response = app.get(
url='/confirm',
params={'id': signed_up})
def test_confirm(client, smtp_mock, redis_mock, file_mock, signed_up):
response = client.get(
path='/confirm',
data={'id': signed_up})
assert response.status_code == 302
assert response.location == 'https://publiccode.eu/openletter/success'
# Check logfile written.
@@ -38,15 +38,15 @@ def test_confirm(app, smtp_mock, redis_mock, file_mock, signed_up):
assert "THE NAME" in email[2]


def test_confirm_no_id(app):
response = app.get(
url='/confirm',
status=400)
def test_confirm_no_id(client):
response = client.get(
path='/confirm')
assert response.status_code == 422


# FIXME: not well handled yet.
#def test_confirm_bad_id(app):
# response = app.get(
# url='/confirm',
# params={'id': 'BAD-ID'},
# status=500)
#def test_confirm_bad_id(client):
# response = client.get(
# path='/confirm',
# data={'id': 'BAD-ID'},
# assert response.status_code == 404

+ 34
- 34
test/test_email.py View File

@@ -25,10 +25,10 @@
# Without confirmation
# -----------------------------------------------------------------------------

def test_email_get(app, smtp_mock, redis_mock, file_mock):
response = app.get(
url='/email',
params={
def test_email_get(client, smtp_mock, redis_mock, file_mock):
response = client.get(
path='/email',
data={
'appid': 'contact',
'from': 'EMAIL@example.com',
'subject': "EMAIL-SUBJECT",
@@ -51,27 +51,27 @@ def test_email_get(app, smtp_mock, redis_mock, file_mock):
assert "EMAIL-CONTENT" in email[2]


def test_email_get_no_params(app):
response = app.get(
url='/email',
status=400)
def test_email_get_no_params(client):
response = client.get(
path='/email')
assert response.status_code == 422


def test_email_get_bad_appid(app):
response = app.get(
url='/email',
params={'appid': 'BAD-APPID'},
status=404)
def test_email_get_bad_appid(client):
response = client.get(
path='/email',
data={'appid': 'BAD-APPID'})
assert response.status_code == 404


# -----------------------------------------------------------------------------
# With confirmation
# -----------------------------------------------------------------------------

def test_email_get_with_confirmation(app, smtp_mock, redis_mock, file_mock):
response = app.get(
url='/email',
params={
def test_email_get_with_confirmation(client, smtp_mock, redis_mock, file_mock):
response = client.get(
path='/email',
data={
'appid': 'pmpc-sign',
'name': "THE NAME",
'confirm': 'EMAIL@example.com'})
@@ -90,10 +90,10 @@ def test_email_get_with_confirmation(app, smtp_mock, redis_mock, file_mock):
assert "Subject: Public Code: Please confirm your signature" in email[2]


def test_email_get_duplicate(app, smtp_mock, redis_mock, file_mock, signed_up):
response = app.get(
url='/email',
params={
def test_email_get_duplicate(client, smtp_mock, redis_mock, file_mock, signed_up):
response = client.get(
path='/email',
data={
'appid': 'pmpc-sign',
'name': "THE NAME",
'confirm': 'EMAIL@example.com'})
@@ -120,10 +120,10 @@ def test_email_get_duplicate(app, smtp_mock, redis_mock, file_mock, signed_up):
# Without confirmation
# -----------------------------------------------------------------------------

def test_email_post(app, smtp_mock, redis_mock, file_mock):
response = app.post(
url='/email',
params={
def test_email_post(client, smtp_mock, redis_mock, file_mock):
response = client.post(
path='/email',
data={
'appid': 'contact',
'from': 'EMAIL@example.com',
'subject': "EMAIL-SUBJECT",
@@ -146,14 +146,14 @@ def test_email_post(app, smtp_mock, redis_mock, file_mock):
assert "EMAIL-CONTENT" in email[2]


def test_email_post_no_params(app):
response = app.post(
url='/email',
status=400)
def test_email_post_no_params(client):
response = client.post(
path='/email')
assert response.status_code == 422


def test_email_post_bad_appid(app):
response = app.post(
url='/email',
params={'appid': 'BAD-APPID'},
status=404)
def test_email_post_bad_appid(client):
response = client.post(
path='/email',
data={'appid': 'BAD-APPID'})
assert response.status_code == 404

Loading…
Cancel
Save