Testing Django view with environment variables - django

I am starting to write tests for a Django app, which relies on several environment variables. When I am testing it in the shell, I can import os and specify the variables and my tests work just fine. However, when I put them into tests.py, I still get a key error because those variables are not found. here's what my test looks like:
from django.utils import unittest
from django.test.utils import setup_test_environment
from django.test.client import Client
import os
os.environ['a'] = 'a'
os.environ['b'] = 'b'
class ViewTests(unittest.TestCase):
def setUp(self):
setup_test_environment()
def test_login_returning_right_template(self):
""" get / should return login.html template """
c = Client()
resp = c.get('/')
self.assertEqual(resp.templates[0].name, 'login.html')
Is this the wrong place to initialize those variables? I tried to do it on setUp, but with the same result - they are not found. Any suggestions on how to initialize environment variables before running a test suite?
Thanks!
Luka

You should not relay on os.envior in your views. If you have to, do It in your settings.py
MY_CUSTOM_SETTING = os.environ.get('a', 'default_value')
And in views use settings variable:
from django.conf.settings import MY_CUSTOM_SETTING
print MY_CUSTOM_SETTING
Then in your test you can set this setting:
from django.test import TestCase
class MyTestCase(TestCase):
def test_something(self):
with self.settings(MY_CUSTOM_SETTING='a'):
c = Client()
resp = c.get('/')
self.assertEqual(resp.templates[0].name, 'login.html')

Related

Securing Flask-Admin When Using Blueprints and Application Factory

I've set up Flask Admin and it is working, but am struggling with adding authentication. There are several tutorials I've followed but can't seem to get them to work with how I've set up my app. Per the documentation on Flask-Admin regarding authentication (and slightly tweaked based on how I'm importing various elements), you just need to add:
class AdminView(ModelView):
def is_accessible(self):
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
return redirect(url_for('login', next=request.url))
But I can still access the /admin route without logging in. (I also would like to add an additional conditional that checks that the user is listed as an admin, which is a boolean column in the user table, but I haven't gotten this first part to work).
I've tried putting the above inside and outside of the create_app() function. Does this have to do with my blueprints? If so, where would I put this code?
# __init__.py
from flask import Flask
from dotenv import load_dotenv
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager, current_user
from flask_migrate import Migrate
from SIMS_Portal.config import Config
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flaskext.markdown import Markdown
load_dotenv()
db = SQLAlchemy()
bcrypt = Bcrypt()
login_manager = LoginManager()
login_manager.login_view = 'users.login'
login_manager.login_message_category = 'danger'
mail = Mail()
from SIMS_Portal import models
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
bcrypt.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
admin = Admin(app, name='SIMS Admin Portal', template_mode='bootstrap4', endpoint='admin')
Markdown(app)
from SIMS_Portal.main.routes import main
from SIMS_Portal.assignments.routes import assignments
from SIMS_Portal.emergencies.routes import emergencies
from SIMS_Portal.portfolios.routes import portfolios
from SIMS_Portal.users.routes import users
from SIMS_Portal.errors.handlers import errors
app.register_blueprint(main)
app.register_blueprint(assignments)
app.register_blueprint(emergencies)
app.register_blueprint(portfolios)
app.register_blueprint(users)
app.register_blueprint(errors)
from SIMS_Portal.models import User, Assignment, Emergency, Portfolio, NationalSociety
admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Assignment, db.session))
admin.add_view(ModelView(Emergency, db.session))
admin.add_view(ModelView(Portfolio, db.session))
admin.add_view(ModelView(NationalSociety, db.session))
return app
Got some help from the r/flask community which I'll share here for anyone else that has set their app up the same way and found existing tutorials unhelpful. The key when using an app factory like in my case is the swap out the ModelView for the AdminView you define, which can go before the create_app() function. In my __init__.py, I first defined that custom class that inherits from ModelView (and add a check that the user is not only logged in but also listed as an admin in my database):
class AdminView(ModelView):
def is_accessible(self):
if current_user.is_admin == 1:
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
return render_template('errors/403.html'), 403
Then within the create_app() I swap out what I had previously included as admin.add_view(ModelView(User, db.session)) for admin.add_view(AdminView(User, db.session))
That little difference makes obvious sense, but again, I couldn't find tutorials that covered this.

django selenium liveserver ignores css

When I create a test and I request a view I have two options.
Running the django dev server and call self.selenium.get('http://localhost:8000' + reverse('my_view'))
Using the selenium live server and call self.selenium.get(self.live_server_url + reverse('my_view'))
The problem with the first option is that it's a hardcoded location.
The problem with the second option is that the live server does not request css and js files. so its just pure HTML
How it should look:
How it actually looks:
Am I missing anything why the css and js files are not requested?
Base Testfile setup:
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.urls import reverse
from django.utils.translation import gettext as _
from selenium.webdriver.firefox.webdriver import WebDriver
class SeleniumTests(StaticLiveServerTestCase):
#classmethod
def setUpClass(cls):
super(SeleniumTests, cls).setUpClass()
cls.selenium = WebDriver()
lang_cookie = cls.selenium.get_cookie('django_language')
browser_lang = cls.selenium.execute_script("return window.navigator.userLanguage || window.navigator.language")
translation.activate(lang_cookie if lang_cookie != None else browser_lang)
#classmethod
def tearDownClass(cls):
cls.selenium.quit()
super(SeleniumTests, cls).tearDownClass()
def test_login_form(self):
url = self.live_server_url + reverse('...')
self.selenium.get(url)
I use geckodriver-v0.26.0-win64.

Clearing Python Flask cache does not work

I have the following three files.
app.py
from flask_restful import Api
from lib import globals
from flask import Flask
from flask.ext.cache import Cache
globals.algos_app = Flask(__name__)
#cache in file system
globals.cache = Cache(globals.algos_app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': '/tmp'})
api = Api(globals.algos_app)
api.add_resource(Test, '/test')
if __name__ == '__main__':
globals.algos_app.run(host='0.0.0.0', debug=True)
globals.py
global algos_app
global cache
Test.py
from flask_restful import Resource
from lib import globals
from flask_restful import Resource
import time
class Test(Resource):
def get(self):
return self.someMethod()
def post(self):
globals.cache.clear()
return self.someMethod()
#globals.cache.cached()
def someMethod(self):
return str(time.ctime())
I have a GET method which needs to the value from the cache and a POST method which updates the cache by first clearing the cache.
However, no matter I call the GET or the POST method, it always gives me the value from the cache.
PS: At the moment I am simply testing on the development server however I do need to deploy it using WSGI later.
I am not sure if it is the best way, but I did it using the following way.
class Test(Resource):
def get(self):
return globals.cache.get('curr_time')
def post(self):
result = self.someMethod()
globals.cache.set('curr_time', result, timeout=3600)
def someMethod(self):
return str(time.ctime())

Flask questions about BaseConverter and __init.py__

I am trying to use a custom Baseconverter to ensure that my urls are "clean". I finally got it to work as follows:
My init.py is:
import os
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.login import LoginManager
from flask.ext.openid import OpenID
from config import basedir
from slugify import slugify
app = Flask(__name__)
from werkzeug.routing import BaseConverter
class MyStringConverter(BaseConverter):
def to_python(self, value):
return value
def to_url(self, values):
return slugify(values)
app.url_map.converters['mystring'] = MyStringConverter
app.config.from_object('config')
db = SQLAlchemy(app)
lm = LoginManager()
lm.init_app(app)
lm.login_view = 'login'
oid = OpenID(app, os.path.join(basedir, 'tmp'))
from app import views, models
But if I define the MyStringConverter class and add app.url_map.converters['mystring'] = MyStringConverter at the end of the file instead, I got a LookupError: the converter 'mystring' does not exist error. Why is this?
Is this the correct way to clean up my urls? I'm not sure if I need to do anything to the to_python() method or if I am going about this the right way.

Adding Blueprints to Flask seems to break logging

In my Flask server app, I wanted to split up my routes into separate files so I used Blueprint. However this caused logging to fail within the constructor function used by a route. Can anyone see what I might have done wrong to cause this?
Simplified example ...
main.py ...
#!/usr/bin/python
import logging
import logging.handlers
from flask import Flask, Blueprint
from my_routes import *
logger = logging.getLogger("")
logger.setLevel(logging.DEBUG)
handler = logging.handlers.RotatingFileHandler("flask.log",
maxBytes=3000000, backupCount=2)
formatter = logging.Formatter(
'[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logging.getLogger().addHandler(logging.StreamHandler())
logging.debug("started app")
app = Flask(__name__)
app.register_blueprint(api_v1_0)
if __name__ == '__main__':
logging.info("Starting server")
app.run(host="0.0.0.0", port=9000, debug=True)
my_routes.py ...
import logging
import logging.handlers
from flask import Flask, Blueprint
class Class1():
def __init__(self):
logging.debug("Class1.__init__()") # This statement does not get logged
self.prop1=11
def method1(self):
logging.debug("Class1.method1()")
return self.prop1
obj1 = Class1()
api_v1_0 = Blueprint('api_v1_0', __name__)
#api_v1_0.route("/route1", methods=["GET"])
def route1():
logging.debug("route1()")
return(str(obj1.method1()))
You create an instance of Class1 in the global scope of module my_routes.py, so the constructor runs at the time you import that module, the from my_routes import * line in main.py. This is before your logging handler is configured, so there is nowhere to log at that time.
The solution is simple, move your import statement below the chunk of code that sets up the logging handler.