From 5fcb5620b7f0825601bf0429e17263842cf84724 Mon Sep 17 00:00:00 2001 From: AXON Date: Wed, 30 Aug 2023 04:27:05 -0700 Subject: [PATCH] Dynamically generate template and resolve existing issues (#11) --- .gitignore | 2 +- .gitpod.yml | 36 ++++++-- LICENSE | 21 +++++ LICENSE.rst | 28 ------ MANIFEST.in | 6 -- README.md | 4 +- flaskr/__init__.py | 50 ----------- flaskr/auth.py | 116 ------------------------ flaskr/blog.py | 125 -------------------------- flaskr/db.py | 54 ----------- flaskr/schema.sql | 20 ----- flaskr/static/style.css | 134 ---------------------------- flaskr/templates/auth/login.html | 15 ---- flaskr/templates/auth/register.html | 15 ---- flaskr/templates/base.html | 24 ----- flaskr/templates/blog/create.html | 15 ---- flaskr/templates/blog/index.html | 28 ------ flaskr/templates/blog/update.html | 19 ---- setup.cfg | 28 ------ setup.py | 4 - tests/conftest.py | 62 ------------- tests/data.sql | 8 -- tests/test_auth.py | 69 -------------- tests/test_blog.py | 83 ----------------- tests/test_db.py | 29 ------ tests/test_factory.py | 12 --- 26 files changed, 51 insertions(+), 956 deletions(-) create mode 100644 LICENSE delete mode 100644 LICENSE.rst delete mode 100644 MANIFEST.in delete mode 100644 flaskr/__init__.py delete mode 100644 flaskr/auth.py delete mode 100644 flaskr/blog.py delete mode 100644 flaskr/db.py delete mode 100644 flaskr/schema.sql delete mode 100644 flaskr/static/style.css delete mode 100644 flaskr/templates/auth/login.html delete mode 100644 flaskr/templates/auth/register.html delete mode 100644 flaskr/templates/base.html delete mode 100644 flaskr/templates/blog/create.html delete mode 100644 flaskr/templates/blog/index.html delete mode 100644 flaskr/templates/blog/update.html delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tests/conftest.py delete mode 100644 tests/data.sql delete mode 100644 tests/test_auth.py delete mode 100644 tests/test_blog.py delete mode 100644 tests/test_db.py delete mode 100644 tests/test_factory.py diff --git a/.gitignore b/.gitignore index 85a3584..a306afb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -venv/ +.venv/ *.pyc __pycache__/ instance/ diff --git a/.gitpod.yml b/.gitpod.yml index de0dd20..a25d698 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,17 +1,35 @@ +# Image source code: https://github.com/axonasif/workspace-images/tree/tmp +# Also see: https://github.com/gitpod-io/workspace-images/issues/1071 +image: axonasif/workspace-python@sha256:f5ba627a31505ea6cf100abe8e552d7ff9e0abd6ba46745b6d6dab349c001430 + +tasks: + - name: Init project and run server + init: | + PROJECT_NAME="flaskr" + + if test ! -e "${PROJECT_NAME}"; then { + tmp_dir=/tmp/.fgc + git clone --filter=tree:0 https://github.com/pallets/flask "${tmp_dir}" + mv "${tmp_dir}/examples/tutorial"/{.*,*} . 2>/dev/null + rm -rf "${tmp_dir}" + } fi + + pip install -e . + if test -e "${PROJECT_NAME}"; then { + flask --app "${PROJECT_NAME}" init-db + } fi + + command: | + PROJECT_NAME="flaskr" + + flask --app "${PROJECT_NAME}" run --debug + ports: - port: 5000 onOpen: open-preview description: Flask App preview -tasks: - - init: | - export FLASK_APP=flaskr - export FLASK_ENV=development - pip install -e . - flask init-db - - command: | - flask run - vscode: extensions: - ms-python.python + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e786ac1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Gitpod + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE.rst b/LICENSE.rst deleted file mode 100644 index 9d227a0..0000000 --- a/LICENSE.rst +++ /dev/null @@ -1,28 +0,0 @@ -Copyright 2010 Pallets - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 97d55d5..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include LICENSE.rst -include flaskr/schema.sql -graft flaskr/static -graft flaskr/templates -graft tests -global-exclude *.pyc diff --git a/README.md b/README.md index 6bc5d95..c2b6dcf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # A Python Flask template on Gitpod -This is a [Python Flask](https://flask.palletsprojects.com/en/2.0.x) template configured for ephemeral cloud development environments on [Gitpod](https://www.gitpod.io/). +This is a [Python Flask](https://flask.palletsprojects.com/) template generator configured for ephemeral cloud development environments on [Gitpod](https://www.gitpod.io/). ## Next Steps @@ -16,4 +16,4 @@ Click the above "Open in Gitpod" button to start a new workspace. Once you're re ### An existing project -To get started with Python Flask on Gitpod, add a [`.gitpod.yml`](./.gitpod.yml) file which contains the configuration to improve the developer experience on Gitpod. To learn more, please see the [Getting Started](https://www.gitpod.io/docs/getting-started) documentation. +Copy and commit the [`.gitpod.yml`](./.gitpod.yml) to your existing project to have the same configuration of this template. To learn more, please see the [Getting Started](https://www.gitpod.io/docs/getting-started) documentation. diff --git a/flaskr/__init__.py b/flaskr/__init__.py deleted file mode 100644 index bb9cce5..0000000 --- a/flaskr/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -import os - -from flask import Flask - - -def create_app(test_config=None): - """Create and configure an instance of the Flask application.""" - app = Flask(__name__, instance_relative_config=True) - app.config.from_mapping( - # a default secret that should be overridden by instance config - SECRET_KEY="dev", - # store the database in the instance folder - DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"), - ) - - if test_config is None: - # load the instance config, if it exists, when not testing - app.config.from_pyfile("config.py", silent=True) - else: - # load the test config if passed in - app.config.update(test_config) - - # ensure the instance folder exists - try: - os.makedirs(app.instance_path) - except OSError: - pass - - @app.route("/hello") - def hello(): - return "Hello, World!" - - # register the database commands - from flaskr import db - - db.init_app(app) - - # apply the blueprints to the app - from flaskr import auth, blog - - app.register_blueprint(auth.bp) - app.register_blueprint(blog.bp) - - # make url_for('index') == url_for('blog.index') - # in another app, you might define a separate main index here with - # app.route, while giving the blog blueprint a url_prefix, but for - # the tutorial the blog will be the main index - app.add_url_rule("/", endpoint="index") - - return app diff --git a/flaskr/auth.py b/flaskr/auth.py deleted file mode 100644 index bcd3c67..0000000 --- a/flaskr/auth.py +++ /dev/null @@ -1,116 +0,0 @@ -import functools - -from flask import Blueprint -from flask import flash -from flask import g -from flask import redirect -from flask import render_template -from flask import request -from flask import session -from flask import url_for -from werkzeug.security import check_password_hash -from werkzeug.security import generate_password_hash - -from flaskr.db import get_db - -bp = Blueprint("auth", __name__, url_prefix="/auth") - - -def login_required(view): - """View decorator that redirects anonymous users to the login page.""" - - @functools.wraps(view) - def wrapped_view(**kwargs): - if g.user is None: - return redirect(url_for("auth.login")) - - return view(**kwargs) - - return wrapped_view - - -@bp.before_app_request -def load_logged_in_user(): - """If a user id is stored in the session, load the user object from - the database into ``g.user``.""" - user_id = session.get("user_id") - - if user_id is None: - g.user = None - else: - g.user = ( - get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone() - ) - - -@bp.route("/register", methods=("GET", "POST")) -def register(): - """Register a new user. - - Validates that the username is not already taken. Hashes the - password for security. - """ - if request.method == "POST": - username = request.form["username"] - password = request.form["password"] - db = get_db() - error = None - - if not username: - error = "Username is required." - elif not password: - error = "Password is required." - elif ( - db.execute("SELECT id FROM user WHERE username = ?", (username,)).fetchone() - is not None - ): - error = f"User {username} is already registered." - - if error is None: - # the name is available, store it in the database and go to - # the login page - db.execute( - "INSERT INTO user (username, password) VALUES (?, ?)", - (username, generate_password_hash(password)), - ) - db.commit() - return redirect(url_for("auth.login")) - - flash(error) - - return render_template("auth/register.html") - - -@bp.route("/login", methods=("GET", "POST")) -def login(): - """Log in a registered user by adding the user id to the session.""" - if request.method == "POST": - username = request.form["username"] - password = request.form["password"] - db = get_db() - error = None - user = db.execute( - "SELECT * FROM user WHERE username = ?", (username,) - ).fetchone() - - if user is None: - error = "Incorrect username." - elif not check_password_hash(user["password"], password): - error = "Incorrect password." - - if error is None: - # store the user id in a new session and return to the index - session.clear() - session["user_id"] = user["id"] - return redirect(url_for("index")) - - flash(error) - - return render_template("auth/login.html") - - -@bp.route("/logout") -def logout(): - """Clear the current session, including the stored user id.""" - session.clear() - return redirect(url_for("index")) diff --git a/flaskr/blog.py b/flaskr/blog.py deleted file mode 100644 index 3704626..0000000 --- a/flaskr/blog.py +++ /dev/null @@ -1,125 +0,0 @@ -from flask import Blueprint -from flask import flash -from flask import g -from flask import redirect -from flask import render_template -from flask import request -from flask import url_for -from werkzeug.exceptions import abort - -from flaskr.auth import login_required -from flaskr.db import get_db - -bp = Blueprint("blog", __name__) - - -@bp.route("/") -def index(): - """Show all the posts, most recent first.""" - db = get_db() - posts = db.execute( - "SELECT p.id, title, body, created, author_id, username" - " FROM post p JOIN user u ON p.author_id = u.id" - " ORDER BY created DESC" - ).fetchall() - return render_template("blog/index.html", posts=posts) - - -def get_post(id, check_author=True): - """Get a post and its author by id. - - Checks that the id exists and optionally that the current user is - the author. - - :param id: id of post to get - :param check_author: require the current user to be the author - :return: the post with author information - :raise 404: if a post with the given id doesn't exist - :raise 403: if the current user isn't the author - """ - post = ( - get_db() - .execute( - "SELECT p.id, title, body, created, author_id, username" - " FROM post p JOIN user u ON p.author_id = u.id" - " WHERE p.id = ?", - (id,), - ) - .fetchone() - ) - - if post is None: - abort(404, f"Post id {id} doesn't exist.") - - if check_author and post["author_id"] != g.user["id"]: - abort(403) - - return post - - -@bp.route("/create", methods=("GET", "POST")) -@login_required -def create(): - """Create a new post for the current user.""" - if request.method == "POST": - title = request.form["title"] - body = request.form["body"] - error = None - - if not title: - error = "Title is required." - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - "INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)", - (title, body, g.user["id"]), - ) - db.commit() - return redirect(url_for("blog.index")) - - return render_template("blog/create.html") - - -@bp.route("//update", methods=("GET", "POST")) -@login_required -def update(id): - """Update a post if the current user is the author.""" - post = get_post(id) - - if request.method == "POST": - title = request.form["title"] - body = request.form["body"] - error = None - - if not title: - error = "Title is required." - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - "UPDATE post SET title = ?, body = ? WHERE id = ?", (title, body, id) - ) - db.commit() - return redirect(url_for("blog.index")) - - return render_template("blog/update.html", post=post) - - -@bp.route("//delete", methods=("POST",)) -@login_required -def delete(id): - """Delete a post. - - Ensures that the post exists and that the logged in user is the - author of the post. - """ - get_post(id) - db = get_db() - db.execute("DELETE FROM post WHERE id = ?", (id,)) - db.commit() - return redirect(url_for("blog.index")) diff --git a/flaskr/db.py b/flaskr/db.py deleted file mode 100644 index f1e2dc3..0000000 --- a/flaskr/db.py +++ /dev/null @@ -1,54 +0,0 @@ -import sqlite3 - -import click -from flask import current_app -from flask import g -from flask.cli import with_appcontext - - -def get_db(): - """Connect to the application's configured database. The connection - is unique for each request and will be reused if this is called - again. - """ - if "db" not in g: - g.db = sqlite3.connect( - current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES - ) - g.db.row_factory = sqlite3.Row - - return g.db - - -def close_db(e=None): - """If this request connected to the database, close the - connection. - """ - db = g.pop("db", None) - - if db is not None: - db.close() - - -def init_db(): - """Clear existing data and create new tables.""" - db = get_db() - - with current_app.open_resource("schema.sql") as f: - db.executescript(f.read().decode("utf8")) - - -@click.command("init-db") -@with_appcontext -def init_db_command(): - """Clear existing data and create new tables.""" - init_db() - click.echo("Initialized the database.") - - -def init_app(app): - """Register database functions with the Flask app. This is called by - the application factory. - """ - app.teardown_appcontext(close_db) - app.cli.add_command(init_db_command) diff --git a/flaskr/schema.sql b/flaskr/schema.sql deleted file mode 100644 index dd4c866..0000000 --- a/flaskr/schema.sql +++ /dev/null @@ -1,20 +0,0 @@ --- Initialize the database. --- Drop any existing data and create empty tables. - -DROP TABLE IF EXISTS user; -DROP TABLE IF EXISTS post; - -CREATE TABLE user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL -); - -CREATE TABLE post ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - author_id INTEGER NOT NULL, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - title TEXT NOT NULL, - body TEXT NOT NULL, - FOREIGN KEY (author_id) REFERENCES user (id) -); diff --git a/flaskr/static/style.css b/flaskr/static/style.css deleted file mode 100644 index 2f1f4d0..0000000 --- a/flaskr/static/style.css +++ /dev/null @@ -1,134 +0,0 @@ -html { - font-family: sans-serif; - background: #eee; - padding: 1rem; -} - -body { - max-width: 960px; - margin: 0 auto; - background: white; -} - -h1, h2, h3, h4, h5, h6 { - font-family: serif; - color: #377ba8; - margin: 1rem 0; -} - -a { - color: #377ba8; -} - -hr { - border: none; - border-top: 1px solid lightgray; -} - -nav { - background: lightgray; - display: flex; - align-items: center; - padding: 0 0.5rem; -} - -nav h1 { - flex: auto; - margin: 0; -} - -nav h1 a { - text-decoration: none; - padding: 0.25rem 0.5rem; -} - -nav ul { - display: flex; - list-style: none; - margin: 0; - padding: 0; -} - -nav ul li a, nav ul li span, header .action { - display: block; - padding: 0.5rem; -} - -.content { - padding: 0 1rem 1rem; -} - -.content > header { - border-bottom: 1px solid lightgray; - display: flex; - align-items: flex-end; -} - -.content > header h1 { - flex: auto; - margin: 1rem 0 0.25rem 0; -} - -.flash { - margin: 1em 0; - padding: 1em; - background: #cae6f6; - border: 1px solid #377ba8; -} - -.post > header { - display: flex; - align-items: flex-end; - font-size: 0.85em; -} - -.post > header > div:first-of-type { - flex: auto; -} - -.post > header h1 { - font-size: 1.5em; - margin-bottom: 0; -} - -.post .about { - color: slategray; - font-style: italic; -} - -.post .body { - white-space: pre-line; -} - -.content:last-child { - margin-bottom: 0; -} - -.content form { - margin: 1em 0; - display: flex; - flex-direction: column; -} - -.content label { - font-weight: bold; - margin-bottom: 0.5em; -} - -.content input, .content textarea { - margin-bottom: 1em; -} - -.content textarea { - min-height: 12em; - resize: vertical; -} - -input.danger { - color: #cc2f2e; -} - -input[type=submit] { - align-self: start; - min-width: 10em; -} diff --git a/flaskr/templates/auth/login.html b/flaskr/templates/auth/login.html deleted file mode 100644 index b326b5a..0000000 --- a/flaskr/templates/auth/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Log In{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} diff --git a/flaskr/templates/auth/register.html b/flaskr/templates/auth/register.html deleted file mode 100644 index 4320e17..0000000 --- a/flaskr/templates/auth/register.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Register{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} diff --git a/flaskr/templates/base.html b/flaskr/templates/base.html deleted file mode 100644 index f09e926..0000000 --- a/flaskr/templates/base.html +++ /dev/null @@ -1,24 +0,0 @@ - -{% block title %}{% endblock %} - Flaskr - - -
-
- {% block header %}{% endblock %} -
- {% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} - {% block content %}{% endblock %} -
diff --git a/flaskr/templates/blog/create.html b/flaskr/templates/blog/create.html deleted file mode 100644 index 88e31e4..0000000 --- a/flaskr/templates/blog/create.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}New Post{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} diff --git a/flaskr/templates/blog/index.html b/flaskr/templates/blog/index.html deleted file mode 100644 index 3481b8e..0000000 --- a/flaskr/templates/blog/index.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Posts{% endblock %}

- {% if g.user %} - New - {% endif %} -{% endblock %} - -{% block content %} - {% for post in posts %} -
-
-
-

{{ post['title'] }}

-
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
-
- {% if g.user['id'] == post['author_id'] %} - Edit - {% endif %} -
-

{{ post['body'] }}

-
- {% if not loop.last %} -
- {% endif %} - {% endfor %} -{% endblock %} diff --git a/flaskr/templates/blog/update.html b/flaskr/templates/blog/update.html deleted file mode 100644 index 2c405e6..0000000 --- a/flaskr/templates/blog/update.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-
-
- -
-{% endblock %} diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d001093..0000000 --- a/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[metadata] -name = flaskr -version = 1.0.0 -url = https://flask.palletsprojects.com/tutorial/ -license = BSD-3-Clause -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = The basic blog app built in the Flask tutorial. -long_description = file: README.rst -long_description_content_type = text/x-rst - -[options] -packages = find: -include_package_data = true -install_requires = - Flask - -[options.extras_require] -test = - pytest - -[tool:pytest] -testpaths = tests - -[coverage:run] -branch = True -source = - flaskr diff --git a/setup.py b/setup.py deleted file mode 100644 index b024da8..0000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -from setuptools import setup - - -setup() diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 6bf62f0..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import tempfile - -import pytest - -from flaskr import create_app -from flaskr.db import get_db -from flaskr.db import init_db - -# read in SQL for populating test data -with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f: - _data_sql = f.read().decode("utf8") - - -@pytest.fixture -def app(): - """Create and configure a new app instance for each test.""" - # create a temporary file to isolate the database for each test - db_fd, db_path = tempfile.mkstemp() - # create the app with common test config - app = create_app({"TESTING": True, "DATABASE": db_path}) - - # create the database and load test data - with app.app_context(): - init_db() - get_db().executescript(_data_sql) - - yield app - - # close and remove the temporary database - os.close(db_fd) - os.unlink(db_path) - - -@pytest.fixture -def client(app): - """A test client for the app.""" - return app.test_client() - - -@pytest.fixture -def runner(app): - """A test runner for the app's Click commands.""" - return app.test_cli_runner() - - -class AuthActions: - def __init__(self, client): - self._client = client - - def login(self, username="test", password="test"): - return self._client.post( - "/auth/login", data={"username": username, "password": password} - ) - - def logout(self): - return self._client.get("/auth/logout") - - -@pytest.fixture -def auth(client): - return AuthActions(client) diff --git a/tests/data.sql b/tests/data.sql deleted file mode 100644 index 9b68006..0000000 --- a/tests/data.sql +++ /dev/null @@ -1,8 +0,0 @@ -INSERT INTO user (username, password) -VALUES - ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), - ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); - -INSERT INTO post (title, body, author_id, created) -VALUES - ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); diff --git a/tests/test_auth.py b/tests/test_auth.py deleted file mode 100644 index 3ac9a12..0000000 --- a/tests/test_auth.py +++ /dev/null @@ -1,69 +0,0 @@ -import pytest -from flask import g -from flask import session - -from flaskr.db import get_db - - -def test_register(client, app): - # test that viewing the page renders without template errors - assert client.get("/auth/register").status_code == 200 - - # test that successful registration redirects to the login page - response = client.post("/auth/register", data={"username": "a", "password": "a"}) - assert "http://localhost/auth/login" == response.headers["Location"] - - # test that the user was inserted into the database - with app.app_context(): - assert ( - get_db().execute("select * from user where username = 'a'").fetchone() - is not None - ) - - -@pytest.mark.parametrize( - ("username", "password", "message"), - ( - ("", "", b"Username is required."), - ("a", "", b"Password is required."), - ("test", "test", b"already registered"), - ), -) -def test_register_validate_input(client, username, password, message): - response = client.post( - "/auth/register", data={"username": username, "password": password} - ) - assert message in response.data - - -def test_login(client, auth): - # test that viewing the page renders without template errors - assert client.get("/auth/login").status_code == 200 - - # test that successful login redirects to the index page - response = auth.login() - assert response.headers["Location"] == "http://localhost/" - - # login request set the user_id in the session - # check that the user is loaded from the session - with client: - client.get("/") - assert session["user_id"] == 1 - assert g.user["username"] == "test" - - -@pytest.mark.parametrize( - ("username", "password", "message"), - (("a", "test", b"Incorrect username."), ("test", "a", b"Incorrect password.")), -) -def test_login_validate_input(auth, username, password, message): - response = auth.login(username, password) - assert message in response.data - - -def test_logout(client, auth): - auth.login() - - with client: - auth.logout() - assert "user_id" not in session diff --git a/tests/test_blog.py b/tests/test_blog.py deleted file mode 100644 index 9185968..0000000 --- a/tests/test_blog.py +++ /dev/null @@ -1,83 +0,0 @@ -import pytest - -from flaskr.db import get_db - - -def test_index(client, auth): - response = client.get("/") - assert b"Log In" in response.data - assert b"Register" in response.data - - auth.login() - response = client.get("/") - assert b"test title" in response.data - assert b"by test on 2018-01-01" in response.data - assert b"test\nbody" in response.data - assert b'href="/1/update"' in response.data - - -@pytest.mark.parametrize("path", ("/create", "/1/update", "/1/delete")) -def test_login_required(client, path): - response = client.post(path) - assert response.headers["Location"] == "http://localhost/auth/login" - - -def test_author_required(app, client, auth): - # change the post author to another user - with app.app_context(): - db = get_db() - db.execute("UPDATE post SET author_id = 2 WHERE id = 1") - db.commit() - - auth.login() - # current user can't modify other user's post - assert client.post("/1/update").status_code == 403 - assert client.post("/1/delete").status_code == 403 - # current user doesn't see edit link - assert b'href="/1/update"' not in client.get("/").data - - -@pytest.mark.parametrize("path", ("/2/update", "/2/delete")) -def test_exists_required(client, auth, path): - auth.login() - assert client.post(path).status_code == 404 - - -def test_create(client, auth, app): - auth.login() - assert client.get("/create").status_code == 200 - client.post("/create", data={"title": "created", "body": ""}) - - with app.app_context(): - db = get_db() - count = db.execute("SELECT COUNT(id) FROM post").fetchone()[0] - assert count == 2 - - -def test_update(client, auth, app): - auth.login() - assert client.get("/1/update").status_code == 200 - client.post("/1/update", data={"title": "updated", "body": ""}) - - with app.app_context(): - db = get_db() - post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() - assert post["title"] == "updated" - - -@pytest.mark.parametrize("path", ("/create", "/1/update")) -def test_create_update_validate(client, auth, path): - auth.login() - response = client.post(path, data={"title": "", "body": ""}) - assert b"Title is required." in response.data - - -def test_delete(client, auth, app): - auth.login() - response = client.post("/1/delete") - assert response.headers["Location"] == "http://localhost/" - - with app.app_context(): - db = get_db() - post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() - assert post is None diff --git a/tests/test_db.py b/tests/test_db.py deleted file mode 100644 index 2363bf8..0000000 --- a/tests/test_db.py +++ /dev/null @@ -1,29 +0,0 @@ -import sqlite3 - -import pytest - -from flaskr.db import get_db - - -def test_get_close_db(app): - with app.app_context(): - db = get_db() - assert db is get_db() - - with pytest.raises(sqlite3.ProgrammingError) as e: - db.execute("SELECT 1") - - assert "closed" in str(e.value) - - -def test_init_db_command(runner, monkeypatch): - class Recorder: - called = False - - def fake_init_db(): - Recorder.called = True - - monkeypatch.setattr("flaskr.db.init_db", fake_init_db) - result = runner.invoke(args=["init-db"]) - assert "Initialized" in result.output - assert Recorder.called diff --git a/tests/test_factory.py b/tests/test_factory.py deleted file mode 100644 index 9b7ca57..0000000 --- a/tests/test_factory.py +++ /dev/null @@ -1,12 +0,0 @@ -from flaskr import create_app - - -def test_config(): - """Test create_app without passing test config.""" - assert not create_app().testing - assert create_app({"TESTING": True}).testing - - -def test_hello(client): - response = client.get("/hello") - assert response.data == b"Hello, World!"