mirror of
https://github.com/shouptech/humulus.git
synced 2026-02-03 16:59:41 +00:00
618 lines
18 KiB
Python
618 lines
18 KiB
Python
# Copyright 2019 Mike Shoup
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import json
|
|
from decimal import Decimal
|
|
from io import BytesIO
|
|
|
|
from humulus.couch import get_doc
|
|
from humulus.recipes import (
|
|
FermentableForm,
|
|
HopForm,
|
|
RecipeForm,
|
|
YeastForm,
|
|
MashForm,
|
|
MashStepForm,
|
|
)
|
|
|
|
|
|
def test_index(client):
|
|
"""Test success in retrieving index."""
|
|
# Test for bad request
|
|
response = client.get("/recipes/?sort_by=foobar")
|
|
assert response.status_code == 400
|
|
|
|
# Verify defaults
|
|
response = client.get("/recipes/")
|
|
assert response.status_code == 200
|
|
# Assert recipes are returned
|
|
assert b"Awesome Lager" in response.data
|
|
assert b"Awesome Beer" in response.data
|
|
assert (
|
|
b'"/recipes/?descending=true&sort_by=name">Name ↑'
|
|
in response.data
|
|
)
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=date">Created On'
|
|
in response.data
|
|
)
|
|
|
|
# Test sort by name descending
|
|
response = client.get("/recipes/?descending=true&sort_by=name")
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=name">Name ↓'
|
|
in response.data
|
|
)
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=date">Created On'
|
|
in response.data
|
|
)
|
|
|
|
# Test sort by date ascending
|
|
response = client.get("/recipes/?descending=false&sort_by=date")
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=name">Name' in response.data
|
|
)
|
|
assert (
|
|
b'"/recipes/?descending=true&sort_by=date">Created On ↑'
|
|
in response.data
|
|
)
|
|
|
|
# Test sort by date descending
|
|
response = client.get("/recipes/?descending=true&sort_by=date")
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=name">Name' in response.data
|
|
)
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=date">Created On ↓'
|
|
in response.data
|
|
)
|
|
|
|
# Test sort by volume ascending
|
|
response = client.get("/recipes/?descending=false&sort_by=volume")
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=name">Name' in response.data
|
|
)
|
|
assert (
|
|
b'"/recipes/?descending=true&sort_by=volume">Batch Size ↑'
|
|
in response.data
|
|
)
|
|
|
|
# Test sort by volume descending
|
|
response = client.get("/recipes/?descending=true&sort_by=volume")
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=name">Name' in response.data
|
|
)
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=volume">Batch Size ↓'
|
|
in response.data
|
|
)
|
|
|
|
# Test sort by type ascending
|
|
response = client.get("/recipes/?descending=false&sort_by=type")
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=name">Name' in response.data
|
|
)
|
|
assert (
|
|
b'"/recipes/?descending=true&sort_by=type">Type ↑'
|
|
in response.data
|
|
)
|
|
|
|
# Test sort by type descending
|
|
response = client.get("/recipes/?descending=true&sort_by=type")
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=name">Name' in response.data
|
|
)
|
|
assert (
|
|
b'"/recipes/?descending=false&sort_by=type">Type ↓'
|
|
in response.data
|
|
)
|
|
|
|
|
|
def test_create(client, app, auth):
|
|
"""Test success in creating a recipe document."""
|
|
# Test GET without login
|
|
response = client.get("/recipes/create")
|
|
assert response.status_code == 302
|
|
|
|
# Test GET with login
|
|
auth.login()
|
|
response = client.get("/recipes/create")
|
|
assert response.status_code == 200
|
|
|
|
# Test POST
|
|
data = {
|
|
"efficiency": "65",
|
|
"name": "Test",
|
|
"notes": "Test",
|
|
"volume": "5.5",
|
|
"style": "1A",
|
|
}
|
|
response = client.post("/recipes/create", data=data)
|
|
assert response.status_code == 302
|
|
|
|
with app.app_context():
|
|
doc = get_doc("test")
|
|
|
|
assert doc["name"] == "Test"
|
|
assert doc["notes"] == "Test"
|
|
assert doc["volume"] == "5.5"
|
|
assert doc["efficiency"] == "65"
|
|
assert doc["style"] == "1A"
|
|
|
|
|
|
def test_update(client, app, auth):
|
|
"""Test success in updating a recipe document."""
|
|
# Test GET without login
|
|
response = client.get("/recipes/update/awesome-lager")
|
|
assert response.status_code == 302
|
|
|
|
auth.login()
|
|
# Test GET on a bare minimum recipe
|
|
response = client.get("/recipes/update/awesome-lager")
|
|
assert response.status_code == 200
|
|
assert b"Awesome Lager" in response.data
|
|
|
|
# Test GET on a more complete recipe
|
|
response = client.get("/recipes/update/full-recipe")
|
|
assert response.status_code == 200
|
|
test_items = [
|
|
b"Awesome Beer",
|
|
b"2row",
|
|
b"Dextrose",
|
|
b"Nugget (US)",
|
|
b"CTZ (US)",
|
|
b"Northern California Ale",
|
|
]
|
|
for item in test_items:
|
|
assert item in response.data
|
|
|
|
# Test GET on a recipe missing most yeast fields
|
|
response = client.get("/recipes/update/partial-yeast-recipe")
|
|
assert response.status_code == 200
|
|
test_items = [b"Partial Beer", b"US-05"]
|
|
for item in test_items:
|
|
assert item in response.data
|
|
|
|
# Get a doc, make an update, and test a POST
|
|
id = "awesome-lager"
|
|
with app.app_context():
|
|
doc = get_doc(id)
|
|
# Remove unneeded fields
|
|
doc.pop("_id")
|
|
rev = doc.pop("_rev")
|
|
response = client.post(
|
|
"/recipes/update/awesome-lager", query_string={"rev": rev}, data=doc
|
|
)
|
|
assert response.status_code == 302
|
|
|
|
# Test response without valid/conflicted rev
|
|
response = client.post(
|
|
"/recipes/update/awesome-lager", query_string={"rev": ""}, data=doc
|
|
)
|
|
assert response.status_code == 302
|
|
with client.session_transaction() as session:
|
|
flash_message = dict(session["_flashes"]).pop("danger", None)
|
|
assert "Update conflict" in flash_message
|
|
|
|
|
|
def test_info(client, monkeypatch):
|
|
"""Test success in retrieving a recipe document."""
|
|
|
|
def mock_get_doc(id):
|
|
# This function always raises KeyError
|
|
raise KeyError(id)
|
|
|
|
# Validate 404
|
|
response = client.get("/recipes/info/thisdoesnotexist")
|
|
assert response.status_code == 404
|
|
|
|
# Validate response for existing doc
|
|
response = client.get("/recipes/info/awesome-lager")
|
|
assert response.status_code == 200
|
|
assert b"Awesome Lager" in response.data
|
|
|
|
# Validate response for recipe with style
|
|
response = client.get("/recipes/info/full-recipe")
|
|
assert response.status_code == 200
|
|
assert b"Awesome Beer" in response.data
|
|
assert b"Test Style" in response.data
|
|
|
|
# Validate warning is flashed when style cannot be found
|
|
monkeypatch.setattr("humulus.recipes.get_doc", mock_get_doc)
|
|
response = client.get("/recipes/info/full-recipe")
|
|
assert b"Could not find style" in response.data
|
|
|
|
|
|
def test_info_json(client):
|
|
"""Test success in retrieving a JSON recipe."""
|
|
# Validate 404
|
|
response = client.get("/recipes/info/thisdoesnotexist/json")
|
|
assert response.status_code == 404
|
|
|
|
# Validate response for existing doc
|
|
response = client.get("/recipes/info/awesome-lager/json")
|
|
assert response.status_code == 200
|
|
assert response.is_json
|
|
assert response.get_json()["name"] == "Awesome Lager"
|
|
|
|
|
|
def test_step_form_doc(app):
|
|
"""Evaluates conditionals in generation of doc from a step form."""
|
|
step = MashStepForm()
|
|
step.name.data = "Test Mash Step"
|
|
step.type.data = "Infusion"
|
|
step.temp.data = Decimal("152")
|
|
step.time.data = Decimal("60")
|
|
assert step.doc == {
|
|
"name": "Test Mash Step",
|
|
"type": "Infusion",
|
|
"temp": "152",
|
|
"time": "60",
|
|
}
|
|
|
|
step.amount.data = Decimal("3.5")
|
|
assert step.doc == {
|
|
"name": "Test Mash Step",
|
|
"type": "Infusion",
|
|
"temp": "152",
|
|
"time": "60",
|
|
"amount": "3.5",
|
|
}
|
|
|
|
|
|
def test_yeast_form_doc(app):
|
|
"""Evaluates conditionals in generation of doc from a yeast form."""
|
|
yeast = YeastForm()
|
|
yeast.name.data = "Test"
|
|
yeast.low_attenuation.data = Decimal("60")
|
|
yeast.high_attenuation.data = Decimal("75")
|
|
|
|
assert yeast.doc == {
|
|
"name": "Test",
|
|
"low_attenuation": "60",
|
|
"high_attenuation": "75",
|
|
}
|
|
|
|
yeast.type.data = "Dry"
|
|
yeast.code.data = "INIS-001"
|
|
yeast.lab.data = "Inland Island"
|
|
yeast.flocculation.data = "Low"
|
|
yeast.min_temperature.data = Decimal("40")
|
|
yeast.max_temperature.data = Decimal("50")
|
|
yeast.abv_tolerance.data = Decimal("15")
|
|
|
|
assert yeast.doc == {
|
|
"name": "Test",
|
|
"low_attenuation": "60",
|
|
"high_attenuation": "75",
|
|
"flocculation": "Low",
|
|
"type": "Dry",
|
|
"code": "INIS-001",
|
|
"lab": "Inland Island",
|
|
"min_temperature": "40",
|
|
"max_temperature": "50",
|
|
"abv_tolerance": "15",
|
|
}
|
|
|
|
|
|
def test_recipe_form_doc(app):
|
|
"""Test if a recipeform can be turned into a document.
|
|
|
|
This test also tests that subforms can be turned into a document. Subforms
|
|
are not tested individually since they will never be used individually.
|
|
"""
|
|
with app.app_context():
|
|
recipe = RecipeForm()
|
|
|
|
recipe.name.data = "Test"
|
|
recipe.efficiency.data = Decimal("65")
|
|
recipe.volume.data = Decimal("5.5")
|
|
recipe.notes.data = "This is a test"
|
|
recipe.type.data = "All-Grain"
|
|
recipe.style.data = "1A"
|
|
|
|
assert recipe.doc == {
|
|
"name": "Test",
|
|
"efficiency": "65",
|
|
"type": "All-Grain",
|
|
"volume": "5.5",
|
|
"notes": "This is a test",
|
|
"fermentables": [],
|
|
"hops": [],
|
|
"$type": "recipe",
|
|
"style": "1A",
|
|
}
|
|
|
|
ferm = FermentableForm()
|
|
ferm.name.data = "Test"
|
|
ferm.type.data = "Grain"
|
|
ferm.amount.data = Decimal("5.5")
|
|
ferm.ppg.data = Decimal("37")
|
|
ferm.color.data = Decimal("1.8")
|
|
|
|
hop = HopForm()
|
|
hop.name.data = "Test"
|
|
hop.use.data = "Boil"
|
|
hop.alpha.data = Decimal("12.5")
|
|
hop.duration.data = Decimal("60")
|
|
hop.amount.data = Decimal("0.5")
|
|
|
|
yeast = YeastForm()
|
|
yeast.name.data = "Test"
|
|
yeast.low_attenuation.data = "70"
|
|
yeast.high_attenuation.data = "75"
|
|
|
|
step = MashStepForm()
|
|
step.name.data = "Test Mash Step"
|
|
step.type.data = "Infusion"
|
|
step.temp.data = Decimal("152")
|
|
step.time.data = Decimal("60")
|
|
step.amount.data = Decimal("3.5")
|
|
mash = MashForm()
|
|
mash.name.data = "Single Infusion"
|
|
mash.steps = [step]
|
|
|
|
recipe.fermentables = [ferm]
|
|
recipe.hops = [hop]
|
|
recipe.mash = mash
|
|
recipe.yeast = yeast
|
|
|
|
assert recipe.doc == {
|
|
"name": "Test",
|
|
"efficiency": "65",
|
|
"type": "All-Grain",
|
|
"volume": "5.5",
|
|
"notes": "This is a test",
|
|
"$type": "recipe",
|
|
"style": "1A",
|
|
"fermentables": [
|
|
{
|
|
"name": "Test",
|
|
"type": "Grain",
|
|
"amount": "5.5",
|
|
"ppg": "37",
|
|
"color": "1.8",
|
|
}
|
|
],
|
|
"hops": [
|
|
{
|
|
"name": "Test",
|
|
"use": "Boil",
|
|
"alpha": "12.5",
|
|
"duration": "60",
|
|
"amount": "0.5",
|
|
}
|
|
],
|
|
"yeast": {
|
|
"name": "Test",
|
|
"low_attenuation": "70",
|
|
"high_attenuation": "75",
|
|
},
|
|
"mash": {
|
|
"name": "Single Infusion",
|
|
"steps": [
|
|
{
|
|
"name": "Test Mash Step",
|
|
"type": "Infusion",
|
|
"temp": "152",
|
|
"time": "60",
|
|
"amount": "3.5",
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def test_recipe_delete(client, auth):
|
|
"""Test success in deleting a document."""
|
|
# Try to delete a document without logging in
|
|
response = client.post("/recipes/delete/awesome-lager")
|
|
response = client.get("/recipes/info/awesome-lager")
|
|
assert response.status_code == 200
|
|
|
|
# Delete document after login
|
|
auth.login()
|
|
# Try to delete a document without logging in
|
|
response = client.post("/recipes/delete/awesome-lager")
|
|
response = client.get("/recipes/info/awesome-lager")
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_recipe_create_json(client, sample_recipes, auth):
|
|
"""Test uploading JSON recipe."""
|
|
# Test GET without logging in
|
|
response = client.get("/recipes/create/json")
|
|
assert response.status_code == 302
|
|
|
|
# Test GET after logging in
|
|
auth.login()
|
|
response = client.get("/recipes/create/json")
|
|
assert response.status_code == 200
|
|
|
|
# Test upload some good data
|
|
data = {
|
|
"upload": (
|
|
BytesIO(json.dumps(sample_recipes["sweetstout"]).encode()),
|
|
"sweetstout.json",
|
|
)
|
|
}
|
|
response = client.post(
|
|
"/recipes/create/json",
|
|
buffered=True,
|
|
content_type="multipart/form-data",
|
|
data=data,
|
|
)
|
|
assert response.status_code == 302
|
|
assert "recipes/info/sweet-stout" in response.headers["Location"]
|
|
|
|
# Test upload with some bad data
|
|
data = {"upload": (BytesIO(b"NOT JSON"), "file")}
|
|
response = client.post(
|
|
"/recipes/create/json",
|
|
buffered=True,
|
|
content_type="multipart/form-data",
|
|
data=data,
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_copyfrom(app, sample_recipes):
|
|
recipe = {
|
|
"name": "Test",
|
|
"type": "All-Grain",
|
|
"efficiency": "65",
|
|
"volume": "5.5",
|
|
"notes": "Notes",
|
|
"style": "18A",
|
|
"fermentables": [
|
|
{
|
|
"name": "Test",
|
|
"type": "Grain",
|
|
"amount": "1",
|
|
"ppg": "36",
|
|
"color": "4",
|
|
}
|
|
],
|
|
"hops": [
|
|
{
|
|
"name": "Test",
|
|
"use": "Boil",
|
|
"alpha": "5.5",
|
|
"duration": "30",
|
|
"amount": "1",
|
|
}
|
|
],
|
|
}
|
|
|
|
with app.app_context():
|
|
form = RecipeForm()
|
|
form.copyfrom(recipe)
|
|
assert form.name.data == recipe["name"]
|
|
assert form.type.data == recipe["type"]
|
|
assert form.efficiency.data == Decimal(recipe["efficiency"])
|
|
assert form.volume.data == Decimal(recipe["volume"])
|
|
assert form.notes.data == recipe["notes"]
|
|
assert len(form.fermentables) == len(recipe["fermentables"])
|
|
assert (
|
|
form.fermentables[0].form.name.data
|
|
== recipe["fermentables"][0]["name"]
|
|
)
|
|
assert (
|
|
form.fermentables[0].form.type.data
|
|
== recipe["fermentables"][0]["type"]
|
|
)
|
|
assert form.fermentables[0].form.amount.data == Decimal(
|
|
recipe["fermentables"][0]["amount"]
|
|
)
|
|
assert form.fermentables[0].form.ppg.data == Decimal(
|
|
recipe["fermentables"][0]["ppg"]
|
|
)
|
|
assert form.fermentables[0].form.color.data == Decimal(
|
|
recipe["fermentables"][0]["color"]
|
|
)
|
|
assert len(form.hops) == len(recipe["hops"])
|
|
assert form.hops[0].form.name.data == recipe["hops"][0]["name"]
|
|
assert form.hops[0].form.use.data == recipe["hops"][0]["use"]
|
|
assert form.hops[0].form.alpha.data == Decimal(recipe["hops"][0]["alpha"])
|
|
assert form.hops[0].form.duration.data == Decimal(
|
|
recipe["hops"][0]["duration"]
|
|
)
|
|
assert form.hops[0].form.amount.data == Decimal(
|
|
recipe["hops"][0]["amount"]
|
|
)
|
|
|
|
recipe["yeast"] = {
|
|
"name": "Test",
|
|
"low_attenuation": "65",
|
|
"high_attenuation": "68",
|
|
}
|
|
recipe["mash"] = {}
|
|
with app.app_context():
|
|
form = RecipeForm()
|
|
form.copyfrom(recipe)
|
|
assert form.yeast.form.name.data == recipe["yeast"]["name"]
|
|
assert form.yeast.form.low_attenuation.data == Decimal(
|
|
recipe["yeast"]["low_attenuation"]
|
|
)
|
|
assert form.yeast.form.high_attenuation.data == Decimal(
|
|
recipe["yeast"]["high_attenuation"]
|
|
)
|
|
|
|
recipe["yeast"].update(
|
|
{
|
|
"type": "Liquid",
|
|
"lab": "Test",
|
|
"code": "Test",
|
|
"flocculation": "Low",
|
|
"min_temperature": "65",
|
|
"max_temperature": "68",
|
|
"abv_tolerance": "15",
|
|
}
|
|
)
|
|
with app.app_context():
|
|
form = RecipeForm()
|
|
form.copyfrom(recipe)
|
|
assert form.yeast.form.type.data == recipe["yeast"]["type"]
|
|
assert form.yeast.form.lab.data == recipe["yeast"]["lab"]
|
|
assert form.yeast.form.code.data == recipe["yeast"]["code"]
|
|
assert form.yeast.form.flocculation.data == recipe["yeast"]["flocculation"]
|
|
assert form.yeast.form.min_temperature.data == Decimal(
|
|
recipe["yeast"]["min_temperature"]
|
|
)
|
|
assert form.yeast.form.max_temperature.data == Decimal(
|
|
recipe["yeast"]["max_temperature"]
|
|
)
|
|
assert form.yeast.form.abv_tolerance.data == Decimal(
|
|
recipe["yeast"]["abv_tolerance"]
|
|
)
|
|
|
|
recipe["mash"] = {
|
|
"name": "Test",
|
|
"steps": [
|
|
{
|
|
"name": "Infusion",
|
|
"type": "Infusion",
|
|
"temp": "152",
|
|
"time": "60",
|
|
}
|
|
],
|
|
}
|
|
with app.app_context():
|
|
form = RecipeForm()
|
|
form.copyfrom(recipe)
|
|
assert form.mash.form.name.data == recipe["mash"]["name"]
|
|
assert len(form.mash.form.steps) == len(recipe["mash"]["steps"])
|
|
assert (
|
|
form.mash.form.steps[0].form.name.data
|
|
== recipe["mash"]["steps"][0]["name"]
|
|
)
|
|
assert (
|
|
form.mash.form.steps[0].form.type.data
|
|
== recipe["mash"]["steps"][0]["type"]
|
|
)
|
|
assert form.mash.form.steps[0].form.temp.data == Decimal(
|
|
recipe["mash"]["steps"][0]["temp"]
|
|
)
|
|
assert form.mash.form.steps[0].form.time.data == Decimal(
|
|
recipe["mash"]["steps"][0]["time"]
|
|
)
|
|
|
|
recipe["mash"]["steps"][0]["amount"] = "3.5"
|
|
with app.app_context():
|
|
form = RecipeForm()
|
|
form.copyfrom(recipe)
|
|
assert form.mash.form.steps[0].form.amount.data == Decimal(
|
|
recipe["mash"]["steps"][0]["amount"]
|
|
)
|