# 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"] )