0

In flask, I'm trying to test posting a db object with credentials. I'm getting AttributeError: 'NoneType' object has no attribute 'encode' (at bottom) when I use generate_password_hash to simulate a password in the tested method because password is None.

In test method below, I have comments that confirm that prior to calling post, password is not None.

Why?

-- model

class Member(db.Model, UserMixin):
    __tablename__ = 'member'
    id = db.Column(db.Integer, primary_key=True)
    active = db.Column('is_active', db.Boolean(), nullable=False, server_default='1')
    email = db.Column(db.String(75), unique=True)
    password = db.Column(db.String(255), nullable=False, server_default='')
    email_confirmed_at = db.Column('confirmed_at', db.DateTime, default=db.func.current_timestamp())
    first_name = db.Column(db.String(50), nullable=False, server_default='')
    last_name = db.Column(db.String(50), nullable=False, server_default='')


    def __init__(self, active, email, password, email_confirmed_at, first_name, last_name) -> None:
        self.active = active
        self.email = email
        self.password = password
        self.email_confirmed_at = email_confirmed_at
        self.first_name = first_name
        self.last_name = last_name

    def __repr__(self) -> str:
        return f'id={self.id}, name={self.first_name} {self.last_name}, active={self.active}, email={self.email}'

-- method

@auth.route('/signup', methods=['POST'])
def signup_post():
    active = False
    email = request.form.get('email')
    email_confirmed_at = datetime.datetime.now()
    first_name = request.form.get('first_name')
    last_name = request.form.get('last_name')
    password = request.form.get('password')

    member = Member.query.filter_by(email=email).first()

    if member:
        flash('Email address already exists')
        return redirect(url_for('auth.signup'))

    member_new = Member(
        active=active,
        email=email, 
        email_confirmed_at=email_confirmed_at,
        first_name=first_name, 
        last_name=last_name,
        password=generate_password_hash(password=password, method='sha256')
    )
    db.session.add(member_new)
    db.session.commit()

    return redirect(url_for('auth.login'))

-- test fixtures

@pytest.fixture(scope='session')
@pytest.mark.filterwarnings('ignore::DeprecationWarning')
def app():
    _app = create_app()
    _app.config.from_object(os.environ['TEST_SETTINGS'])
    with Postgresql() as postgresql:
        _app.config['SQLALCHEMY_DATABASE_URI'] = postgresql.url()
        ctx = _app.app_context()
        ctx.push()

        yield _app

        ctx.pop()


@pytest.fixture(scope='session')
def client(app):
    return app.test_client()

-- test method

def test_signup_post(client):
    with client:
        data = {
            'active':False,
            'email':'michael@demo.com',
            'email_confirmed_at':'2022-01-10 00:32:05.041647',
            'first_name':'michael',
            'last_name':'rodgers',
            'password':generate_password_hash(password='123', method='sha256')
        }
        
        # assert data['password'] == 'hello'         # intentional fail here to confirm password is sha256 value
        # assert data['password']                    # passes

        response = client.post(                      # FAILS HERE
            '/signup',
            data=json.dumps(data),
            content_type='application/json',
        )
    # data = json.loads(response.data.decode())
    assert response.status_code == 201
    assert 'michael@demo.com was added!' in data['message']
    assert 'success' in data['status']

-- create_app

def create_app():
    app = Flask(__name__)
    app.config.from_object(os.environ['DEV_SETTINGS'])
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    db.init_app(app)

    login_manager = LoginManager()
    login_manager.login_view = 'auth.login'
    login_manager.init_app(app)


    from .models import Member

    @login_manager.user_loader
    def load_user(user_id):
        # query for the user using user_id pk
        return Member.query.get(int(user_id))

    with app.app_context():

        from .auth import auth as auth_bp
        app.register_blueprint(auth_bp)

        from .routes import routes as routes_bp
        app.register_blueprint(routes_bp)

        db.create_all()

    return app

-- error

tests/test_endpoints.py:15: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
venvn/lib/python3.9/site-packages/werkzeug/test.py:1134: in post
    return self.open(*args, **kw)
venvn/lib/python3.9/site-packages/flask/testing.py:222: in open
    return Client.open(
venvn/lib/python3.9/site-packages/werkzeug/test.py:1074: in open
    response = self.run_wsgi_app(request.environ, buffered=buffered)
venvn/lib/python3.9/site-packages/werkzeug/test.py:945: in run_wsgi_app
    rv = run_wsgi_app(self.application, environ, buffered=buffered)
venvn/lib/python3.9/site-packages/werkzeug/test.py:1231: in run_wsgi_app
    app_rv = app(environ, start_response)
venvn/lib/python3.9/site-packages/flask/app.py:2464: in __call__
    return self.wsgi_app(environ, start_response)
venvn/lib/python3.9/site-packages/flask/app.py:2450: in wsgi_app
    response = self.handle_exception(e)
venvn/lib/python3.9/site-packages/flask/app.py:1867: in handle_exception
    reraise(exc_type, exc_value, tb)
venvn/lib/python3.9/site-packages/flask/_compat.py:39: in reraise
    raise value
venvn/lib/python3.9/site-packages/flask/app.py:2447: in wsgi_app
    response = self.full_dispatch_request()
venvn/lib/python3.9/site-packages/flask/app.py:1952: in full_dispatch_request
    rv = self.handle_user_exception(e)
venvn/lib/python3.9/site-packages/flask/app.py:1821: in handle_user_exception
    reraise(exc_type, exc_value, tb)
venvn/lib/python3.9/site-packages/flask/_compat.py:39: in reraise
    raise value
venvn/lib/python3.9/site-packages/flask/app.py:1950: in full_dispatch_request
    rv = self.dispatch_request()
venvn/lib/python3.9/site-packages/flask/app.py:1936: in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
myapp/auth.py:59: in signup_post
    password=generate_password_hash(password=password, method='sha256')
venvn/lib/python3.9/site-packages/werkzeug/security.py:200: in generate_password_hash
    h, actual_method = _hash_internal(method, salt, password)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'sha256', salt = b'eYNsNBRyTiRtGv3W', password = None              # confirms password is None

    def _hash_internal(method: str, salt: str, password: str) -> t.Tuple[str, str]:
        """Internal password hash helper.  Supports plaintext without salt,
        unsalted and salted passwords.  In case salted passwords are used
        hmac is used.
        """
        if method == "plain":
            return password, method
    
        salt = salt.encode("utf-8")
>       password = password.encode("utf-8")
E       AttributeError: 'NoneType' object has no attribute 'encode'

venvn/lib/python3.9/site-packages/werkzeug/security.py:148: AttributeError
chocalaca
  • 330
  • 2
  • 17
  • In `test_signup_post`, you provide a hard-coded password, so there it should work. In `signup_post`, have you made sure that `password = request.form.get('password')` really initializes `password` to a string? For the sake of defensive programming, I would add a test like `if password is None: ` just after initializing `password` in `signup_post`. – joanis Feb 21 '22 at 20:15
  • I'll consider that. But running pytest tests/test_endpoints.py::test_signup_post is what returns the error above, so that test fails – chocalaca Feb 21 '22 at 20:27
  • `test_signup_post` is not included in the stack trace you show, only `signup_post` is, so are you sure? – joanis Feb 21 '22 at 20:40
  • Hm, good find!...Not sure how to use that info though. Are my variable names getting crosseed, i.e., using 'password' too much? Should I not be reusing generate_password_hash? – chocalaca Feb 21 '22 at 20:48
  • Does this answer your question? [Why do I get AttributeError: 'NoneType' object has no attribute 'something'?](https://stackoverflow.com/questions/8949252/why-do-i-get-attributeerror-nonetype-object-has-no-attribute-something) – Ulrich Eckhardt Feb 21 '22 at 20:57
  • @UlrichEckhardt That's not helpful. OP understands that. The question is, why is that variable None in the first place? – joanis Feb 21 '22 at 21:17
  • @chocalaca I'm not sure. You'll have to carefully examine that stack trace, and make sure you test harness is calling what you want it to call. The short answer is that, indeed, that error message means your variable is None. For the long answer you might have to run your code in a debugger or add a bunch of print statements (i.e., do print debugging). Only you have all the necessary information here! – joanis Feb 21 '22 at 21:23
  • And, actually, this line in your log `method = 'sha256', salt = b'eYNsNBRyTiRtGv3W', password = None` seems to suggest you have a unit test case specifically devised to making sure your function handles password=None gracefully, which you don't. – joanis Feb 21 '22 at 21:24
  • 1
    @joanis, OP asks literally "Why?" without any context what that relates to. Yes, previous sentences indicate they checked for `!= None`, but the topic of the question here still shows lack of understanding. I'm not convinced they are asking the right question. In any case, a [mcve] would help, or stepping through the code with a debugger, or maybe adding assertions. – Ulrich Eckhardt Feb 22 '22 at 07:07
  • @UlrichEckhardt Yes, I have to agree those are good points, and the question needs improvement. Given the exchange in the comments here, I'm not sure the question can be answered as it stands. – joanis Feb 22 '22 at 14:30
  • Thanks to all and apologies if question was unclear. I will post what worked for me. – chocalaca Feb 23 '22 at 19:54

1 Answers1

0
def test_signup_post(client):
    form = {
        'active':False,
        'email':'m@demo.com',
        'email_confirmed_at':'2022-01-10 00:32:05.041647',
        'first_name':'m',
        'last_name':'r',
        'password':'123'
    }
    response = client.post('/signup', data=form)

    assert "http://localhost/login" == response.headers['Location']
    assert b'Redirecting' in response.data
    assert response.status_code == 302
    assert response.request.path == "/signup"
chocalaca
  • 330
  • 2
  • 17