diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3340e87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +.ipynb_checkpoints +.idea diff --git a/Dockerfiles/app/Dockerfile b/Dockerfiles/app/Dockerfile new file mode 100644 index 0000000..6eef40d --- /dev/null +++ b/Dockerfiles/app/Dockerfile @@ -0,0 +1,20 @@ +FROM ubuntu:latest + +RUN sed -i 's/archive\.ubuntu\.com/ap-southeast-1\.ec2\.archive\.ubuntu\.com/g' /etc/apt/sources.list &&\ + apt update +ADD setup.sh requirements.txt / +RUN ls +RUN sh setup.sh +#RUN apt-get install -y python-pip +#RUN pip install -U pip +#RUN pip install -U flask +#RUN pip install -U flask-cors +#RUN pip install -U pymongo +#RUN pip install -U flask-sqlalchemy +#RUN pip install -U flask-mongoengine + +WORKDIR /usr/src/app + +EXPOSE 8080 +# figure out how to change this based on ENV +CMD ["python", "./service/app.py"] diff --git a/Dockerfiles/app/Dockerfile-dev b/Dockerfiles/app/Dockerfile-dev new file mode 100644 index 0000000..579be53 --- /dev/null +++ b/Dockerfiles/app/Dockerfile-dev @@ -0,0 +1,13 @@ +FROM ubuntu:latest + +RUN sed -i 's/archive\.ubuntu\.com/ap-southeast-1\.ec2\.archive\.ubuntu\.com/g' /etc/apt/sources.list &&\ + apt update +ADD setup.sh requirements.txt / +RUN sh setup.sh +RUN rm setup.sh requirements.txt + +WORKDIR /usr/src/app + +EXPOSE 8080 +# figure out how to change this based on ENV +CMD ["sh", "-c", "FLASK_DEBUG=1 FLASK_APP=/usr/src/app/service/flask_app.py python -m flask run -p 8080 -h 0.0.0.0"] diff --git a/Dockerfiles/app/Testfile b/Dockerfiles/app/Testfile new file mode 100644 index 0000000..8cf9726 --- /dev/null +++ b/Dockerfiles/app/Testfile @@ -0,0 +1,9 @@ +FROM python:2.7 + +ADD requirements.txt / + +RUN pip install -r requirements.txt --user +RUN pip install pytest pytest-cov pytest-flask + +COPY . /usr/src/app +WORKDIR /usr/src/app \ No newline at end of file diff --git a/Dockerfiles/web/Dockerfile b/Dockerfiles/web/Dockerfile new file mode 100644 index 0000000..8d1247e --- /dev/null +++ b/Dockerfiles/web/Dockerfile @@ -0,0 +1,11 @@ +FROM ubuntu:latest + +RUN apt-get update \ + && apt-get install -y apache2 + +RUN echo "ServerName localhost " >> /etc/apache2/apache2.conf +RUN echo "$user hard nproc 20" >> /etc/security/limits.conf + +WORKDIR /var/www/html +EXPOSE 80 +CMD rm -f /var/run/apache2/apache2.pid && apachectl -D FOREGROUND diff --git a/README.md b/README.md index 9a814e3..fcd0a9b 100644 --- a/README.md +++ b/README.md @@ -96,48 +96,71 @@ Please fill out this section with details relevant to your team. ### Team Members -1. Member 1 Name -2. Member 2 Name -3. Member 3 Name -4. Member 4 Name +1. Chen Hui +2. Kyaw Zawlin +3. Shi Qing +4. Tan Xue Si ### Short Answer Questions #### Question 1: Briefly describe the web technology stack used in your implementation. -Answer: Please replace this sentence with your answer. +Answer: +1. HTML, JS, CSS for front-end +2. Apache to host web server +3. MongoDB for database,mongoengine as our orm framework +4. Python + Flask for backend API +5. pyTest for testing framework + +All contained within their individual docker containers. #### Question 2: Are there any security considerations your team thought about? -Answer: Please replace this sentence with your answer. +Answer: +1. Since we use http instead of https, password is transmitted in plaintext. The password is hashed and salted before storing into the database. +2. Salting prevent rainbow table type attacks from succeeding and recovering the original plaintext even when our database is compromised. +3. There may be multiple users with the same password, thus the password is salted with both the user's username and password before hash3ng to ensure that the hashed password is not the same for users with the same password.I +4. For user authentication, token is used and this method is not safe since a hacker may be able to get his hands on one token and use it to authenticate as a legitimate user. We include a check for the user's IP address during token authentication to make sure that this is the user who owns the token. +5. In the diary delete and permission adjust API, only diary id and token are given. An attacker may want to delete a diary which does not belong to them. We check the token owner and diary owner before processing the diary. An alternative method such as openid where token is encrypted and can contain private data fields, our method is slightly better because we do not have protection of https. +6. Our app is not vulnerable to typical sql injection attacks as 1) we don't use sql or directly execute db queries and 2) we use an orm framework as our database interface which provides both security and ease of use. #### Question 3: Are there any improvements you would make to the API specification to improve the security of the web application? -Answer: Please replace this sentence with your answer. +Answer: +1. Hashg the passwrod on the client side and we send the hashed password. The server side can hash again with the salt and store the double hashed result in the db. This weay we can avoid plain text transmission of passwords. +2. For diary delete and permission adjust, processing a group of diaries by given ids rather than one id would be a good idea +3. Better response codes for different responses instead of just returning 200 with json error fields #### Question 4: Are there any additional features you would like to highlight? -Answer: Please replace this sentence with your answer. +Answer: +1. In order to develop this app in the future, we added a debug mode which can test the APIs and show the status of the database. It is very convenient. +2. We also have a full test suite utilzing standard scalable testing framework pytest. #### Question 5: Is your web application vulnerable? If yes, how and why? If not, what measures did you take to secure it? -Answer: Please replace this sentence with your answer. +Answer: +1. Yes. Data (password, token, text...) is not encrypted during the transmission. Hacker can obtain it via man in the middle attack. We can secure it via https protocol. However,since api require us to provide http. We can implement this by proxying flask traffic through apache server. +2. There is no limitation for response times. This app is vulnerable under flooding attack. #### Feedback: Is there any other feedback you would like to give? -Answer: Please replace this sentence with your answer. +Answer: Docker is fun! ### Declaration #### Please declare your individual contributions to the assignment: -1. Member 1 Name - - Integrated feature x into component y - - Implemented z -2. Member 2 Name +1. Chen Hui + - Implemented diary and user API endpoints + - Implemented additional test cases +2. Kyaw Zawlin + - Designed database schema + - Setup app and database interfaces +3. Shi Qing + - Front-end design - Wrote the front-end code -3. Member 3 Name - - Designed the database schema -4. Member 4 Name - - Implemented x +4. Tan Xue Si + - Implemented test runner + - Dockerize and docker-compose containers diff --git a/clean_docker.sh b/clean_docker.sh new file mode 100755 index 0000000..82f99f4 --- /dev/null +++ b/clean_docker.sh @@ -0,0 +1,4 @@ +#/bin/sh + +sudo docker stop $(sudo docker ps -a -q) +sudo docker rm $(sudo docker ps -a -q) diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..87a7f8a --- /dev/null +++ b/dev.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +sudo docker-compose -f docker-compose-dev.yml build +sudo docker-compose -f docker-compose-dev.yml up diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000..2520f67 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,25 @@ +version: '2' +services: + app: + build: + context: ./ + dockerfile: Dockerfiles/app/Dockerfile-dev + ports: + - "8080:8080" + depends_on: + - mongodb + volumes: + - ./src:/usr/src/app + + web: + image: apache + build: Dockerfiles/web + ports: + - "80:80" + volumes: + - ./src/html:/var/www/html + + mongodb: + image: mongo:latest + volumes: + - /data diff --git a/docker-compose-test.yml b/docker-compose-test.yml new file mode 100644 index 0000000..44b8030 --- /dev/null +++ b/docker-compose-test.yml @@ -0,0 +1,28 @@ +version: '2' +services: + tests: + build: + context: ./ + dockerfile: Dockerfiles/app/Testfile + links: + - mongodb + - app + + app: + build: + context: ./ + dockerfile: Dockerfiles/app/Dockerfile-dev + ports: + - "8080:8080" + depends_on: + - mongodb + volumes: + - ./src:/usr/src/app + + mongodb: + image: mongo:latest + + ports: + - "27017:27017" + volumes: + - /data diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7c28235 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '2' +services: + app: + build: Dockerfiles/app + ports: + - "8080:8080" + depends_on: + - mongodb + volumes: + - ./src:/usr/src/app + + web: + image: apache + build: Dockerfiles/web + ports: + - "80:80" + volumes: + - ./src/html:/var/www/html + + mongodb: + build: Dockerfiles/mongodb + volumes: + - /data diff --git a/drop_db.py b/drop_db.py new file mode 100644 index 0000000..a539520 --- /dev/null +++ b/drop_db.py @@ -0,0 +1,6 @@ + +from flask_mongoengine import MongoEngine +from mongoengine import connect +db = connect('db_test',host='mongodb') +db.drop_database('db_test') +db.close() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..63459ed --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask +pymongo +flask-sqlalchemy +flask-cors +flask-mongoengine diff --git a/run.sh b/run.sh index 97e50da..53a71a1 100755 --- a/run.sh +++ b/run.sh @@ -8,5 +8,7 @@ fi TEAMID=`md5sum README.md | cut -d' ' -f 1` docker kill $(docker ps -q) docker rm $(docker ps -a -q) -docker build . -t $TEAMID -docker run -p 80:80 -p 8080:8080 -t $TEAMID +#docker build . -t $TEAMID +#docker run -p 80:80 -p 8080:8080 -t $TEAMID +docker-compose -f docker-compose-dev.yml -p $TEAMID build +docker-compose -f docker-compose-dev.yml -p $TEAMID up diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..b4d489d --- /dev/null +++ b/setup.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e +apt install python-pip -y +pip install --upgrade pip +pip install -r requirements.txt --user diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/html/diary/create.html b/src/html/diary/create.html new file mode 100644 index 0000000..ee34e93 --- /dev/null +++ b/src/html/diary/create.html @@ -0,0 +1,40 @@ + + + + Create Diary + + + + +

Create Diary

+ +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ True
+ False +
+
+ +
+ +
+
+
+
+
+
+ + + diff --git a/src/html/diary/create.js b/src/html/diary/create.js new file mode 100644 index 0000000..db0ac21 --- /dev/null +++ b/src/html/diary/create.js @@ -0,0 +1,52 @@ +var API_ENDPOINT = "http://localhost:8080" + +function ajax_post(url, data, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 201) { + console.log('responseText:' + xmlhttp.responseText); + try { + var data = JSON.parse(xmlhttp.responseText); + } catch(err) { + + document.getElementById("demo_dbg").innerHTML = err.message + " in " + xmlhttp.responseText; + console.log(err.message + " in " + xmlhttp.responseText); + return ; + } + callback(data); + }else{ + + document.getElementById("demo_dbg").innerHTML = xmlhttp.responseText; + } + }; + + xmlhttp.open("POST", url, true); + xmlhttp.setRequestHeader("Content-type", "application/json"); + xmlhttp.send(JSON.stringify(data)); +} + +function createDiary() { + var title = document.forms["diaryForm"]["title"].value; + var text = document.forms["diaryForm"]["text"].value; + var public = document.forms["diaryForm"]["public"].value; + var token = localStorage.getItem("token"); + + var data = { + 'title': title, + 'text': text, + 'token': token, + 'public': (public === "true")? true: false + } + + ajax_post(API_ENDPOINT + '/diary/create', data, function(data) { + if (data.status) { + document.getElementById("response_status").innerHTML = "Create diary success"; + } + else { + document.getElementById("response_status").innerHTML = "Create diary failed"; + } + }); + return false; +} + + diff --git a/src/html/diary/private-list.html b/src/html/diary/private-list.html new file mode 100644 index 0000000..d05d202 --- /dev/null +++ b/src/html/diary/private-list.html @@ -0,0 +1,19 @@ + + + + My Diary List + + + + +

My Private Diary List

+ +
+
+
+
+
+
+ + + diff --git a/src/html/diary/private-list.js b/src/html/diary/private-list.js new file mode 100644 index 0000000..bfb522d --- /dev/null +++ b/src/html/diary/private-list.js @@ -0,0 +1,83 @@ +var API_ENDPOINT = "http://localhost:8080" + +function ajax_post(url, data, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + console.log('responseText:' + xmlhttp.responseText); + try { + var data = JSON.parse(xmlhttp.responseText); + } catch(err) { + + document.getElementById("demo_dbg").innerHTML = err.message + " in " + xmlhttp.responseText; + console.log(err.message + " in " + xmlhttp.responseText); + return ; + } + callback(data); + }else{ + document.getElementById("demo_dbg").innerHTML = xmlhttp.responseText; + } + }; + + xmlhttp.open("POST", url, true); + xmlhttp.setRequestHeader("Content-type", "application/json"); + xmlhttp.send(JSON.stringify(data)); +} + +function deleteDiary(id) { + var data ={ + id: id, + token: localStorage.getItem("token") + } + ajax_post(API_ENDPOINT + '/diary/delete', data, function(data) { + if (data.status) { + document.getElementById("response_status").innerHTML = "Users delete success"; + window.location.reload(); + } + else { + document.getElementById("response_status").innerHTML = "Users delete failed"; + } + }); +} + + +function toggleDiaryTo(id, to) { + var data = { + id: id, + token: localStorage.getItem("token"), + public: !to + } + + ajax_post(API_ENDPOINT + '/diary/permission', data, function(data) { + if (data.status) { + document.getElementById("response_status").innerHTML = "Users toggle permission success"; + window.location.reload(); + } + else { + document.getElementById("response_status").innerHTML = "Users toggle permission failed"; + } + }); +} + +ajax_post(API_ENDPOINT + '/diary', {'token': localStorage.getItem('token')}, function(data) { + if (data.status) { + var members = data.result; + var output = ""; + document.getElementById("my-diary-list").innerHTML = output; + } + else { + document.getElementById("my-diary-list").innerHTML = "Diary list failed"; + } +}); diff --git a/src/html/diary/public-list.html b/src/html/diary/public-list.html new file mode 100644 index 0000000..8cb24fe --- /dev/null +++ b/src/html/diary/public-list.html @@ -0,0 +1,18 @@ + + + + Public Diary List + + + + +

Public Diary List

+
+
+
+
+
+
+ + + diff --git a/src/html/diary/public-list.js b/src/html/diary/public-list.js new file mode 100644 index 0000000..a8bdc59 --- /dev/null +++ b/src/html/diary/public-list.js @@ -0,0 +1,46 @@ +var API_ENDPOINT = "http://localhost:8080" + +function ajax_get(url, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + console.log('responseText:' + xmlhttp.responseText); + try { + var data = JSON.parse(xmlhttp.responseText); + } catch(err) { + + document.getElementById("demo_dbg").innerHTML = err.message + " in " + xmlhttp.responseText; + console.log(err.message + " in " + xmlhttp.responseText); + return ; + } + callback(data); + }else{ + //document.getElementById("demo_dbg").innerHTML = xmlhttp.responseText; + } + }; + + xmlhttp.open("GET", url, true); + xmlhttp.send(); +} + +ajax_get(API_ENDPOINT + '/diary', function(data) { + if (data.status) { + var members = data.result; + var output = "

All Diaries

"; + document.getElementById("diary-list").innerHTML = output; + //document.getElementById("response_status").innerHTML = "Get Diary list succeeded"; + } + else { + document.getElementById("response_status").innerHTML = "Diary list failed"; + } +}); diff --git a/src/html/index.html b/src/html/index.html index 2a847c7..d95f6f8 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -1,18 +1,21 @@ + + +

Secret Diary Front End

Requirements for the front end are as follows:

+
+
+ +

Debug region

+
+
+ + + diff --git a/src/html/users.js b/src/html/users.js new file mode 100644 index 0000000..c33edae --- /dev/null +++ b/src/html/users.js @@ -0,0 +1,45 @@ +var API_ENDPOINT = "http://localhost:8080" + +// From https://code-maven.com/ajax-request-for-json-data + +function ajax_get(url, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + console.log('responseText:' + xmlhttp.responseText); + try { + var data = JSON.parse(xmlhttp.responseText); + } catch(err) { + + document.getElementById("demo_dbg").innerHTML = err.message + " in " + xmlhttp.responseText; + console.log(err.message + " in " + xmlhttp.responseText); + return ; + } + callback(data); + }else{ + + document.getElementById("demo_dbg").innerHTML = xmlhttp.responseText; + } + }; + + xmlhttp.open("GET", url, true); + xmlhttp.send(); +} + + +ajax_get(API_ENDPOINT + '/users', function(data) { + if (data.status) { + var members = data.result; + var output = "

All Users

"; + document.getElementById("demo_users").innerHTML = output; + } + else { + document.getElementById("demo_users").innerHTML = "Users failed"; + } + console.log(data); +}); + diff --git a/src/html/users/login.html b/src/html/users/login.html new file mode 100644 index 0000000..fda78cd --- /dev/null +++ b/src/html/users/login.html @@ -0,0 +1,31 @@ + + + + Login + + + + +

Login

+
+
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+ + + diff --git a/src/html/users/login.js b/src/html/users/login.js new file mode 100644 index 0000000..0b0174d --- /dev/null +++ b/src/html/users/login.js @@ -0,0 +1,48 @@ +var API_ENDPOINT = "http://localhost:8080" + + +function ajax_post(url, data, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + console.log('responseText:' + xmlhttp.responseText); + try { + var data = JSON.parse(xmlhttp.responseText); + } catch(err) { + + document.getElementById("demo_dbg").innerHTML = err.message + " in " + xmlhttp.responseText; + console.log(err.message + " in " + xmlhttp.responseText); + return ; + } + callback(data); + }else{ + + document.getElementById("demo_dbg").innerHTML = xmlhttp.responseText; + } + }; + + xmlhttp.open("POST", url, true); + xmlhttp.setRequestHeader("Content-type", "application/json"); + xmlhttp.send(JSON.stringify(data)); +} + +function submitLogin() { + var username = document.forms["loginForm"]["username"].value; + var password = document.forms["loginForm"]["password"].value; + var data = { + 'username': username, + 'password': password + } + + ajax_post(API_ENDPOINT + '/users/authenticate', data, function(data) { + if (data.status) { + localStorage.setItem("token", data.result.token); + document.getElementById("response_status").innerHTML = "
Login User succeeded
"; + } + else { + document.getElementById("response_status").innerHTML = "
Login User failed
"; + } + }); + return false; +} + diff --git a/src/html/users/logout.html b/src/html/users/logout.html new file mode 100644 index 0000000..a4033d8 --- /dev/null +++ b/src/html/users/logout.html @@ -0,0 +1,22 @@ + + + + Logout + + + + +

User account

+
+
+
+ +
+
+
+
+
+
+ + + diff --git a/src/html/users/logout.js b/src/html/users/logout.js new file mode 100644 index 0000000..9cf2a8a --- /dev/null +++ b/src/html/users/logout.js @@ -0,0 +1,57 @@ +var API_ENDPOINT = "http://localhost:8080" + +function ajax_post(url, data, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + console.log('responseText:' + xmlhttp.responseText); + try { + var data = JSON.parse(xmlhttp.responseText); + } catch(err) { + + document.getElementById("demo_dbg").innerHTML = err.message + " in " + xmlhttp.responseText; + console.log(err.message + " in " + xmlhttp.responseText); + return ; + } + callback(data); + }else{ + document.getElementById("demo_dbg").innerHTML = xmlhttp.responseText; + } + }; + + xmlhttp.open("POST", url, true); + xmlhttp.setRequestHeader("Content-type", "application/json"); + xmlhttp.send(JSON.stringify(data)); +} + +ajax_post(API_ENDPOINT + '/users', {'token': localStorage.getItem("token")}, function(data) { + if (data.status) { + var member = JSON.parse(data.result); + var output = "
" + + "
Full name: " + member["fullname"] + "
" + + "
Username: " + member["username"] + "
" + + "
Age: " + member["age"] + "
" + + "
"; + + document.getElementById("user-account").innerHTML = output; + document.getElementById("response_status").innerHTML = "Get User account succeeded"; + } + else { + document.getElementById("response_status").innerHTML = "Get User account failed"; + } +}); + +function logout() { + ajax_post(API_ENDPOINT + '/users/expire', {'token': localStorage.getItem("token")}, function(data) { + if (data.status) { + localStorage.setItem("token", ''); + document.getElementById("response_status").innerHTML = "User logout succeeded"; + } + else { + document.getElementById("response_status").innerHTML = "User logout failed"; + } +}); +} + + + diff --git a/src/html/users/register.html b/src/html/users/register.html new file mode 100644 index 0000000..c3a36a2 --- /dev/null +++ b/src/html/users/register.html @@ -0,0 +1,41 @@ + + + + Register + + + + +

Register

+
+
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+ + + diff --git a/src/html/users/register.js b/src/html/users/register.js new file mode 100644 index 0000000..db6dbf7 --- /dev/null +++ b/src/html/users/register.js @@ -0,0 +1,48 @@ +var API_ENDPOINT = "http://localhost:8080" + +function ajax_post(url, data, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 201) { + console.log('responseText:' + xmlhttp.responseText); + try { + var data = JSON.parse(xmlhttp.responseText); + } catch(err) { + document.getElementById("demo_dbg").innerHTML = err.message + " in " + xmlhttp.responseText; + console.log(err.message + " in " + xmlhttp.responseText); + return ; + } + callback(data); + }else{ + + document.getElementById("demo_dbg").innerHTML = xmlhttp.responseText; + } + }; + + xmlhttp.open("POST", url, true); + xmlhttp.setRequestHeader("Content-type", "application/json"); + xmlhttp.send(JSON.stringify(data)); +} + +function registerSubmit() { + var fullname = document.forms["registerForm"]["fullname"].value; + var age = document.forms["registerForm"]["age"].value; + var username = document.forms["registerForm"]["username"].value; + var password = document.forms["registerForm"]["password"].value; + var data = { + 'fullname': fullname, + 'age': age, + 'username': username, + 'password': password + } + + ajax_post(API_ENDPOINT + '/users/register', data, function(data) { + if (data.status) { + document.getElementById("response_status").innerHTML = "
Register User succeeded
"; + } else { + document.getElementById("response_status").innerHTML = "
Register User failed
"; + } + }); + return false; +} + diff --git a/src/service/__init__.py b/src/service/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/service/__init__.py @@ -0,0 +1 @@ + diff --git a/src/service/app.py b/src/service/app.py index ade6772..72176dd 100644 --- a/src/service/app.py +++ b/src/service/app.py @@ -1,55 +1,44 @@ #!/usr/bin/python - +# todo:reorder and rearrange from flask import Flask from flask_cors import CORS -import json +from flask_mongoengine import MongoEngine import os +import sys + +sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '../../'))) + + +db = MongoEngine() -app = Flask(__name__) -# Enable cross origin sharing for all endpoints -CORS(app) - -# Remember to update this list -ENDPOINT_LIST = ['/', '/meta/heartbeat', '/meta/members'] - -def make_json_response(data, status=True, code=200): - """Utility function to create the JSON responses.""" - - to_serialize = {} - if status: - to_serialize['status'] = True - if data is not None: - to_serialize['result'] = data - else: - to_serialize['status'] = False - to_serialize['error'] = data - response = app.response_class( - response=json.dumps(to_serialize), - status=code, - mimetype='application/json' - ) - return response +def create_app(**config_overrides): + app = Flask(__name__, static_folder='static', static_url_path='') -@app.route("/") -def index(): - """Returns a list of implemented endpoints.""" - return make_json_response(ENDPOINT_LIST) + # Load config. + from views import views + app.register_blueprint(views) + # apply overrides + app.config.update(config_overrides) -@app.route("/meta/heartbeat") -def meta_heartbeat(): - """Returns true""" - return make_json_response(None) + # Setup the database. + db.init_app(app) + return app -@app.route("/meta/members") -def meta_members(): - """Returns a list of team members""" - with open("./team_members.txt") as f: - team_members = f.read().strip().split("\n") - return make_json_response(team_members) +# app = create_app( + # MONGODB_SETTINGS={'db': 'db_deploy', 'host': 'mongodb'}, + # TESTING=True, + # SALT='IfHYBwi5ZUFZD9VaonnK', +# ) +# app = None +# SALT = 'dfdf' +# SALT = app.config.get('SALT') + + +# Enable cross origin sharing for all endpoints if __name__ == '__main__': # Change the working directory to the script directory @@ -57,5 +46,12 @@ def meta_members(): dname = os.path.dirname(abspath) os.chdir(dname) + app = create_app( + MONGODB_SETTINGS={'db': 'db_deploy', 'host': 'mongodb'}, + TESTING=True, + SALT='IfHYBwi5ZUFZD9VaonnK', + ) + CORS(app) + # Run the application app.run(debug=False, port=8080, host="0.0.0.0") diff --git a/src/service/db_example.py b/src/service/db_example.py new file mode 100644 index 0000000..a4b75f3 --- /dev/null +++ b/src/service/db_example.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +import os +import sys +import datetime +import flask +import bson +import uuid +import json + +sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '../../'))) + +from flask_mongoengine import MongoEngine + +app = flask.Flask(__name__) +app.config.from_object(__name__) +app.config['MONGODB_SETTINGS'] = {'DB': 'db_test'} +app.config['TESTING'] = True +app.config['SECRET_KEY'] = 'flask+mongoengine=<3' +app.debug = True +db = MongoEngine() +db.init_app(app) + + +class User(db.Document): + uid = db.SequenceField() + name = db.StringField() + password = db.StringField() + registered_date = db.DateTimeField(default=datetime.datetime.now) + + def __repr__(self): + return 'id=%s, data = [%s:%s]'%(self.pk,self.name,self.password) + +class Token(db.Document): + token = db.StringField() + expiry = db.DateTimeField(default=datetime.datetime.now()+datetime.timedelta(days=1)) + isexpired = db.BooleanField() + data = db.StringField() + def __repr__(self): + return 'token=%s, data = [%s]'%(self.token,self.data) + +class Counter(db.Document): + count = db.IntField() + def __repr__(self): + return 'counter = %d'%(self.count) + + + +# insert +def test_insert(): + count = Counter(count=0) + count.save() + dtnow = datetime.datetime.now() + print dtnow.isoformat() + + user = User(name='TestUser',password='asdf') + user.save() + + user = User(name='TestUser2',password='asdf') + user.save() + + user = User(name='TestUser3',password='asdf') + user.save() + + data = {'pk':str(user.pk),'ip':'127.0.0.1'} + token = Token(token=str(uuid.uuid4()),data = json.dumps(data)) + token.save() + + data2 = {'pk':str(user.pk),'ip':'127.0.0.1'} + token = Token(token=str(uuid.uuid4()),data = json.dumps(data2)) + token.save() + + data3 = {'pk':str(user.pk),'ip':'127.0.0.1'} + token = Token(token=str(uuid.uuid4()),data = json.dumps(data3)) + token.save() + + count = Counter.objects()[0] + count1 = count.count + 1 + count.update(count=count1) + + + +def test_update(): + objs = User.objects(name='TestUser2') + objs[0].update(name='testchanged') + + obj = User.objects(name='TestUser3').first() + obj.update(name='testchanged3') + +def test_delete(): + objs=User.objects(name='TestUser') + objs[0].delete() + +def test_searchby_pk(): + pk = User.objects()[0].pk + print User.objects(pk=pk) +User.drop_collection()#clear all User collection +test_insert() +print User.objects() +results = Token.objects() +for result in results: + print result.token +print Counter.objects() +#test_update() +#print User.objects() +#test_delete() +#print User.objects() +#test_searchby_pk() + diff --git a/src/service/flask_app.py b/src/service/flask_app.py new file mode 100644 index 0000000..7fe0b1d --- /dev/null +++ b/src/service/flask_app.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# todo:reorder and rearrange +from flask import Flask +from flask_cors import CORS +from flask_mongoengine import MongoEngine +import os +import sys + +sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '../../'))) + + +db = MongoEngine() + + +def create_app(**config_overrides): + app = Flask(__name__, static_folder='static', static_url_path='') + + # Load config. + from views import views + app.register_blueprint(views) + + # apply overrides + app.config.update(config_overrides) + + # Setup the database. + db.init_app(app) + + return app + + +app = create_app( + MONGODB_SETTINGS={'db': 'db_deploy', 'host': 'mongodb'}, + TESTING=True, + SALT='IfHYBwi5ZUFZD9VaonnK', +) +# app = None +# SALT = 'dfdf' +SALT = app.config.get('SALT') + + +# Enable cross origin sharing for all endpoints + +# if __name__ == '__main__': + # # Change the working directory to the script directory + # abspath = os.path.abspath(__file__) + # dname = os.path.dirname(abspath) + # os.chdir(dname) + + # app = create_app( + # MONGODB_SETTINGS={'db': 'db_deploy', 'host': 'mongodb'}, + # TESTING=True, + # SALT='IfHYBwi5ZUFZD9VaonnK', + # ) + # CORS(app) + + # # Run the application + # app.run(debug=False, port=8080, host="0.0.0.0") diff --git a/src/service/models.py b/src/service/models.py new file mode 100644 index 0000000..6027f64 --- /dev/null +++ b/src/service/models.py @@ -0,0 +1,77 @@ +from flask import current_app, request +from app import db +import datetime +import json + + +def is_token_valid(token_str): + token = Token.objects(token=token_str).first() + if token is None: return False + if token.isexpired: return False + if token.expiry < datetime.datetime.now(): return False + + token_data = json.loads(token.data) + if request.remote_addr != token_data['ip']: return False + return True + + +def db_object_to_json(doc): + ret = {} + print doc._fields + # field_dict = doc.get_fields_info() + for field in doc._fields: + current_app.logger.info(field) + ret[field] = str(doc[field]) + return json.dumps(ret) + + +def db_object_to_dict(doc): + ret = {} + # field_dict = doc.get_fields_info() + for field in doc._fields: + current_app.logger.info(field) + ret[field] = str(doc[field]) + return ret + + +class User(db.Document): + username = db.StringField() + hashed_password = db.StringField() + password = db.StringField() + fullname = db.StringField() + age = db.IntField() + registered_date = db.DateTimeField(default=datetime.datetime.now) + + def __str__(self): + return self.__repr__(); + + def __repr__(self): + return db_object_to_json(self) + + +class Token(db.Document): + token = db.StringField() + expiry = db.DateTimeField(default=datetime.datetime.now() + datetime.timedelta(days=1)) + isexpired = db.BooleanField() + data = db.StringField() + + def __str__(self): + return self.__repr__(); + + def __repr__(self): + return db_object_to_json(self) + + +class Diary(db.Document): + id = db.SequenceField(primary_key=True) + title = db.StringField() + username = db.StringField() + published_time = db.StringField() # ISO8601 + public = db.BooleanField() + text = db.StringField() + + def __str__(self): + return self.__repr__(); + + def __repr__(self): + return db_object_to_json(self) diff --git a/src/service/start_services.sh b/src/service/start_services.sh index a970fe8..83431ed 100644 --- a/src/service/start_services.sh +++ b/src/service/start_services.sh @@ -1,4 +1,4 @@ #!/bin/bash apachectl start -python /service/app.py +python app.py diff --git a/src/service/team_members.txt b/src/service/team_members.txt index 50bdea8..60c5c6b 100644 --- a/src/service/team_members.txt +++ b/src/service/team_members.txt @@ -1,3 +1,4 @@ -Jeremy Heng -John Galt -Audrey Shida +Zawlin +Xue Si +Shi Qing +Chen Hui diff --git a/src/service/views.py b/src/service/views.py new file mode 100644 index 0000000..5845d4b --- /dev/null +++ b/src/service/views.py @@ -0,0 +1,407 @@ +from flask import current_app, Blueprint, request +from models import is_token_valid, User, Token, Diary, db_object_to_dict, db_object_to_json +import datetime +import hashlib +import json +import uuid +from bson import ObjectId + +views = Blueprint('views', __name__) + +# Remember to update this list +ENDPOINT_LIST = ['/', '/meta/heartbeat', '/meta/members', + '/users', '/users/register', '/users/authenticate', '/users/expire', + '/diary', '/diary/create', '/diary/delete', '/diary/permission'] + + +def make_json_response(data, status=True, code=200): + """Utility function to create the JSON responses.""" + + to_serialize = {} + if status: + to_serialize['status'] = True + if data is not None: + to_serialize['result'] = data + else: + to_serialize['status'] = False + to_serialize['error'] = data + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + + +@views.route('/') +def index(): + """Returns a list of implemented endpoints.""" + return make_json_response(ENDPOINT_LIST) + + +@views.route("/meta/heartbeat") +def meta_heartbeat(): + """Returns true""" + return make_json_response(None) + + +@views.route("/meta/members") +def meta_members(): + team_members = ['Zawlin', 'Xue Si', 'Shi Qing', 'Chen Hui'] + return make_json_response(team_members) + + +@views.route("/users", methods=['POST']) +def users(): + to_serialize = {'status': False} + payload = request.get_json() + if payload and 'token' in payload: + token_str = payload['token'] + code = 200 + if not is_token_valid(token_str): + to_serialize['status'] = False + to_serialize['error'] = 'Invalid authentication token.' + else: + token = Token.objects(token=token_str).first() + data = json.loads(token.data) + pk = data['pk'] + user = User.objects(pk=ObjectId(pk)).first() + result = {'username': user.username, 'fullname': user.fullname, 'age': user.age} + to_serialize['status'] = True + to_serialize['result'] = json.dumps(result) + + # todo make the json_response() better + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + + +@views.route("/users/register", methods=['POST']) +def users_register(): + # todo:remove plain text password storage + # + # print request.args # for get + # print request.form # for post + SALT = current_app.config.get('SALT') + username, fullname, age, password = None, None, None, None + payload = request.get_json() + current_app.logger.info(payload) + if payload and \ + 'password' in payload and \ + 'username' in payload and \ + 'fullname' in payload and \ + 'age' in payload: + username = payload['username'] + password = payload['password'] + fullname = payload['fullname'] + age = payload['age'] + to_serialize = {'status': False} + code = 200 + if username is None or fullname is None or age is None or password is None: + to_serialize['error'] = 'Required parameter is missing' + elif User.objects(username=username).first() is not None: + to_serialize['error'] = 'Username already exists' + else: + to_serialize['status'] = True + # note our 'salt' is actually salt+username for extra safety + hashed_password = hashlib.sha512(password + SALT + username).hexdigest() + User(username=username, hashed_password=hashed_password, fullname=fullname, age=age).save() + code = 201 + + # todo make the json_response() better + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + # return make_json_response(ENDPOINT_LIST) + + +@views.route("/users/authenticate", methods=['POST']) +def users_authenticate(): + SALT = current_app.config.get('SALT') + payload = request.get_json() + username = None + password = None + + if payload: + if 'username' in payload: + username = payload['username'] + if 'password' in payload: + password = payload['password'] + + token = None + + to_serialize = {'status': False} + code = 200 + if username is None or password is None: + to_serialize['error'] = 'Required parameter is missing' + else: + hashed_password = hashlib.sha512(password + SALT + username).hexdigest() + user = User.objects(hashed_password=hashed_password).first() ##???? + if user is not None: + data = {'pk': str(user.pk), 'ip': request.remote_addr} + token = Token(token=str(uuid.uuid4()), data=json.dumps(data)) + token.save() + + if token is not None: + to_serialize['status'] = True + to_serialize['result'] = {'token': token.token} + # todo make the json_response() better + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + + +@views.route("/users/expire", methods=['POST']) +def users_expire(): + payload = request.get_json() + if payload and 'token' in payload: + token_str = payload['token'] + to_serialize = {'status': False} + code = 200 + if not is_token_valid(token_str): + to_serialize['status'] = False + else: + token = Token.objects(token=token_str).first() + token.delete() + to_serialize['status'] = True + + # todo make the json_response() better + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + + +@views.route("/diary") +def diary(): + to_serialize = {'status': False} + code = 200 + results = Diary.objects(public=True) + result = [] + for oneresult in results: + diary1 = {'id': oneresult.id, 'title': oneresult.title, 'author': oneresult.username, + 'publish_date': oneresult.published_time, 'public': oneresult.public, 'text': oneresult.text} + result.append(json.dumps(diary1)) + to_serialize['status'] = True + to_serialize['result'] = result + + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + + +@views.route("/diary", methods=['POST']) +def diary_post(): + to_serialize = {'status': False} + payload = request.get_json() + if payload: + token_str = payload['token'] + else: + token_str = payload + code = 200 + if is_token_valid(token_str) == False: + to_serialize['status'] = False + to_serialize['error'] = 'Invalid authentication token.' + else: + token = Token.objects(token=token_str).first() + data = json.loads(token.data) + pk = data['pk'] + user = User.objects(pk=ObjectId(pk)).first() + username = user.username + results = Diary.objects(username=username) + result = [] + if results is not None: + for oneresult in results: + diary = {'id': oneresult.id, 'title': oneresult.title, 'author': oneresult.username, + 'publish_date': oneresult.published_time, 'public': oneresult.public, 'text': oneresult.text} + result.append(json.dumps(diary)) + to_serialize['status'] = True + to_serialize['result'] = result + + # todo make the json_response() better + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + + +@views.route("/diary/create", methods=['POST']) +def diary_creation(): + to_serialize = {'status': False} + title,text,public,token=None,None,None,None + payload = request.get_json() + payload2 = request.get_json() + if payload2 and \ + 'title' in payload2 and \ + 'text' in payload2 and \ + 'public' in payload2: + title = payload2['title'] + text = payload2['text'] + public = payload2['public'] + + if payload: + token_str = payload['token'] + else: + token_str = payload + code = 200 + if is_token_valid(token_str) == False: + to_serialize['status'] = False + to_serialize['error'] = 'Invalid authentication token.' + else: + if title is None or text is None or public is None: + to_serialize['error'] = 'Required parameter is missing' + else: + token = Token.objects(token=token_str).first() + data = json.loads(token.data) + pk = data['pk'] + user = User.objects(pk=ObjectId(pk)).first() + username = user.username + dtnow = datetime.datetime.now() + published_time = dtnow.replace(microsecond=0).isoformat() + + diary = Diary(title=title, username=username, published_time=published_time, public=public, text=text) + diary.save() + id=diary.id + to_serialize['status'] = True + to_serialize['result'] = {'id': id} + + # todo make the json_response() better + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + + ) + return response + + +@views.route("/diary/delete", methods=['POST']) +def diary_delete(): + to_serialize = {'status': False} + payload = request.get_json() + payload2 = request.get_json() + if payload2 and \ + 'id' in payload2: + id = payload2['id'] + + + if payload: + token_str = payload['token'] + else: + token_str = payload + code = 200 + if is_token_valid(token_str) == False: + to_serialize['status'] = False + to_serialize['error'] = 'Invalid authentication token.' + else: + if id is None: + to_serialize['error'] = 'Required parameter is missing' + else: + token = Token.objects(token=token_str).first() + data = json.loads(token.data) + pk = data['pk'] + user = User.objects(pk=ObjectId(pk)).first() + username = user.username + diary = Diary.objects(id=id).first() + DiaryOwner = diary.username + if DiaryOwner == username: + diary.delete() + to_serialize['status'] = True + + # todo make the json_response() better + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + + +@views.route("/diary/permission", methods=['POST']) +def diary_permission(): + to_serialize = {'status': False} + payload = request.get_json() + payload2 = request.get_json() + if payload2 and \ + 'id' in payload2 and \ + 'public' in payload2: + + id = payload2['id'] + public = payload2['public'] + + if payload: + token_str = payload['token'] + else: + token_str = payload + code = 200 + if is_token_valid(token_str) == False: + to_serialize['status'] = False + to_serialize['error'] = 'Invalid authentication token.' + else: + if id is None or public is None: + to_serialize['error'] = 'Required parameter is missing' + else: + token = Token.objects(token=token_str).first() + data = json.loads(token.data) + pk = data['pk'] + user = User.objects(pk=ObjectId(pk)).first() + username = user.username + diary = Diary.objects(id=id).first() + DiaryOwner = diary.username + if DiaryOwner == username: + diary.update(public=public) + to_serialize['status'] = True + + # todo make the json_response() better + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + + +@views.route("/debug/resetdb") +def debug_resetdb(): + to_serialize = {'status': 'success'} + User.drop_collection() + Token.drop_collection() + Diary.drop_collection() + code = 200 + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response + + +@views.route("/debug/rawdb") +def debug_getrawdb(): + to_serialize = {'status': 'success'} + to_serialize['users'] = [db_object_to_dict(usr) for usr in User.objects()] + to_serialize['tokens'] = [db_object_to_dict(token) for token in Token.objects()] + to_serialize['diaries'] = [db_object_to_dict(diary) for diary in Diary.objects()] + code = 200 + response = current_app.response_class( + response=json.dumps(to_serialize), + status=code, + mimetype='application/json' + ) + return response diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..af04b2e --- /dev/null +++ b/test.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +sudo docker-compose -f docker-compose-test.yml -p test build + +sudo docker-compose -f docker-compose-test.yml -p test run tests python drop_db.py +sudo docker-compose -f docker-compose-test.yml -p test run tests python -m py.test --cov=src/service/ tests +sudo docker stop test_app_1 test_mongodb_1 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..aa74ac1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,21 @@ +import pytest +import json +from src.service.app import create_app +from flask_mongoengine import MongoEngine +from mongoengine import connect + + + + +@pytest.fixture(scope='session') +def app(): + + # db = connect('mongodb') + # db.drop_database('db_test') + # db.close() + app = create_app( + MONGODB_SETTINGS={'db': 'db_test','host':'mongodb'}, + TESTING=True, + SALT='IfHYBwi5ZUFZD9VaonnK', + ) + return app diff --git a/tests/test_diary_empty.py b/tests/test_diary_empty.py new file mode 100644 index 0000000..879ae50 --- /dev/null +++ b/tests/test_diary_empty.py @@ -0,0 +1,132 @@ +import pytest +import json +from flask import url_for +from src.service.models import Diary + +# from flask import current_app as app +# from src.service.models import User, Token, Diary + +from utils import send_post_data, send_post +from flask_mongoengine import MongoEngine +from mongoengine import connect +@pytest.mark.usefixtures('client_class') +class TestDiaryEmpty(object): + + @classmethod + def setup_class(cls): + + db = connect('db_test',host='mongodb') + db.drop_database('db_test') + db.close() + # Diary.drop_collection() + # Token.drop_collection() + # User.drop_collection() + pass + # db = connect('mongodb') + # db.drop_database('db_test') + # db.close() + def test_diary_get_empty(self): + diary = Diary.objects(id=999).first() + if diary: + diary.delete() + response = self.client.get(url_for('views.diary')) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'result' in data + assert 'status' in data + + assert data['status'] + assert len(data['result']) == 0 + + def test_diary_post_no_token(self): + response = send_post(self.client,url_for('views.diary')) + assert response.status_code == 400 + + # data = json.loads(response.data) + # assert 'error' in data + # assert 'status' in data + + # assert not data['status'] + # assert 'Invalid authentication token' in data['error'] + + def test_diary_post_invalid_token(self): + response = send_post_data(self.client,url_for('views.diary'), data=dict(token="e7326198-7055-4559-8d2b-b4568855211e")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Invalid authentication token' in data['error'] + + def test_diary_create_no_token(self): + response = send_post(self.client,url_for('views.diary_creation')) + assert response.status_code == 400 + + # data = json.loads(response.data) + # assert 'error' in data + # assert 'status' in data + + # assert not data['status'] + # assert 'Invalid authentication token' in data['error'] + + def test_diary_create_invalid_token(self): + response = send_post_data(self.client,url_for('views.diary_creation'), data=dict(token="e7326198-7055-4559-8d2b-b4568855211e")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Invalid authentication token' in data['error'] + + def test_diary_delete_no_token(self): + response = send_post(self.client,url_for('views.diary_delete')) + assert response.status_code == 400 + + # data = json.loads(response.data) + # assert 'error' in data + # assert 'status' in data + + # assert not data['status'] + # assert 'Invalid authentication token' in data['error'] + + def test_diary_delete_invalid_token(self): + response = send_post_data(self.client,url_for('views.diary_delete'), data=dict(token="e7326198-7055-4559-8d2b-b4568855211e")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Invalid authentication token' in data['error'] + + def test_diary_permission_no_token(self): + response = send_post(self.client,url_for('views.diary_permission')) + assert response.status_code == 400 + + # data = json.loads(response.data) + # assert 'error' in data + # assert 'status' in data + + # assert not data['status'] + # assert 'Invalid authentication token' in data['error'] + + def test_diary_permission_invalid_token(self): + response = send_post_data(self.client,url_for('views.diary_permission'), data=dict(token="e7326198-7055-4559-8d2b-b4568855211e")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Invalid authentication token' in data['error'] + + +if __name__ == '__main__': + pytest.main() diff --git a/tests/test_diary_nonempty.py b/tests/test_diary_nonempty.py new file mode 100644 index 0000000..df1b271 --- /dev/null +++ b/tests/test_diary_nonempty.py @@ -0,0 +1,242 @@ +import pytest +import json +import hashlib +import datetime +from flask import url_for +from flask import current_app as app +from src.service.models import User, Token, Diary +from utils import send_post_data + +from flask_mongoengine import MongoEngine +from mongoengine import connect +user1 = "user1" +user1pw = "password1" +user1name = "user1" +user1age = "1" + +user2 = "user2" +user2pw = "password2" +user2name = "user2" +user2age = "2" + +diary_time = datetime.datetime.now().isoformat() +diary_public_id = None +diary_public_title = 'Public title' +diary_public_text = 'Public text' + +diary_private_id = 222 +diary_private_title = 'Private title' +diary_private_text = 'Private text' + +token1uuid = "9cbf0381-38f0-46e3-8709-831e7ecbdd2e" +token2uuid = "a3a95081-a9af-4122-91cf-1804dbe8ad01" +expired_token = "2f1830bb-46d9-4df4-b456-93d3c15c9198" +localhost = '127.0.0.1' + + +def add_user(username, name, age, pw): + #SALT = app.config.get('SALT') + SALT='IfHYBwi5ZUFZD9VaonnK' + hash_password = hashlib.sha512(pw + SALT + username).hexdigest() + User(username=username, hashed_password=hash_password, fullname=name, age=age).save() + user = User.objects(username=username).first() + return str(user.pk) + + +def add_token(token_str, data, expired=False): + token = Token(token=token_str, data=json.dumps(data), isexpired=expired) + token.save() + + +def add_diary( title, username, published_time, public, text): + diary = Diary(title=title, username=username, published_time=published_time, public=public, text=text) + diary.save() + #diary_public_id=diary.id + return diary.id + + +def delete_user(username): + user = User.objects(username=username).first() + user.delete() + + +def delete_token(token_str): + token = Token.objects(token=token_str).first() + if token: + token.delete() + + +def delete_diary(diary_id): + diary = Diary.objects(id=diary_id).first() + if diary: + diary.delete() + + +@pytest.mark.usefixtures('client_class') +class TestDiaryNonEmpty(object): + @classmethod + def setup_class(cls): + + db = connect('db_test',host='mongodb') + db.drop_database('db_test') + db.close() + global diary_public_id,diary_private_id + user1id = add_user(user1, user1name, user1age, user1pw) + data = {'pk': user1id, 'ip': localhost} + add_token(token1uuid, data) + add_token(expired_token, data, True) + + user2id = add_user(user2, user2name, user2age, user2pw) + data = {'pk': user2id, 'ip': localhost} + add_token(token2uuid, data) + + diary_public_id=add_diary( diary_public_title, user1, diary_time, True, diary_public_text) + diary_private_id=add_diary( diary_private_title, user1, diary_time, False, diary_private_text) + + @classmethod + def teardown_class(cls): + delete_user(user1) + delete_user(user2) + + delete_token(token1uuid) + delete_token(expired_token) + delete_token(token2uuid) + + delete_diary(diary_public_id) + delete_diary(diary_private_id) + + def test_diary_get(self): + response = self.client.get(url_for('views.diary')) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'result' in data + assert 'status' in data + + assert data['status'] + assert len(data['result']) == 1 + + diary_data = json.loads(data['result'][0]) + assert diary_data['id'] == int(diary_public_id) + assert diary_data['title'] == diary_public_title + assert diary_data['author'] == user1 + assert diary_data['publish_date'] == diary_time + assert diary_data['public'] + assert diary_data['text'] == diary_public_text + + def test_diary_post_owner(self): + response = send_post_data(self.client,url_for('views.diary'), + data=dict(token=token1uuid)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'result' in data + assert 'status' in data + + assert data['status'] + assert len(data['result']) == 2 + + diary_data = json.loads(data['result'][1]) + assert diary_data['id'] == int(diary_private_id) + assert diary_data['title'] == diary_private_title + assert diary_data['author'] == user1 + assert diary_data['publish_date'] == diary_time + assert not diary_data['public'] + assert diary_data['text'] == diary_private_text + + def test_diary_post_not_owner(self): + response = send_post_data(self.client,url_for('views.diary'), + data=dict(token=token2uuid)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'result' in data + assert 'status' in data + + assert data['status'] + assert len(data['result']) == 0 + + def test_diary_create_success(self): + response = send_post_data(self.client,url_for('views.diary_creation'), + data=dict(token=token2uuid,title=diary_public_title,text=diary_public_text,public=True)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'result' in data + assert 'status' in data + assert 'id' in data['result'] + + assert data['status'] + assert data['result']['id'] == 3 + + delete_diary(data['result']['id']) + + def test_diary_create_error(self): + response = send_post_data(self.client,url_for('views.diary_creation'), + data=dict(token=token2uuid)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + + def test_diary_delete_not_owner(self): + response = send_post_data(self.client,url_for('views.diary_delete'), + data=dict(token=token2uuid, id=diary_private_id)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'status' in data + assert not data['status'] + + def test_diary_delete_owner(self): + diary_id = add_diary("test_diary_delete", user2, diary_time, True, "user2 owner") + response = send_post_data(self.client,url_for('views.diary_delete'), + data=dict(token=token2uuid, id=diary_id)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'status' in data + assert data['status'] + + diary = Diary.objects(id=999).first() + assert not diary + + def test_diary_permission_not_owner(self): + response = send_post_data(self.client,url_for('views.diary_permission'), + data=dict(token=token2uuid, id=diary_private_id, public=True)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'status' in data + assert not data['status'] + + diary = Diary.objects(id=diary_private_id).first() + assert not diary.public + + def test_diary_permission_owner_private_to_public(self): + response = send_post_data(self.client,url_for('views.diary_permission'), + data=dict(token=token1uuid, id=diary_private_id, public=True)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'status' in data + assert data['status'] + + diary = Diary.objects(id=diary_private_id).first() + assert diary.public + + def test_diary_permission_owner_public_to_private(self): + response = send_post_data(self.client,url_for('views.diary_permission'), + data=dict(token=token1uuid, id=diary_public_id, public=False)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'status' in data + assert data['status'] + + diary = Diary.objects(id=diary_public_id).first() + assert not diary.public + + +if __name__ == '__main__': + pytest.main() diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..c9ed82e --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,20 @@ +import pytest +import json +from flask import Flask, url_for +from src.service.views import ENDPOINT_LIST + + +def test_get_index(client): + page = client.get(url_for('views.index')) # can use the endpoint(method) name here + assert page.status_code == 200 # response code + + data = json.loads(page.data) # response data + assert 'result' in data + assert 'status' in data + + assert data['status'] + assert data['result'] == ENDPOINT_LIST + + +if __name__ == '__main__': + pytest.main() diff --git a/tests/test_meta.py b/tests/test_meta.py new file mode 100644 index 0000000..2781c4b --- /dev/null +++ b/tests/test_meta.py @@ -0,0 +1,28 @@ +import pytest +import json +from flask import url_for + + +def test_meta_heartbeat(client): + page = client.get(url_for('views.meta_heartbeat')) + assert page.status_code == 200 + + data = json.loads(page.data) + assert 'status' in data + assert data['status'] + + +def test_meta_members(client): + page = client.get(url_for('views.meta_members')) + assert page.status_code == 200 + + data = json.loads(page.data) + assert 'status' in data + assert 'result' in data + + assert data['status'] + assert data['result'] == ['Zawlin', 'Xue Si', 'Shi Qing', 'Chen Hui'] + + +if __name__ == '__main__': + pytest.main() diff --git a/tests/test_users_empty.py b/tests/test_users_empty.py new file mode 100644 index 0000000..1622a5e --- /dev/null +++ b/tests/test_users_empty.py @@ -0,0 +1,205 @@ +import pytest +import json +from flask import url_for +from src.service.models import User +import urllib2 +from utils import send_post_data, send_post + +from flask_mongoengine import MongoEngine +from mongoengine import connect +@pytest.mark.usefixtures('client_class') +class TestUsersEmpty(object): + + @classmethod + def setup_class(self): + db = connect('db_test',host='mongodb') + db.drop_database('db_test') + db.close() + + + @classmethod + def teardown_class(self): + pass + + def test_users_no_token(self): + response = send_post(self.client, + url_for('views.users')) + assert response.status_code == 400 + + # data = json.loads(response.data) + # assert 'error' in data + # assert 'status' in data + # + # assert not data['status'] + # assert 'Invalid authentication token' in data['error'] + + def test_users_invalid_token(self): + response = send_post_data(self.client, + url_for('views.users'), + dict(token="b563fdc7-1c1c-46d8-a7a0-42ea1f1d9c4d")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Invalid authentication token' in data['error'] + + def test_user_register_no_args(self): + response = send_post(self.client, + url_for('views.users_register')) + assert response.status_code == 400 + + # data = json.loads(response.data) + # assert 'error' in data + # assert 'status' in data + # + # assert not data['status'] + # assert 'Required parameter is missing' in data['error'] + + def test_user_register_no_username(self): + response = send_post_data(self.client, + url_for('views.users_register'), + dict(password="2", fullname="3", age="4")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Required parameter is missing' in data['error'] + + def test_user_register_no_password(self): + response = send_post_data(self.client, + url_for('views.users_register'), + dict(username="1", fullname="3", age="4")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Required parameter is missing' in data['error'] + + def test_user_register_no_fullname(self): + response = send_post_data(self.client, + url_for('views.users_register'), + dict(username="1", password="2", age="4")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Required parameter is missing' in data['error'] + + def test_user_register_no_age(self): + response = send_post_data(self.client, + url_for('views.users_register'), + dict(username="1", password="2", fullname="3")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Required parameter is missing' in data['error'] + + def test_user_register_success(self): + response = send_post_data(self.client, + url_for('views.users_register'), + dict(username="1", password="2", fullname="3", age="4")) + assert response.status_code == 201 + + data = json.loads(response.data) + assert 'error' not in data + assert 'status' in data + + assert data['status'] + + # delete user + user = User.objects(username="1").first() + user.delete() + assert not User.objects(username="1").first() + + def test_user_authenticate_no_args(self): + response = send_post(self.client, + url_for('views.users_authenticate')) + assert response.status_code == 400 + + # data = json.loads(response.data) + # assert 'error' in data + # assert 'status' in data + # + # assert not data['status'] + # assert 'Required parameter is missing' in data['error'] + + def test_user_authenticate_no_username(self): + response = send_post_data(self.client, + url_for('views.users_authenticate'), + dict(password="2")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Required parameter is missing' in data['error'] + + def test_user_authenticate_no_password(self): + response = send_post_data(self.client, + url_for('views.users_authenticate'), + dict(username="1")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Required parameter is missing' in data['error'] + + def test_user_authenticate_valid_args_no_token(self): + response = send_post_data(self.client, + url_for('views.users_authenticate'), + dict(username="1", password="2")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' not in data + assert 'status' in data + + assert not data['status'] + + def test_users_expire_no_token(self): + response = send_post(self.client, + url_for('views.users_expire')) + assert response.status_code == 400 + # + # data = json.loads(response.data) + # assert 'error' not in data + # assert 'status' in data + # + # assert not data['status'] + + def test_users_expire_invalid_token(self): + response = send_post_data(self.client, + url_for('views.users_expire'), + dict(token="b563fdc7-1c1c-46d8-a7a0-42ea1f1d9c4d")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' not in data + assert 'status' in data + + assert not data['status'] + + +if __name__ == '__main__': + pytest.main() diff --git a/tests/test_users_nonempty.py b/tests/test_users_nonempty.py new file mode 100644 index 0000000..34480f0 --- /dev/null +++ b/tests/test_users_nonempty.py @@ -0,0 +1,130 @@ +import pytest +import json +import hashlib +from flask import url_for +#from src.service.app import SALT +from flask import current_app as app +from src.service.models import User, Token, is_token_valid +from utils import send_post_data +user1 = "user1" +user1pw = "password1" +user1name = "user1" +user1age = "1" +token1uuid = "f7d86d6c-2c13-47b2-8d45-3da9cf943fc9" +expired_token = "e7326198-7055-4559-8d2b-b4568855211e" +localhost = '127.0.0.1' + +from flask_mongoengine import MongoEngine +from mongoengine import connect + +@pytest.mark.usefixtures('client_class') +class TestUsersNonEmpty(object): + @classmethod + def setup_class(cls): + + db = connect('db_test',host='mongodb') + db.drop_database('db_test') + db.close() + # db = connect('mongodb') + # db.drop_database('db_test') + # db.close() + # SALT = app.config.get('SALT') + # print SALT + SALT='IfHYBwi5ZUFZD9VaonnK' + hash_password = hashlib.sha512(user1pw + SALT + user1).hexdigest() + User(username=user1, hashed_password=hash_password, fullname=user1name, age=user1age).save() + user = User.objects(username=user1).first() + userid = str(user.pk) + + data = {'pk': userid, 'ip': localhost} + token = Token(token=token1uuid, data=json.dumps(data)) + token.save() + + token = Token(token=expired_token, data=json.dumps(data), isexpired=True) + token.save() + + @classmethod + def teardown_class(cls): + user = User.objects(username=user1).first() + user.delete() + + token = Token.objects(token=token1uuid).first() + if token: + token.delete() + + token = Token.objects(token=expired_token).first() + if token: + token.delete() + + def test_users_valid_token(self): + response = send_post_data(self.client, + url_for('views.users'), + dict(token=token1uuid)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'result' in data + assert 'status' in data + + assert data['status'] + user_data = json.loads(data['result']) + assert user_data['username'] == user1 + assert user_data['fullname'] == user1name + assert user_data['age'] == int(user1age) + + def test_users_register_username_exist(self): + response = send_post_data(self.client, + url_for('views.users_register'), + dict(username=user1, password="2", fullname="3", age="4")) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'error' in data + assert 'status' in data + + assert not data['status'] + assert 'Username already exists' in data['error'] + + def test_users_authenticate_success(self): + response = send_post_data(self.client, + url_for('views.users_authenticate'), + dict(username=user1, password=user1pw,fullname=user1name,age=user1age)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'status' in data + assert 'result' in data + assert 'token' in data['result'] + + assert data['status'] + token_str = data['result']['token'] + assert is_token_valid(token_str) + + token = Token.objects(token=token_str).first() + token.delete() + assert not Token.objects(token=token_str).first() + + def test_users_expire_token_success(self): + response = send_post_data(self.client, + url_for('views.users_expire'), + dict(token=token1uuid)) + assert response.status_code == 200 + + data = json.loads(response.data) + print data['status'] + assert 'status' in data + assert data['status'] + + def test_users_expire_expired_token(self): + response = send_post_data(self.client, + url_for('views.users_expire'), + dict(token=expired_token)) + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'status' in data + assert not data['status'] + + +if __name__ == '__main__': + pytest.main() diff --git a/tests/tests.py b/tests/tests.py new file mode 100644 index 0000000..c9978a2 --- /dev/null +++ b/tests/tests.py @@ -0,0 +1 @@ +print "Doing nothing" diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..af515d3 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,14 @@ +import json + + +def send_post_data(client, endpoint, data): + return client.post(endpoint, + data=json.dumps(data), + content_type='application/json', + environ_base={'REMOTE_ADDR': '127.0.0.1'}) + + +def send_post(client, endpoint): + return client.post(endpoint, + content_type='application/json', + environ_base={'REMOTE_ADDR': '127.0.0.1'})