def test_get_close_db(app): with app.app_context(): db = get_db() assert db is get_db() # get_db()로 가져온 db와 get_db()의 값이 같은지 확인합니다. with pytest.raises(sqlite3.ProgrammingError) as e: db.execute('SELECT 1') assert 'closed' in str(e.value)
def test_register(client, app): # /auth/register의 status_code가 200이 맞는지 확인 # client.get()은 GET 요청을 하여 응답을 받는 코드 assert client.get('/auth/register').status_code == 200 # client.post로 /auth/register에 {'username': '******', 'password': '******'} 데이터를 POST로 전달 후 응답받음 # client.post()는 POST 요청을 하여 응답을 받는 코드 # post로 보내는 데이터는 dict 타입을 form 데이터로 변경해준다. response = client.post( '/auth/register', data={ 'username': '******', 'password': '******' } # data값은 form데이터로 변경시켜준다. ) # register view가 login view로 리디렉션될 때, 헤더는 login URL을 가지는 Location 헤더를 가진다. # response로 받아온 데이터는 bytes로 구성된다. # 만약 페이지에서 렌더링할 값이 예상된다면(ex:문자열), 데이터 안에 있는지 확인해야한다. # Bytes는 Bytes와 비교되어야 한다. 만약 text를 비교하려면 get_data(as_text=True)를 사용합니다. assert 'http://localhost/auth/login' == response.headers['Location'] with app.app_context(): assert get_db().execute("SELECT * FROM user WHERE username = '******'", ).fetchone() is not None # a를 가져올 수 있는지 확인
def create(): if request.method == 'POST': title = request.form['title'] # 제목 받아옴 body = request.form['body'] # 글 내용 받아옴 error = None # 유효성 검사 if not title: # title이 없을 경우 에러 error = 'Title is required.' if error is not None: print("blog.py's error = ", error) flash(error) print("is flashed the error? = ", error) else: # 유효하면 db = get_db() # db 받아오고 # 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')
def update(id): 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)
def index(): 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() # db 쿼리 실행 및 모든 레코드 get return render_template('blog/index.html', posts=posts)
def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() # db를 받아온다. error = None user = db.execute( 'SELECT * FROM user WHERE username = ?', (username, )).fetchone() # why fetch를 하는가? -> 데이터를 가져오기 위해서! # fetchone()은 실행한 쿼리에서 하나의 행을 반환합니다. # 반환할 데이터가 없으면 None을 반환합니다. # fetchall()은 실행한 쿼리의 모든 데이터를 가져옵니다. if user is None: # 사용자가 없으면 -> 에러 error = 'Incorrect username' elif not check_password_hash(user['password'], password): # 패스워드가 유효하지 않으면 error = 'Incorrect password' if error is None: # 에러가 없으면 # 세션은 요청에 대한 데이터를 dict형식으로 저장하는 것이다. # 들어온 값이 유효하다면(에러가 없으면) 새로운 세션에서 user의 id를 저장합니다. # 데이터는 보내진 쿠키에 저장되고, 브라우저는 후속 요청으로 이것을 다시 보냅니다. # 플라스크는 데이터를 변조할수 없도록 데이터에 안전하게 sign합니다. session.clear() # 세션을 클리어하고 session['user_id'] = user['id'] return redirect(url_for('index')) flash(error) return render_template('auth/login.html')
def load_logged_in_user(): user_id = session.get('user_id') # 세션으로부터 user_id 가져옴 if user_id is None: # user_id가 없으면 g.user = None g.user = None else: # 있으면 g.user에 데이터베이스에서 user_id에 해당하는 # 하나의 레코드, 데이터를 가져옴 g.user = get_db().execute('SELECT * FROM user WHERE id = ?', (user_id, )).fetchone()
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'
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_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
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
def get_post(id, check_author=True): 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() # abort는 HTTP의 상태 코드에서 특별한 예외가 발생할때 수행된다. # 오류와 함께 출력하려면 선택적인 메시지가 필요합니다. # 404는 Not Found, 403은 Forbidden을 의미합니다. if post is None: abort(404, f"Post id {id} doesn't exist.") # check_author은 작성자를 확인하지 않고 게시물을 얻는데 함수를 사용할 수 있도록 정의됩니다. # 이는 사용자가 게시물을 수정하지 않는 페이지에 개별 게시물을 표시하기 위한 뷰를 작성할 때 유용합니다. # ? if check_author and post['author_id'] != g.user['id']: abort(403) return post
def create(): 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')
def register(): # user가 폼을 작성하여 제출하면, request.method가 'POST'인지 확인합니다. if request.method == 'POST': # 입력값을 가져옵니다. request.form은 dict 타입으로, 아래와 같이 value값을 가져올 수 있습니다. username = request.form['username'] password = request.form['password'] db = get_db() error = None # POST이면 입력값에 대한 유효성 검사를 합니다. if not username: # 받아온 username이 비어있는 값이라면, error에 문자열을 할당합니다. error = 'Username is required.' elif not password: # 받아온 password가 비어있는 값이라면, error에 문자열을 할당합니다. error = 'Password is required.' if error is None: # error가 없으면 (값을 제대로 받아왔다면) try: db.execute( # SQL 쿼리를 수행한다. ?를 통하여 변수를 받을 수 있다. # 여기서 사용하는 sqlite3 데이터베이스는 escaping value를 사용하여 SQL injection 공격에 취약하지 않다. # What is SQL injection attack? # SQL 삽입은 응용 프로그램 보안 상의 허점을 의도적으로 이용해, 악의적인 SQL문을 실행되게 함으로써 데이터베이스를 비정상적으로 조작하는 코드 인젝션 공격 방법이다 "INSERT INTO user (username, password) VALUES (?, ?)", (username, generate_password_hash(password)), # generate_password_hash를 이용하여 비밀번호를 해시하여 저장합니다. ) db.commit() # 실행한 쿼리 저장 except db.IntegrityError: # sqlite3.IntegrityError은 username이 이미 존재하는 경우 발생합니다. error = f"User {username} is already registered." else: # 회원가입 완료, 회원 정보를 저장하고 로그인 페이지로 리디렉션 됩니다. # url_for 함수를 통해 URL을 생성하여 리다이렉트 응답을 수행 # url을 직접 쓸 수도 있지만, 유지보수시 모든 코드의 url을 바꾸어야하는 번거로움이 발생할 수 있으므로 # url_for를 사용하는 것이 좋다. return redirect(url_for("auth.login")) flash(error ) # 아래의 render_template를 수행할 때 retrieve할 수 있게 error 메시지를 저장합니다. # 현재의 회원가입 웹 페이지에 error을 띄울 수 있도록 flash 함수를 사용한다! return render_template('auth/register.html')
def delete(id): get_post(id) db = get_db() db.execute('DELETE FROM post WHERE id = ?', (id, )) db.commit() return redirect(url_for('blog.index'))