Remove unused files
This commit is contained in:
parent
33a75609bd
commit
52ba579637
|
@ -1,35 +0,0 @@
|
||||||
# pull official base image
|
|
||||||
FROM python:3.11.2-slim-buster AS prod
|
|
||||||
|
|
||||||
# set working directory
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
# set environment variables
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
|
||||||
ENV FLASK_DEBUG 0
|
|
||||||
ENV FLASK_ENV production
|
|
||||||
ARG SECRET_KEY
|
|
||||||
ENV SECRET_KEY $SECRET_KEY
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get -y install netcat gcc postgresql \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& groupadd -g 999 python \
|
|
||||||
&& useradd -r -u 999 -g python python \
|
|
||||||
&& python -m venv /usr/src/app/.venv \
|
|
||||||
&& chown -R python:python /usr/src/app
|
|
||||||
|
|
||||||
ENV PATH="/usr/src/app/.venv/bin:$PATH"
|
|
||||||
ENV PIP_NO_CACHE_DIR=off
|
|
||||||
USER 999
|
|
||||||
|
|
||||||
COPY --chown=python:python requirements.txt requirements.txt
|
|
||||||
RUN python -m pip install --upgrade pip && \
|
|
||||||
python -m pip install -r requirements.txt
|
|
||||||
|
|
||||||
COPY --chown=python:python . .
|
|
||||||
|
|
||||||
# run gunicorn
|
|
||||||
CMD ["/usr/src/app/.venv/bin/gunicorn", "manage:app"]
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
# pull official base image
|
|
||||||
ARG BASE_IMAGE
|
|
||||||
FROM ${BASE_IMAGE}
|
|
||||||
|
|
||||||
ENV FLASK_DEBUG=1
|
|
||||||
ENV FLASK_ENV=development
|
|
||||||
ENV DATABASE_TEST_URL=postgresql://postgres:postgres@api-db:5432/api_test
|
|
||||||
|
|
||||||
# add and install requirements
|
|
||||||
COPY --chown=python:python ./requirements.test.txt .
|
|
||||||
RUN python -m pip install -r requirements.test.txt
|
|
||||||
|
|
||||||
# add app
|
|
||||||
COPY --chown=python:python src/tests src/tests
|
|
||||||
|
|
||||||
# new
|
|
||||||
# add entrypoint.sh
|
|
||||||
COPY --chown=python:python src/.cicd/test.sh .
|
|
||||||
RUN chmod +x /usr/src/app/test.sh
|
|
||||||
|
|
||||||
CMD ["/usr/src/app/test.sh"]
|
|
|
@ -1,16 +0,0 @@
|
||||||
[[source]]
|
|
||||||
url = "https://pypi.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
name = "pypi"
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
flask = "==2.2.3"
|
|
||||||
Werkzeug = "==2.3.7"
|
|
||||||
flask-restx = "==1.0.6"
|
|
||||||
flask-sqlalchemy = "==3.0.3"
|
|
||||||
psycopg2-binary = "==2.9.5"
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
|
|
||||||
[requires]
|
|
||||||
python_version = "3.11"
|
|
|
@ -1,30 +0,0 @@
|
||||||
from flask.cli import FlaskGroup
|
|
||||||
|
|
||||||
from src import create_app, db
|
|
||||||
from src.api.models.users import User
|
|
||||||
from src.api.models.zones import Zone
|
|
||||||
|
|
||||||
|
|
||||||
app = create_app() # new
|
|
||||||
cli = FlaskGroup(create_app=create_app)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("recreate_db")
|
|
||||||
def recreate_db():
|
|
||||||
db.drop_all()
|
|
||||||
db.create_all()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("seed_db")
|
|
||||||
def seed_db():
|
|
||||||
db.session.add(User(username="fede", email="fede@gmail.com", password="password1234"))
|
|
||||||
db.session.add(User(username="martin", email="martin@gmail.com", password="password1234"))
|
|
||||||
db.session.add(User(username="nacho", email="nacho@gmail.com", password="password1234"))
|
|
||||||
db.session.add(Zone(name="Belgrano"))
|
|
||||||
db.session.add(Zone(name="San Isidro"))
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
cli()
|
|
|
@ -1,10 +0,0 @@
|
||||||
## Testing
|
|
||||||
pytest==7.2.2
|
|
||||||
pytest-cov==4.0.0
|
|
||||||
flake8==6.0.0
|
|
||||||
black==23.1.0
|
|
||||||
isort==5.12.0
|
|
||||||
bandit==1.7.5
|
|
||||||
pactman==2.3.0
|
|
||||||
pytest-xdist==3.2.0
|
|
||||||
pytest-watch==4.2.0
|
|
|
@ -1,10 +0,0 @@
|
||||||
## Prod
|
|
||||||
flask==2.2.3
|
|
||||||
Werkzeug==2.3.7
|
|
||||||
flask-restx==1.0.6
|
|
||||||
Flask-SQLAlchemy==3.0.3
|
|
||||||
psycopg2-binary==2.9.5
|
|
||||||
flask-cors==3.0.10
|
|
||||||
flask-bcrypt==1.0.1
|
|
||||||
pyjwt==2.6.0
|
|
||||||
gunicorn==20.1.0
|
|
|
@ -1,23 +0,0 @@
|
||||||
#!/bin/bash -e
|
|
||||||
|
|
||||||
|
|
||||||
if [ "${TEST_TARGET:-}" = "INTEGRATION" ]; then
|
|
||||||
# Execute your command here
|
|
||||||
/usr/src/app/.venv/bin/gunicorn manage:app
|
|
||||||
else
|
|
||||||
## pytest
|
|
||||||
python -m pytest "src/tests" --junitxml=report.xml
|
|
||||||
|
|
||||||
## Coverage
|
|
||||||
python -m pytest "src/tests" -p no:warnings --cov="src" --cov-report xml
|
|
||||||
|
|
||||||
|
|
||||||
## Linting
|
|
||||||
flake8 src --extend-ignore E221
|
|
||||||
# black src --check
|
|
||||||
# isort src --check
|
|
||||||
|
|
||||||
## Security
|
|
||||||
# bandit -c .bandit.yml -r .
|
|
||||||
fi
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask_bcrypt import Bcrypt
|
|
||||||
from flask_cors import CORS
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
|
|
||||||
# instantiate the db
|
|
||||||
db = SQLAlchemy()
|
|
||||||
cors = CORS()
|
|
||||||
bcrypt = Bcrypt()
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(script_info=None):
|
|
||||||
# instantiate the app
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# set config
|
|
||||||
app_settings = os.getenv("APP_SETTINGS")
|
|
||||||
app.config.from_object(app_settings)
|
|
||||||
|
|
||||||
# set up extensions
|
|
||||||
db.init_app(app)
|
|
||||||
cors.init_app(app, resources={r"*": {"origins": "*"}})
|
|
||||||
|
|
||||||
# register api
|
|
||||||
from src.api import api
|
|
||||||
|
|
||||||
api.init_app(app)
|
|
||||||
|
|
||||||
# shell context for flask cli
|
|
||||||
@app.shell_context_processor
|
|
||||||
def ctx():
|
|
||||||
return {"app": app, "db": db}
|
|
||||||
|
|
||||||
return app
|
|
|
@ -1,15 +0,0 @@
|
||||||
from flask_restx import Api
|
|
||||||
from src.api.auth import auth_namespace
|
|
||||||
from src.api.ping import ping_namespace
|
|
||||||
from src.api.users import NAMESPACE as NAMESPACE_USERS
|
|
||||||
from src.api.users import users_namespace
|
|
||||||
from src.api.zones import NAMESPACE as NAMESPACE_ZONES
|
|
||||||
from src.api.zones import zones_namespace
|
|
||||||
|
|
||||||
api = Api(version="1.0", title="Users API", doc="/doc")
|
|
||||||
|
|
||||||
|
|
||||||
api.add_namespace(ping_namespace, path="/ping")
|
|
||||||
api.add_namespace(users_namespace, path=f"/{NAMESPACE_USERS}")
|
|
||||||
api.add_namespace(auth_namespace, path="/auth")
|
|
||||||
api.add_namespace(zones_namespace, path=f"/{NAMESPACE_ZONES}")
|
|
|
@ -1,123 +0,0 @@
|
||||||
import jwt
|
|
||||||
from flask import request
|
|
||||||
from flask_restx import Namespace, Resource
|
|
||||||
from src import bcrypt
|
|
||||||
from src.api.cruds.users import add_user, get_user_by_email, get_user_by_id
|
|
||||||
from src.api.models.users import User
|
|
||||||
|
|
||||||
auth_namespace = Namespace("auth")
|
|
||||||
|
|
||||||
auth_user_model = User.get_api_auth_user_model(auth_namespace)
|
|
||||||
auth_full_user_model = User.get_api_auth_full_user_model(auth_namespace)
|
|
||||||
auth_login_model = User.get_api_auth_login_model(auth_namespace)
|
|
||||||
auth_refresh_model = User.get_api_auth_refresh_model(auth_namespace)
|
|
||||||
auth_tokens_model = User.get_api_auth_tokens_model(auth_namespace)
|
|
||||||
|
|
||||||
parser = auth_namespace.parser()
|
|
||||||
parser.add_argument("Authorization", location="headers")
|
|
||||||
|
|
||||||
|
|
||||||
class Register(Resource):
|
|
||||||
@auth_namespace.marshal_with(auth_user_model)
|
|
||||||
@auth_namespace.expect(auth_full_user_model, validate=True)
|
|
||||||
@auth_namespace.response(201, "Success")
|
|
||||||
@auth_namespace.response(400, "Sorry. That email already exists.")
|
|
||||||
def post(self):
|
|
||||||
post_data = request.get_json()
|
|
||||||
username = post_data.get("username")
|
|
||||||
email = post_data.get("email")
|
|
||||||
password = post_data.get("password")
|
|
||||||
|
|
||||||
user = get_user_by_email(email)
|
|
||||||
if user:
|
|
||||||
auth_namespace.abort(400, "Sorry. That email already exists.")
|
|
||||||
user = add_user(username, email, password)
|
|
||||||
|
|
||||||
return user, 201
|
|
||||||
|
|
||||||
|
|
||||||
class Login(Resource):
|
|
||||||
@auth_namespace.marshal_with(auth_tokens_model)
|
|
||||||
@auth_namespace.expect(auth_login_model, validate=True)
|
|
||||||
@auth_namespace.response(201, "Success")
|
|
||||||
@auth_namespace.response(404, "User does not exist")
|
|
||||||
def post(self):
|
|
||||||
post_data = request.get_json()
|
|
||||||
email = post_data.get("email")
|
|
||||||
password = post_data.get("password")
|
|
||||||
response_object = {}
|
|
||||||
|
|
||||||
user = get_user_by_email(email)
|
|
||||||
if not user or not bcrypt.check_password_hash(user.password, password):
|
|
||||||
auth_namespace.abort(404, "User does not exist")
|
|
||||||
|
|
||||||
access_token = user.encode_token(user.id, "access")
|
|
||||||
refresh_token = user.encode_token(user.id, "refresh")
|
|
||||||
|
|
||||||
response_object = {"access_token": access_token, "refresh_token": refresh_token}
|
|
||||||
return response_object, 200
|
|
||||||
|
|
||||||
|
|
||||||
class Refresh(Resource):
|
|
||||||
@auth_namespace.marshal_with(auth_tokens_model)
|
|
||||||
@auth_namespace.expect(auth_refresh_model, validate=True)
|
|
||||||
@auth_namespace.response(200, "Success")
|
|
||||||
@auth_namespace.response(401, "Invalid token")
|
|
||||||
def post(self):
|
|
||||||
post_data = request.get_json()
|
|
||||||
refresh_token = post_data.get("refresh_token")
|
|
||||||
response_object = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = User.decode_token(refresh_token)
|
|
||||||
user = get_user_by_id(user_id)
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
auth_namespace.abort(401, "Invalid token")
|
|
||||||
|
|
||||||
access_token = user.encode_token(user.id, "access")
|
|
||||||
refresh_token = user.encode_token(user.id, "refresh")
|
|
||||||
|
|
||||||
response_object = {
|
|
||||||
"access_token": access_token,
|
|
||||||
"refresh_token": refresh_token,
|
|
||||||
}
|
|
||||||
return response_object, 200
|
|
||||||
except jwt.ExpiredSignatureError:
|
|
||||||
auth_namespace.abort(401, "Signature expired. Please log in again.")
|
|
||||||
return "Signature expired. Please log in again."
|
|
||||||
except jwt.InvalidTokenError:
|
|
||||||
auth_namespace.abort(401, "Invalid token. Please log in again.")
|
|
||||||
|
|
||||||
|
|
||||||
class Status(Resource):
|
|
||||||
@auth_namespace.marshal_with(auth_user_model)
|
|
||||||
@auth_namespace.response(200, "Success")
|
|
||||||
@auth_namespace.response(401, "Invalid token")
|
|
||||||
@auth_namespace.expect(parser)
|
|
||||||
def get(self):
|
|
||||||
auth_header = request.headers.get("Authorization")
|
|
||||||
if auth_header:
|
|
||||||
try:
|
|
||||||
auth_header_list = auth_header.split(" ")
|
|
||||||
if len(auth_header_list) != 2:
|
|
||||||
raise jwt.InvalidTokenError(f"Invalid header: {auth_header}")
|
|
||||||
access_token = auth_header_list[1]
|
|
||||||
resp = User.decode_token(access_token)
|
|
||||||
user = get_user_by_id(resp)
|
|
||||||
if not user:
|
|
||||||
auth_namespace.abort(401, "Invalid token")
|
|
||||||
return user, 200
|
|
||||||
except jwt.ExpiredSignatureError:
|
|
||||||
auth_namespace.abort(401, "Signature expired. Please log in again.")
|
|
||||||
return "Signature expired. Please log in again."
|
|
||||||
except jwt.InvalidTokenError:
|
|
||||||
auth_namespace.abort(401, "Invalid token. Please log in again.")
|
|
||||||
else:
|
|
||||||
auth_namespace.abort(403, "Token required")
|
|
||||||
|
|
||||||
|
|
||||||
auth_namespace.add_resource(Register, "/register")
|
|
||||||
auth_namespace.add_resource(Login, "/login")
|
|
||||||
auth_namespace.add_resource(Refresh, "/refresh")
|
|
||||||
auth_namespace.add_resource(Status, "/status")
|
|
|
@ -1,34 +0,0 @@
|
||||||
from src import db
|
|
||||||
from src.api.models import User
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_users():
|
|
||||||
return User.query.all()
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_id(user_id):
|
|
||||||
return User.query.filter_by(id=user_id).first()
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_email(email):
|
|
||||||
return User.query.filter_by(email=email).first()
|
|
||||||
|
|
||||||
|
|
||||||
def add_user(username, email, password):
|
|
||||||
user = User(username=username, email=email, password=password)
|
|
||||||
db.session.add(user)
|
|
||||||
db.session.commit()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def update_user(user, username, email):
|
|
||||||
user.username = username
|
|
||||||
user.email = email
|
|
||||||
db.session.commit()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def delete_user(user):
|
|
||||||
db.session.delete(user)
|
|
||||||
db.session.commit()
|
|
||||||
return user
|
|
|
@ -1,34 +0,0 @@
|
||||||
from src import db
|
|
||||||
from src.api.models.users import User
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_users():
|
|
||||||
return User.query.all()
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_id(user_id):
|
|
||||||
return User.query.filter_by(id=user_id).first()
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_email(email):
|
|
||||||
return User.query.filter_by(email=email).first()
|
|
||||||
|
|
||||||
|
|
||||||
def add_user(username, email, password):
|
|
||||||
user = User(username=username, email=email, password=password)
|
|
||||||
db.session.add(user)
|
|
||||||
db.session.commit()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def update_user(user, username, email):
|
|
||||||
user.username = username
|
|
||||||
user.email = email
|
|
||||||
db.session.commit()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def delete_user(user):
|
|
||||||
db.session.delete(user)
|
|
||||||
db.session.commit()
|
|
||||||
return user
|
|
|
@ -1,29 +0,0 @@
|
||||||
from src import db
|
|
||||||
from src.api.models.zones import Zone
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_zones():
|
|
||||||
return Zone.query.all()
|
|
||||||
|
|
||||||
|
|
||||||
def get_zone_by_id(zone_id):
|
|
||||||
return Zone.query.filter_by(id=zone_id).first()
|
|
||||||
|
|
||||||
|
|
||||||
def add_zone(name):
|
|
||||||
zone = Zone(name=name)
|
|
||||||
db.session.add(zone)
|
|
||||||
db.session.commit()
|
|
||||||
return zone
|
|
||||||
|
|
||||||
|
|
||||||
def update_zone(zone, name):
|
|
||||||
zone.name = name
|
|
||||||
db.session.commit()
|
|
||||||
return zone
|
|
||||||
|
|
||||||
|
|
||||||
def delete_zone(zone):
|
|
||||||
db.session.delete(zone)
|
|
||||||
db.session.commit()
|
|
||||||
return zone
|
|
|
@ -1,115 +0,0 @@
|
||||||
import datetime
|
|
||||||
|
|
||||||
import jwt
|
|
||||||
from flask import current_app
|
|
||||||
from flask_restx import fields
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from src import bcrypt, db
|
|
||||||
|
|
||||||
|
|
||||||
class User(db.Model):
|
|
||||||
__tablename__ = "users"
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
||||||
username = db.Column(db.String(128), nullable=False)
|
|
||||||
email = db.Column(db.String(128), nullable=False)
|
|
||||||
password = db.Column(db.String(255), nullable=False)
|
|
||||||
active = db.Column(db.Boolean(), default=True, nullable=False)
|
|
||||||
created_date = db.Column(db.DateTime, default=func.now(), nullable=False)
|
|
||||||
|
|
||||||
def __init__(self, username, email, password):
|
|
||||||
self.username = username
|
|
||||||
self.email = email
|
|
||||||
self.password = bcrypt.generate_password_hash(
|
|
||||||
password, current_app.config.get("BCRYPT_LOG_ROUNDS")
|
|
||||||
).decode()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def encode_token(user_id, token_type):
|
|
||||||
print(f"encode_token(user_id={user_id}, token_type={token_type}):")
|
|
||||||
if token_type == "access":
|
|
||||||
seconds = current_app.config.get("ACCESS_TOKEN_EXPIRATION")
|
|
||||||
else:
|
|
||||||
seconds = current_app.config.get("REFRESH_TOKEN_EXPIRATION")
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds),
|
|
||||||
"iat": datetime.datetime.utcnow(),
|
|
||||||
"sub": user_id,
|
|
||||||
}
|
|
||||||
return jwt.encode(
|
|
||||||
payload, current_app.config.get("SECRET_KEY"), algorithm="HS256"
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def decode_token(token):
|
|
||||||
decoded = jwt.decode(
|
|
||||||
token, current_app.config.get("SECRET_KEY"), algorithms=["HS256"]
|
|
||||||
)
|
|
||||||
return decoded["sub"]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_user_model(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"User",
|
|
||||||
{
|
|
||||||
"id": fields.Integer(readOnly=True),
|
|
||||||
"username": fields.String(required=True),
|
|
||||||
"email": fields.String(required=True),
|
|
||||||
"created_date": fields.DateTime,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_user_post_model(cls, namespace):
|
|
||||||
return namespace.inherit(
|
|
||||||
"User Post",
|
|
||||||
cls.get_api_user_model(namespace),
|
|
||||||
{
|
|
||||||
"password": fields.String(required=False),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_user_model(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"User",
|
|
||||||
{
|
|
||||||
"username": fields.String(required=True),
|
|
||||||
"email": fields.String(required=True),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_full_user_model(cls, namespace):
|
|
||||||
return namespace.clone(
|
|
||||||
"User Full",
|
|
||||||
cls.get_api_auth_user_model(namespace),
|
|
||||||
{
|
|
||||||
"password": fields.String(required=True),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_login_model(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"User",
|
|
||||||
{
|
|
||||||
"password": fields.String(required=True),
|
|
||||||
"email": fields.String(required=True),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_refresh_model(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"Refresh", {"refresh_token": fields.String(required=True)}
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_tokens_model(cls, namespace):
|
|
||||||
return namespace.clone(
|
|
||||||
"Access and Refresh Token",
|
|
||||||
cls.get_api_auth_refresh_model(namespace),
|
|
||||||
{"access_token": fields.String(required=True)},
|
|
||||||
)
|
|
|
@ -1,20 +0,0 @@
|
||||||
from flask_restx import fields
|
|
||||||
|
|
||||||
|
|
||||||
def get_model_create_response(namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"GenericCreateResponse",
|
|
||||||
{
|
|
||||||
"id": fields.Integer(readOnly=True),
|
|
||||||
"message": fields.String(required=True),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_model_error_response(namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"GenericCreateResponse",
|
|
||||||
{
|
|
||||||
"message": fields.String(required=True),
|
|
||||||
},
|
|
||||||
)
|
|
|
@ -1,115 +0,0 @@
|
||||||
import datetime
|
|
||||||
|
|
||||||
import jwt
|
|
||||||
from flask import current_app
|
|
||||||
from flask_restx import fields
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from src import bcrypt, db
|
|
||||||
|
|
||||||
|
|
||||||
class User(db.Model):
|
|
||||||
__tablename__ = "users"
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
||||||
username = db.Column(db.String(128), nullable=False)
|
|
||||||
email = db.Column(db.String(128), nullable=False)
|
|
||||||
password = db.Column(db.String(255), nullable=False)
|
|
||||||
active = db.Column(db.Boolean(), default=True, nullable=False)
|
|
||||||
created_date = db.Column(db.DateTime, default=func.now(), nullable=False)
|
|
||||||
|
|
||||||
def __init__(self, username, email, password):
|
|
||||||
self.username = username
|
|
||||||
self.email = email
|
|
||||||
self.password = bcrypt.generate_password_hash(
|
|
||||||
password, current_app.config.get("BCRYPT_LOG_ROUNDS")
|
|
||||||
).decode()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def encode_token(user_id, token_type):
|
|
||||||
print(f"encode_token(user_id={user_id}, token_type={token_type}):")
|
|
||||||
if token_type == "access":
|
|
||||||
seconds = current_app.config.get("ACCESS_TOKEN_EXPIRATION")
|
|
||||||
else:
|
|
||||||
seconds = current_app.config.get("REFRESH_TOKEN_EXPIRATION")
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds),
|
|
||||||
"iat": datetime.datetime.utcnow(),
|
|
||||||
"sub": user_id,
|
|
||||||
}
|
|
||||||
return jwt.encode(
|
|
||||||
payload, current_app.config.get("SECRET_KEY"), algorithm="HS256"
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def decode_token(token):
|
|
||||||
decoded = jwt.decode(
|
|
||||||
token, current_app.config.get("SECRET_KEY"), algorithms=["HS256"]
|
|
||||||
)
|
|
||||||
return decoded["sub"]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_user_model(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"User",
|
|
||||||
{
|
|
||||||
"id": fields.Integer(readOnly=True),
|
|
||||||
"username": fields.String(required=True),
|
|
||||||
"email": fields.String(required=True),
|
|
||||||
"created_date": fields.DateTime,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_user_post_model(cls, namespace):
|
|
||||||
return namespace.inherit(
|
|
||||||
"User Post",
|
|
||||||
cls.get_api_user_model(namespace),
|
|
||||||
{
|
|
||||||
"password": fields.String(required=False),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_user_model(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"User",
|
|
||||||
{
|
|
||||||
"username": fields.String(required=True),
|
|
||||||
"email": fields.String(required=True),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_full_user_model(cls, namespace):
|
|
||||||
return namespace.clone(
|
|
||||||
"User Full",
|
|
||||||
cls.get_api_auth_user_model(namespace),
|
|
||||||
{
|
|
||||||
"password": fields.String(required=True),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_login_model(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"User",
|
|
||||||
{
|
|
||||||
"password": fields.String(required=True),
|
|
||||||
"email": fields.String(required=True),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_refresh_model(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"Refresh", {"refresh_token": fields.String(required=True)}
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_api_auth_tokens_model(cls, namespace):
|
|
||||||
return namespace.clone(
|
|
||||||
"Access and Refresh Token",
|
|
||||||
cls.get_api_auth_refresh_model(namespace),
|
|
||||||
{"access_token": fields.String(required=True)},
|
|
||||||
)
|
|
|
@ -1,33 +0,0 @@
|
||||||
from flask_restx import fields
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from src import db
|
|
||||||
|
|
||||||
|
|
||||||
class Zone(db.Model):
|
|
||||||
__tablename__ = "zones"
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
||||||
name = db.Column(db.String(128), nullable=False)
|
|
||||||
create_date = db.Column(db.DateTime, default=func.now(), nullable=False)
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_model_full(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"ZoneFull",
|
|
||||||
{
|
|
||||||
"id": fields.Integer(readOnly=True),
|
|
||||||
"name": fields.String(required=True),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_model_create(cls, namespace):
|
|
||||||
return namespace.model(
|
|
||||||
"ZoneCreate",
|
|
||||||
{
|
|
||||||
"name": fields.String(required=False),
|
|
||||||
},
|
|
||||||
)
|
|
|
@ -1,34 +0,0 @@
|
||||||
from flask import Blueprint
|
|
||||||
from flask_restx import Api, Namespace, Resource
|
|
||||||
|
|
||||||
from src import db
|
|
||||||
from src.api.models.users import User
|
|
||||||
from src.api.models.zones import Zone
|
|
||||||
|
|
||||||
ping_blueprint = Blueprint("ping", __name__)
|
|
||||||
api = Api(ping_blueprint)
|
|
||||||
|
|
||||||
ping_namespace = Namespace("ping")
|
|
||||||
|
|
||||||
|
|
||||||
class Ping(Resource):
|
|
||||||
def delete(self):
|
|
||||||
db.drop_all()
|
|
||||||
db.create_all()
|
|
||||||
db.session.commit()
|
|
||||||
return {"status": "recreated"}
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
db.session.add(User(username="fede", email="fede@gmail.com", password="password1234"))
|
|
||||||
db.session.add(User(username="martin", email="martin@gmail.com", password="password1234"))
|
|
||||||
db.session.add(User(username="nacho", email="nacho@gmail.com", password="password1234"))
|
|
||||||
db.session.add(Zone(name="Belgrano"))
|
|
||||||
db.session.add(Zone(name="San Isidro"))
|
|
||||||
db.session.commit()
|
|
||||||
return {"status": "seeded"}
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return {"status": "success", "message": "pong!"}
|
|
||||||
|
|
||||||
|
|
||||||
ping_namespace.add_resource(Ping, "")
|
|
|
@ -1,101 +0,0 @@
|
||||||
from flask import request
|
|
||||||
from flask_restx import Namespace, Resource
|
|
||||||
from src.api.models.users import User
|
|
||||||
|
|
||||||
from src.api.cruds.users import ( # isort:skip
|
|
||||||
get_all_users,
|
|
||||||
get_user_by_email,
|
|
||||||
add_user,
|
|
||||||
get_user_by_id,
|
|
||||||
update_user,
|
|
||||||
delete_user,
|
|
||||||
)
|
|
||||||
|
|
||||||
NAMESPACE = "users"
|
|
||||||
|
|
||||||
users_namespace = Namespace(NAMESPACE)
|
|
||||||
|
|
||||||
user_api_model = User.get_api_user_model(users_namespace)
|
|
||||||
user_post_api_model = User.get_api_user_post_model(users_namespace)
|
|
||||||
|
|
||||||
|
|
||||||
class UsersList(Resource):
|
|
||||||
@users_namespace.response(200, "Success")
|
|
||||||
@users_namespace.marshal_with(user_api_model, as_list=True)
|
|
||||||
def get(self):
|
|
||||||
return get_all_users(), 200
|
|
||||||
|
|
||||||
@users_namespace.response(201, "<user_email> was added!")
|
|
||||||
@users_namespace.response(400, "Sorry. That email already exists.")
|
|
||||||
@users_namespace.expect(user_post_api_model, validate=True)
|
|
||||||
def post(self):
|
|
||||||
post_data = request.get_json()
|
|
||||||
username = post_data.get("username")
|
|
||||||
email = post_data.get("email")
|
|
||||||
password = post_data.get("password")
|
|
||||||
response_object = {}
|
|
||||||
|
|
||||||
user = get_user_by_email(email)
|
|
||||||
if user:
|
|
||||||
response_object["message"] = "Sorry. That email already exists."
|
|
||||||
return response_object, 400
|
|
||||||
|
|
||||||
user = add_user(username, email, password)
|
|
||||||
|
|
||||||
response_object["message"] = f"{email} was added!"
|
|
||||||
response_object["id"] = user.id
|
|
||||||
return response_object, 201
|
|
||||||
|
|
||||||
|
|
||||||
class Users(Resource):
|
|
||||||
@users_namespace.response(200, "Success")
|
|
||||||
@users_namespace.response(404, "User <user_id> does not exist")
|
|
||||||
@users_namespace.marshal_with(user_api_model)
|
|
||||||
def get(self, user_id):
|
|
||||||
user = get_user_by_id(user_id) # updated
|
|
||||||
if not user:
|
|
||||||
users_namespace.abort(404, f"User {user_id} does not exist")
|
|
||||||
return user, 200
|
|
||||||
|
|
||||||
@users_namespace.response(200, "<user_id> was updated!")
|
|
||||||
@users_namespace.response(404, "User <user_id> does not exist")
|
|
||||||
@users_namespace.response(400, "Sorry. That email already exists.")
|
|
||||||
@users_namespace.expect(user_api_model, validate=True)
|
|
||||||
def put(self, user_id):
|
|
||||||
post_data = request.get_json()
|
|
||||||
username = post_data.get("username")
|
|
||||||
email = post_data.get("email")
|
|
||||||
response_object = {}
|
|
||||||
|
|
||||||
user = get_user_by_id(user_id) # updated
|
|
||||||
if not user:
|
|
||||||
users_namespace.abort(404, f"User {user_id} does not exist")
|
|
||||||
|
|
||||||
if get_user_by_email(email): # updated
|
|
||||||
response_object["message"] = "Sorry. That email already exists."
|
|
||||||
return response_object, 400
|
|
||||||
|
|
||||||
user = update_user(user, username, email) # new
|
|
||||||
|
|
||||||
response_object["message"] = f"{user.id} was updated!"
|
|
||||||
response_object["id"] = user.id
|
|
||||||
return response_object, 200
|
|
||||||
|
|
||||||
@users_namespace.response(200, "<user_id> was removed!")
|
|
||||||
@users_namespace.response(404, "User <user_id> does not exist")
|
|
||||||
def delete(self, user_id):
|
|
||||||
response_object = {}
|
|
||||||
user = get_user_by_id(user_id)
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
users_namespace.abort(404, f"User {user_id} does not exist")
|
|
||||||
|
|
||||||
delete_user(user)
|
|
||||||
|
|
||||||
response_object["message"] = f"{user.email} was removed!"
|
|
||||||
response_object["id"] = user.id
|
|
||||||
return response_object, 200
|
|
||||||
|
|
||||||
|
|
||||||
users_namespace.add_resource(UsersList, "")
|
|
||||||
users_namespace.add_resource(Users, "/<int:user_id>")
|
|
|
@ -1,97 +0,0 @@
|
||||||
from flask import request
|
|
||||||
from flask_restx import Namespace, Resource
|
|
||||||
from src.api.models.generic import get_model_create_response
|
|
||||||
from src.api.models.zones import Zone as ZoneModel
|
|
||||||
|
|
||||||
from src.api.cruds.zones import ( # isort:skip
|
|
||||||
get_all_zones,
|
|
||||||
get_zone_by_id,
|
|
||||||
add_zone,
|
|
||||||
update_zone,
|
|
||||||
delete_zone,
|
|
||||||
)
|
|
||||||
|
|
||||||
NAMESPACE = "zones"
|
|
||||||
|
|
||||||
zones_namespace = Namespace(NAMESPACE)
|
|
||||||
|
|
||||||
zone_model_full = ZoneModel.get_model_full(namespace=zones_namespace)
|
|
||||||
zone_model_create = ZoneModel.get_model_create(namespace=zones_namespace)
|
|
||||||
zone_model_create_response = get_model_create_response(namespace=zones_namespace)
|
|
||||||
|
|
||||||
|
|
||||||
class ZonesList(Resource):
|
|
||||||
@zones_namespace.response(201, "Zone was added!")
|
|
||||||
@zones_namespace.response(400, "Invalid payload")
|
|
||||||
@zones_namespace.expect(zone_model_create, validate=True)
|
|
||||||
@zones_namespace.marshal_with(zone_model_create_response)
|
|
||||||
def post(self):
|
|
||||||
post_data = request.get_json()
|
|
||||||
name = post_data.get("name")
|
|
||||||
response_object = {}
|
|
||||||
|
|
||||||
if not name:
|
|
||||||
response_object["message"] = "Invalid payload"
|
|
||||||
return response_object, 400
|
|
||||||
|
|
||||||
zone = add_zone(name=name)
|
|
||||||
|
|
||||||
response_object["message"] = f"{name} was added!"
|
|
||||||
response_object["id"] = zone.id
|
|
||||||
return response_object, 201
|
|
||||||
|
|
||||||
@zones_namespace.response(200, "Success")
|
|
||||||
@zones_namespace.marshal_with(zone_model_full, as_list=True)
|
|
||||||
def get(self):
|
|
||||||
return get_all_zones(), 200
|
|
||||||
|
|
||||||
|
|
||||||
class Zones(Resource):
|
|
||||||
@zones_namespace.response(200, "Success")
|
|
||||||
@zones_namespace.response(404, "Zone <zone_id> does not exist")
|
|
||||||
@zones_namespace.marshal_with(zone_model_full)
|
|
||||||
def get(self, zone_id):
|
|
||||||
zone = get_zone_by_id(zone_id)
|
|
||||||
if not zone:
|
|
||||||
zones_namespace.abort(404, f"Zone {zone_id} does not exist")
|
|
||||||
return zone, 200
|
|
||||||
|
|
||||||
@zones_namespace.response(200, "<zone_id> was updated!")
|
|
||||||
@zones_namespace.response(404, "Zone <zone_id> does not exist")
|
|
||||||
@zones_namespace.response(400, "Invalid payload.")
|
|
||||||
@zones_namespace.expect(zone_model_create, validate=True)
|
|
||||||
def put(self, zone_id):
|
|
||||||
post_data = request.get_json()
|
|
||||||
name = post_data.get("name")
|
|
||||||
response_object = {}
|
|
||||||
|
|
||||||
if not name:
|
|
||||||
zones_namespace.abort(400, "Invalid payload.")
|
|
||||||
|
|
||||||
zone = get_zone_by_id(zone_id)
|
|
||||||
if not zone:
|
|
||||||
zones_namespace.abort(404, f"Zone {zone_id} does not exist")
|
|
||||||
|
|
||||||
zone = update_zone(zone, name)
|
|
||||||
|
|
||||||
response_object["message"] = f"{zone.id} was updated!"
|
|
||||||
response_object["id"] = zone.id
|
|
||||||
return response_object, 200
|
|
||||||
|
|
||||||
@zones_namespace.response(200, "Success")
|
|
||||||
@zones_namespace.response(404, "Zone <zone_id> does not exist")
|
|
||||||
def delete(self, zone_id):
|
|
||||||
response_object = {}
|
|
||||||
|
|
||||||
zone = get_zone_by_id(zone_id)
|
|
||||||
if not zone:
|
|
||||||
zones_namespace.abort(404, f"Zone {zone_id} does not exist")
|
|
||||||
|
|
||||||
delete_zone(zone)
|
|
||||||
|
|
||||||
response_object["message"] = "Success"
|
|
||||||
return response_object, 200
|
|
||||||
|
|
||||||
|
|
||||||
zones_namespace.add_resource(ZonesList, "")
|
|
||||||
zones_namespace.add_resource(Zones, "/<int:zone_id>")
|
|
|
@ -1,67 +0,0 @@
|
||||||
import pytest
|
|
||||||
from flask_restx import Namespace
|
|
||||||
from src import create_app, db
|
|
||||||
from src.api.models.users import User
|
|
||||||
from src.config import ProductionConfig
|
|
||||||
|
|
||||||
# from pactman import Consumer, Provider
|
|
||||||
|
|
||||||
|
|
||||||
# from src.tests.client.client import UsersClient
|
|
||||||
|
|
||||||
PACT_DIR = "src/tests/pacts"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def test_app():
|
|
||||||
app = create_app()
|
|
||||||
app.config.from_object("src.config.TestingConfig")
|
|
||||||
with app.app_context():
|
|
||||||
yield app # testing happens here
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def test_namespace():
|
|
||||||
ns = Namespace("testing")
|
|
||||||
yield ns
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def test_database():
|
|
||||||
db.drop_all()
|
|
||||||
db.create_all()
|
|
||||||
yield db # testing happens here
|
|
||||||
db.session.remove()
|
|
||||||
db.drop_all()
|
|
||||||
|
|
||||||
|
|
||||||
# @pytest.fixture(scope="function")
|
|
||||||
# def pact():
|
|
||||||
# pact = Consumer("UsersConsumer").has_pact_with(
|
|
||||||
# Provider("UsersProvider"), pact_dir=PACT_DIR
|
|
||||||
# )
|
|
||||||
# pact.start_service()
|
|
||||||
# yield pact
|
|
||||||
# pact.stop_service()
|
|
||||||
|
|
||||||
|
|
||||||
# @pytest.fixture(scope="function")
|
|
||||||
# def user_client(pact):
|
|
||||||
# cli = UsersClient(uri=pact.uri)
|
|
||||||
# yield cli
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def prod_config():
|
|
||||||
yield ProductionConfig()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def add_user():
|
|
||||||
def _add_user(username, email, password):
|
|
||||||
user = User(username=username, email=email, password=password)
|
|
||||||
db.session.add(user)
|
|
||||||
db.session.commit()
|
|
||||||
return user
|
|
||||||
|
|
||||||
return _add_user
|
|
|
@ -1,192 +0,0 @@
|
||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
TEST_USERNAME = "fede_auth"
|
|
||||||
TEST_EMAIL = "fede_auth@gmail.com"
|
|
||||||
TEST_PASSWD = "password1234"
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_registration(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/auth/register",
|
|
||||||
data=json.dumps(
|
|
||||||
{
|
|
||||||
"username": TEST_USERNAME,
|
|
||||||
"email": TEST_EMAIL,
|
|
||||||
"password": TEST_PASSWD,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 201
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
assert TEST_USERNAME in data["username"]
|
|
||||||
assert TEST_EMAIL in data["email"]
|
|
||||||
assert "password" not in data
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_registration_duplicate_email(test_app, test_database, add_user):
|
|
||||||
add_user(TEST_USERNAME, TEST_EMAIL, TEST_PASSWD)
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/auth/register",
|
|
||||||
data=json.dumps(
|
|
||||||
{"username": "martin", "email": TEST_EMAIL, "password": "test"}
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
assert "Sorry. That email already exists." == data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"payload",
|
|
||||||
[
|
|
||||||
{},
|
|
||||||
{"email": TEST_EMAIL, "password": TEST_PASSWD},
|
|
||||||
{"username": TEST_USERNAME, "password": TEST_PASSWD},
|
|
||||||
{"email": TEST_EMAIL, "username": TEST_USERNAME},
|
|
||||||
{"mail": TEST_EMAIL, "username": TEST_USERNAME, "password": TEST_PASSWD},
|
|
||||||
{"email": TEST_EMAIL, "user": TEST_USERNAME, "password": TEST_PASSWD},
|
|
||||||
{"email": TEST_EMAIL, "username": TEST_USERNAME, "passwd": TEST_PASSWD},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_user_registration_invalid_json(test_app, test_database, payload):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/auth/register",
|
|
||||||
data=json.dumps(payload),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
assert "Input payload validation failed" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_registered_user_login(test_app, test_database, add_user):
|
|
||||||
add_user(TEST_USERNAME, TEST_EMAIL, TEST_PASSWD)
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/auth/login",
|
|
||||||
data=json.dumps({"email": TEST_EMAIL, "password": TEST_PASSWD}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
assert data["access_token"]
|
|
||||||
assert data["refresh_token"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_not_registered_user_login(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/auth/login",
|
|
||||||
data=json.dumps({"email": "invalid@gmail.com", "password": TEST_PASSWD}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 404
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
assert "User does not exist." in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_valid_refresh(test_app, test_database, add_user):
|
|
||||||
add_user(TEST_USERNAME, TEST_EMAIL, TEST_PASSWD)
|
|
||||||
client = test_app.test_client()
|
|
||||||
# user login
|
|
||||||
resp_login = client.post(
|
|
||||||
"/auth/login",
|
|
||||||
data=json.dumps({"email": TEST_EMAIL, "password": TEST_PASSWD}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
# valid refresh
|
|
||||||
refresh_token = json.loads(resp_login.data.decode())["refresh_token"]
|
|
||||||
resp = client.post(
|
|
||||||
"/auth/refresh",
|
|
||||||
data=json.dumps({"refresh_token": refresh_token}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert data["access_token"]
|
|
||||||
assert data["refresh_token"]
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_refresh_expired_token(test_app, test_database, add_user):
|
|
||||||
add_user("test5", "test5@test.com", "test")
|
|
||||||
client = test_app.test_client()
|
|
||||||
# user login
|
|
||||||
resp_login = client.post(
|
|
||||||
"/auth/login",
|
|
||||||
data=json.dumps({"email": "test5@test.com", "password": "test"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
# invalid token refresh
|
|
||||||
time.sleep(10)
|
|
||||||
refresh_token = json.loads(resp_login.data.decode())["refresh_token"]
|
|
||||||
resp = client.post(
|
|
||||||
"/auth/refresh",
|
|
||||||
data=json.dumps({"refresh_token": refresh_token}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 401
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
assert "Signature expired. Please log in again." in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_refresh(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/auth/refresh",
|
|
||||||
data=json.dumps({"refresh_token": "Invalid"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 401
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
assert "Invalid token. Please log in again." in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_status(test_app, test_database, add_user):
|
|
||||||
add_user("test6", "test6@test.com", "test")
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp_login = client.post(
|
|
||||||
"/auth/login",
|
|
||||||
data=json.dumps({"email": "test6@test.com", "password": "test"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
token = json.loads(resp_login.data.decode())["access_token"]
|
|
||||||
resp = client.get(
|
|
||||||
"/auth/status",
|
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
assert "test6" in data["username"]
|
|
||||||
assert "test6@test.com" in data["email"]
|
|
||||||
assert "password" not in data
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_status(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.get(
|
|
||||||
"/auth/status",
|
|
||||||
headers={"Authorization": "Bearer invalid"},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 401
|
|
||||||
assert resp.content_type == "application/json"
|
|
||||||
assert "Invalid token. Please log in again." in data["message"]
|
|
|
@ -1,10 +0,0 @@
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def test_ping(test_app):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.get("/ping")
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert "pong" in data["message"]
|
|
||||||
assert "success" in data["status"]
|
|
|
@ -1,26 +0,0 @@
|
||||||
import pytest
|
|
||||||
from src.api.models.users import User
|
|
||||||
|
|
||||||
TOKEN_TYPES = ["access", "refresh"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_passwords_are_random(test_app, test_database, add_user):
|
|
||||||
user_one = add_user("fede", "fede@gmail.com", "test")
|
|
||||||
user_two = add_user("fedec", "fedec@gmail.com", "test")
|
|
||||||
assert user_one.password != user_two.password
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("token_type", TOKEN_TYPES)
|
|
||||||
def test_encode_token(test_app, test_database, add_user, token_type):
|
|
||||||
user = add_user("fede", "fede@gmail.com", "test")
|
|
||||||
token = User.encode_token(user.id, token_type)
|
|
||||||
assert isinstance(token, str)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("token_type", TOKEN_TYPES)
|
|
||||||
def test_decode_token(test_app, test_database, add_user, token_type):
|
|
||||||
user = add_user("fede", "fede@gmail.com", "test")
|
|
||||||
token = User.encode_token(user.id, token_type)
|
|
||||||
token_user_id = User.decode_token(token)
|
|
||||||
assert isinstance(token_user_id, int)
|
|
||||||
assert user.id == token_user_id
|
|
|
@ -1,219 +0,0 @@
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from src import bcrypt
|
|
||||||
from src.api.cruds.users import get_user_by_id
|
|
||||||
from src.api.models.users import User
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_user(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/users",
|
|
||||||
data=json.dumps(
|
|
||||||
{
|
|
||||||
"username": "fede",
|
|
||||||
"email": "fcastaneda@itba.edu.ar",
|
|
||||||
"password": "1234566789",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 201
|
|
||||||
assert "fcastaneda@itba.edu.ar was added!" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_user_invalid_json(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/users",
|
|
||||||
data=json.dumps({}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert "Input payload validation failed" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_user_invalid_json_keys(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/users",
|
|
||||||
data=json.dumps({"email": "john@begood.io"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert "Input payload validation failed" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_user_duplicate_email(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
client.post(
|
|
||||||
"/users",
|
|
||||||
data=json.dumps({"username": "fede", "email": "fcastaneda@itba.edu.ar"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
resp = client.post(
|
|
||||||
"/users",
|
|
||||||
data=json.dumps(
|
|
||||||
{
|
|
||||||
"username": "federico",
|
|
||||||
"email": "fcastaneda@itba.edu.ar",
|
|
||||||
"password": "passwd1234",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert "Sorry. That email already exists." in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_single_user(test_app, test_database, add_user):
|
|
||||||
user = add_user("fede", "fede@itba.edu", "passwd1234")
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.get(f"/users/{user.id}")
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert "fede" in data["username"]
|
|
||||||
assert "fede@itba.edu" in data["email"]
|
|
||||||
assert "password" not in data # Password must not be returned
|
|
||||||
|
|
||||||
|
|
||||||
def test_single_user_incorrect_id(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.get("/users/999")
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 404
|
|
||||||
assert "User 999 does not exist" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_all_users(test_app, test_database, add_user):
|
|
||||||
test_database.session.query(User).delete()
|
|
||||||
add_user("martin", "martin@itba.edu", "passwd1234")
|
|
||||||
add_user("nacho", "nacho@itba.edu", "passwd1234")
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.get("/users")
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert len(data) == 2
|
|
||||||
assert "martin" in data[0]["username"]
|
|
||||||
assert "martin@itba.edu" in data[0]["email"]
|
|
||||||
assert "nacho" in data[1]["username"]
|
|
||||||
assert "nacho@itba.edu" in data[1]["email"]
|
|
||||||
assert "password" not in data[0]
|
|
||||||
assert "password" not in data[1]
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_user(test_app, test_database, add_user):
|
|
||||||
USER_EMAIL = "remove-me@itba.edu"
|
|
||||||
test_database.session.query(User).delete()
|
|
||||||
user = add_user("user-to-be-removed", USER_EMAIL, "passwd1234")
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp_one = client.get("/users")
|
|
||||||
data = json.loads(resp_one.data.decode())
|
|
||||||
assert resp_one.status_code == 200
|
|
||||||
assert len(data) == 1
|
|
||||||
|
|
||||||
resp_two = client.delete(f"/users/{user.id}")
|
|
||||||
data = json.loads(resp_two.data.decode())
|
|
||||||
assert resp_two.status_code == 200
|
|
||||||
assert f"{USER_EMAIL} was removed!" in data["message"]
|
|
||||||
|
|
||||||
resp_three = client.get("/users")
|
|
||||||
data = json.loads(resp_three.data.decode())
|
|
||||||
assert resp_three.status_code == 200
|
|
||||||
assert len(data) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_user_incorrect_id(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.delete("/users/999")
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 404
|
|
||||||
assert "User 999 does not exist" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_user(test_app, test_database, add_user):
|
|
||||||
user = add_user("user-to-be-updated", "update-me@itba.edu", "passwd1234")
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp_one = client.put(
|
|
||||||
f"/users/{user.id}",
|
|
||||||
data=json.dumps({"username": "me", "email": "me@itba.edu"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp_one.data.decode())
|
|
||||||
assert resp_one.status_code == 200
|
|
||||||
assert f"{user.id} was updated!" in data["message"]
|
|
||||||
|
|
||||||
resp_two = client.get(f"/users/{user.id}")
|
|
||||||
data = json.loads(resp_two.data.decode())
|
|
||||||
assert resp_two.status_code == 200
|
|
||||||
assert "me" in data["username"]
|
|
||||||
assert "me@itba.edu" in data["email"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"user_id, payload, status_code, message",
|
|
||||||
[
|
|
||||||
[1, {}, 400, "Input payload validation failed"],
|
|
||||||
[1, {"email": "me@itba.edu"}, 400, "Input payload validation failed"],
|
|
||||||
[
|
|
||||||
999,
|
|
||||||
{"username": "me", "email": "me@itba.edu"},
|
|
||||||
404,
|
|
||||||
"User 999 does not exist",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_update_user_invalid(
|
|
||||||
test_app, test_database, user_id, payload, status_code, message
|
|
||||||
):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.put(
|
|
||||||
f"/users/{user_id}",
|
|
||||||
data=json.dumps(payload),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == status_code
|
|
||||||
assert message in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_user_duplicate_email(test_app, test_database, add_user):
|
|
||||||
add_user("carlos", "carlos@garca.org", "passwd1234")
|
|
||||||
user = add_user("charly", "charly@garca.org", "passwd1234")
|
|
||||||
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.put(
|
|
||||||
f"/users/{user.id}",
|
|
||||||
data=json.dumps({"username": "charly", "email": "carlos@garca.org"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert "Sorry. That email already exists." in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_user_with_passord(test_app, test_database, add_user):
|
|
||||||
password_one = "greaterthaneight"
|
|
||||||
password_two = "somethingdifferent"
|
|
||||||
|
|
||||||
user = add_user("user-to-be-updated", "update-me@testdriven.io", password_one)
|
|
||||||
assert bcrypt.check_password_hash(user.password, password_one)
|
|
||||||
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.put(
|
|
||||||
f"/users/{user.id}",
|
|
||||||
data=json.dumps(
|
|
||||||
{"username": "me", "email": "foo@testdriven.io", "password": password_two}
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
assert resp.status_code == 200
|
|
||||||
|
|
||||||
user = get_user_by_id(user.id)
|
|
||||||
assert bcrypt.check_password_hash(user.password, password_one)
|
|
||||||
assert not bcrypt.check_password_hash(user.password, password_two)
|
|
|
@ -1,126 +0,0 @@
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from src.api.cruds.zones import add_zone, get_zone_by_id
|
|
||||||
from src.api.zones import NAMESPACE as ZONE_NAMESPACE
|
|
||||||
|
|
||||||
ZONE_NAME = "San Isidro"
|
|
||||||
ZONE_NAME_2 = "Bajo San Isidro"
|
|
||||||
ZONE_INVALID_ID = 99999999
|
|
||||||
|
|
||||||
|
|
||||||
def test_zone_add(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
f"/{ZONE_NAMESPACE}",
|
|
||||||
data=json.dumps({"name": "Belgrano"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert resp.status_code == 201
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert "Belgrano was added!" in data["message"]
|
|
||||||
assert "id" in data
|
|
||||||
assert isinstance(data["id"], int)
|
|
||||||
|
|
||||||
|
|
||||||
INVALID_ADD_PAYLOADS = [{}, {"name": ""}, {"names": "bla bla"}]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("payload", INVALID_ADD_PAYLOADS)
|
|
||||||
def test_zone_add_no_desc(test_app, test_database, payload):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
f"/{ZONE_NAMESPACE}",
|
|
||||||
data=json.dumps(payload),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert resp.status_code == 400
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert "Invalid payload" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_zone_get_all(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
add_zone(ZONE_NAME)
|
|
||||||
resp = client.get(f"/{ZONE_NAMESPACE}")
|
|
||||||
assert resp.status_code == 200
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert isinstance(data, list)
|
|
||||||
assert len(data) > 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_zone_get(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
zone = add_zone(ZONE_NAME)
|
|
||||||
resp = client.get(f"/{ZONE_NAMESPACE}/{zone.id}")
|
|
||||||
assert resp.status_code == 200
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert isinstance(data, dict)
|
|
||||||
assert "id" in data
|
|
||||||
assert data["id"] == zone.id
|
|
||||||
assert "name" in data
|
|
||||||
assert data["name"] == ZONE_NAME
|
|
||||||
|
|
||||||
|
|
||||||
def test_zone_get_not_found(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.get(f"/{ZONE_NAMESPACE}/{ZONE_INVALID_ID}")
|
|
||||||
assert resp.status_code == 404
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert "message" in data
|
|
||||||
assert f"Zone {ZONE_INVALID_ID} does not exist" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_zone_update(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
zone = add_zone(ZONE_NAME)
|
|
||||||
body = {"name": ZONE_NAME_2}
|
|
||||||
resp = client.put(f"/{ZONE_NAMESPACE}/{zone.id}", json=body)
|
|
||||||
assert resp.status_code == 200
|
|
||||||
updated_zone = get_zone_by_id(zone.id)
|
|
||||||
assert updated_zone.name == ZONE_NAME_2
|
|
||||||
|
|
||||||
|
|
||||||
def test_zone_update_not_found(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
body = {"name": ZONE_NAME_2}
|
|
||||||
resp = client.put(f"/{ZONE_NAMESPACE}/{ZONE_INVALID_ID}", json=body)
|
|
||||||
assert resp.status_code == 404
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert "message" in data
|
|
||||||
assert f"Zone {ZONE_INVALID_ID} does not exist" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("payload", INVALID_ADD_PAYLOADS)
|
|
||||||
def test_zone_update_invalid_payload(test_app, test_database, payload):
|
|
||||||
client = test_app.test_client()
|
|
||||||
zone = add_zone(ZONE_NAME)
|
|
||||||
resp = client.put(f"/{ZONE_NAMESPACE}/{zone.id}", json=payload)
|
|
||||||
|
|
||||||
assert resp.status_code == 400
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert "Invalid payload" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_zone_delete(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
zone = add_zone(ZONE_NAME)
|
|
||||||
zone_id = zone.id
|
|
||||||
resp = client.delete(f"/{ZONE_NAMESPACE}/{zone_id}")
|
|
||||||
assert resp.status_code == 200
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert "message" in data
|
|
||||||
assert data["message"] == "Success"
|
|
||||||
zone = get_zone_by_id(zone_id)
|
|
||||||
assert not zone
|
|
||||||
|
|
||||||
|
|
||||||
def test_zone_delete_not_found(test_app, test_database):
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.delete(f"/{ZONE_NAMESPACE}/{ZONE_INVALID_ID}")
|
|
||||||
assert resp.status_code == 404
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert "message" in data
|
|
||||||
assert f"Zone {ZONE_INVALID_ID} does not exist" in data["message"]
|
|
|
@ -1,44 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from src.config import ProductionConfig
|
|
||||||
|
|
||||||
|
|
||||||
def test_development_config(test_app):
|
|
||||||
test_app.config.from_object("src.config.DevelopmentConfig")
|
|
||||||
assert test_app.config["SECRET_KEY"] == "my_precious"
|
|
||||||
assert not test_app.config["TESTING"]
|
|
||||||
assert test_app.config["SQLALCHEMY_DATABASE_URI"] == os.environ.get("DATABASE_URL")
|
|
||||||
assert test_app.config["BCRYPT_LOG_ROUNDS"] == 4
|
|
||||||
assert test_app.config["ACCESS_TOKEN_EXPIRATION"] == 900
|
|
||||||
assert test_app.config["REFRESH_TOKEN_EXPIRATION"] == 2592000
|
|
||||||
|
|
||||||
|
|
||||||
def test_testing_config(test_app):
|
|
||||||
test_app.config.from_object("src.config.TestingConfig")
|
|
||||||
assert test_app.config["SECRET_KEY"] == "my_precious"
|
|
||||||
assert test_app.config["TESTING"]
|
|
||||||
assert test_app.config["SQLALCHEMY_DATABASE_URI"] == os.environ.get(
|
|
||||||
"DATABASE_TEST_URL"
|
|
||||||
)
|
|
||||||
assert test_app.config["BCRYPT_LOG_ROUNDS"] == 4
|
|
||||||
assert test_app.config["ACCESS_TOKEN_EXPIRATION"] == 5
|
|
||||||
assert test_app.config["REFRESH_TOKEN_EXPIRATION"] == 5
|
|
||||||
|
|
||||||
|
|
||||||
def test_production_config(test_app, monkeypatch):
|
|
||||||
monkeypatch.setenv(
|
|
||||||
"DATABASE_URL", "postgresql://postgres:postgres@api-db:5432/api_users"
|
|
||||||
)
|
|
||||||
test_app.config.from_object(ProductionConfig())
|
|
||||||
# assert test_app.config["SECRET_KEY"] == "my_precious"
|
|
||||||
assert not test_app.config["TESTING"]
|
|
||||||
assert test_app.config["SQLALCHEMY_DATABASE_URI"] == os.environ.get("DATABASE_URL")
|
|
||||||
assert test_app.config["BCRYPT_LOG_ROUNDS"] == 13
|
|
||||||
assert test_app.config["ACCESS_TOKEN_EXPIRATION"] == 900
|
|
||||||
assert test_app.config["REFRESH_TOKEN_EXPIRATION"] == 2592000
|
|
||||||
|
|
||||||
|
|
||||||
def test_production_db_url_rewrite(monkeypatch):
|
|
||||||
monkeypatch.setenv("DATABASE_URL", "postgres://server")
|
|
||||||
prod_config = ProductionConfig()
|
|
||||||
assert prod_config.SQLALCHEMY_DATABASE_URI == "postgresql://server"
|
|
|
@ -1,13 +0,0 @@
|
||||||
from src.api.models.generic import (get_model_create_response,
|
|
||||||
get_model_error_response)
|
|
||||||
|
|
||||||
|
|
||||||
def test_model_create_response(test_namespace):
|
|
||||||
model = get_model_create_response(test_namespace)
|
|
||||||
assert model.get("id")
|
|
||||||
assert model.get("message")
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_model_error_response(test_namespace):
|
|
||||||
model = get_model_error_response(test_namespace)
|
|
||||||
assert "message" in model
|
|
|
@ -1,269 +0,0 @@
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import src.api.users
|
|
||||||
|
|
||||||
|
|
||||||
class AttrDict(dict):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(AttrDict, self).__init__(*args, **kwargs)
|
|
||||||
self.__dict__ = self
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_user(test_app, monkeypatch):
|
|
||||||
def mock_get_user_by_email(email):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def mock_add_user(username, email, passwd):
|
|
||||||
d = AttrDict()
|
|
||||||
d.update(
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"username": "me",
|
|
||||||
"email": "me@itba.edu",
|
|
||||||
"password": "123456789",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return d
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_email", mock_get_user_by_email)
|
|
||||||
monkeypatch.setattr(src.api.users, "add_user", mock_add_user)
|
|
||||||
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/users",
|
|
||||||
data=json.dumps(
|
|
||||||
{
|
|
||||||
"username": "michael",
|
|
||||||
"email": "michael@itba.edu",
|
|
||||||
"password": "123456789",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 201
|
|
||||||
assert "michael@itba.edu was added!" in data["message"]
|
|
||||||
assert data["id"] == 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_user_duplicate_email(test_app, monkeypatch):
|
|
||||||
def mock_get_user_by_email(email):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def mock_add_user(username, email, passwd):
|
|
||||||
d = AttrDict()
|
|
||||||
d.update({"id": 1, "username": "me", "email": "me@itba.edu"})
|
|
||||||
return d
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_email", mock_get_user_by_email)
|
|
||||||
monkeypatch.setattr(src.api.users, "add_user", mock_add_user)
|
|
||||||
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.post(
|
|
||||||
"/users",
|
|
||||||
data=json.dumps(
|
|
||||||
{
|
|
||||||
"username": "michael",
|
|
||||||
"email": "michael@itba.edu",
|
|
||||||
"password": "123456789",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert "Sorry. That email already exists." in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_single_user(test_app, monkeypatch):
|
|
||||||
def mock_get_user_by_id(user_id):
|
|
||||||
return {
|
|
||||||
"id": 1,
|
|
||||||
"username": "jeffrey",
|
|
||||||
"email": "jeffrey@itba.edu",
|
|
||||||
"created_date": datetime.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_id", mock_get_user_by_id)
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.get("/users/1")
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert "jeffrey" in data["username"]
|
|
||||||
assert "jeffrey@itba.edu" in data["email"]
|
|
||||||
assert "password" not in data
|
|
||||||
|
|
||||||
|
|
||||||
def test_single_user_incorrect_id(test_app, monkeypatch):
|
|
||||||
def mock_get_user_by_id(user_id):
|
|
||||||
return None
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_id", mock_get_user_by_id)
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.get("/users/999")
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 404
|
|
||||||
assert "User 999 does not exist" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_all_users(test_app, monkeypatch):
|
|
||||||
def mock_get_all_users():
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"username": "michael",
|
|
||||||
"email": "michael@mherman.org",
|
|
||||||
"created_date": datetime.now(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"username": "fletcher",
|
|
||||||
"email": "fletcher@notreal.com",
|
|
||||||
"created_date": datetime.now(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_all_users", mock_get_all_users)
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.get("/users")
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert len(data) == 2
|
|
||||||
assert "michael" in data[0]["username"]
|
|
||||||
assert "michael@mherman.org" in data[0]["email"]
|
|
||||||
assert "fletcher" in data[1]["username"]
|
|
||||||
assert "fletcher@notreal.com" in data[1]["email"]
|
|
||||||
assert "password" not in data[0]
|
|
||||||
assert "password" not in data[1]
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_user(test_app, monkeypatch):
|
|
||||||
def mock_get_user_by_id(user_id):
|
|
||||||
d = AttrDict()
|
|
||||||
d.update(
|
|
||||||
{"id": 1, "username": "user-to-be-removed", "email": "remove-me@itba.edu"}
|
|
||||||
)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def mock_delete_user(user):
|
|
||||||
return True
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_id", mock_get_user_by_id)
|
|
||||||
monkeypatch.setattr(src.api.users, "delete_user", mock_delete_user)
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp_two = client.delete("/users/1")
|
|
||||||
data = json.loads(resp_two.data.decode())
|
|
||||||
assert resp_two.status_code == 200
|
|
||||||
assert "remove-me@itba.edu was removed!" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_user_incorrect_id(test_app, monkeypatch):
|
|
||||||
def mock_get_user_by_id(user_id):
|
|
||||||
return None
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_id", mock_get_user_by_id)
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.delete("/users/999")
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 404
|
|
||||||
assert "User 999 does not exist" in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_user(test_app, monkeypatch):
|
|
||||||
def mock_get_user_by_id(user_id):
|
|
||||||
d = AttrDict()
|
|
||||||
d.update({"id": 1, "username": "me", "email": "me@itba.edu"})
|
|
||||||
return d
|
|
||||||
|
|
||||||
def mock_update_user(user, username, email):
|
|
||||||
d = AttrDict()
|
|
||||||
d.update({"id": 1, "username": "me", "email": "me@itba.edu"})
|
|
||||||
return d
|
|
||||||
|
|
||||||
def mock_get_user_by_email(email):
|
|
||||||
return None
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_id", mock_get_user_by_id)
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_email", mock_get_user_by_email)
|
|
||||||
monkeypatch.setattr(src.api.users, "update_user", mock_update_user)
|
|
||||||
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp_one = client.put(
|
|
||||||
"/users/1",
|
|
||||||
data=json.dumps({"username": "me", "email": "me@itba.edu"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp_one.data.decode())
|
|
||||||
assert resp_one.status_code == 200
|
|
||||||
assert "1 was updated!" in data["message"]
|
|
||||||
resp_two = client.get("/users/1")
|
|
||||||
data = json.loads(resp_two.data.decode())
|
|
||||||
assert resp_two.status_code == 200
|
|
||||||
assert "me" in data["username"]
|
|
||||||
assert "me@itba.edu" in data["email"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"user_id, payload, status_code, message",
|
|
||||||
[
|
|
||||||
[1, {}, 400, "Input payload validation failed"],
|
|
||||||
[1, {"email": "me@itba.edu"}, 400, "Input payload validation failed"],
|
|
||||||
[
|
|
||||||
999,
|
|
||||||
{"username": "me", "email": "me@itba.edu"},
|
|
||||||
404,
|
|
||||||
"User 999 does not exist",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_update_user_invalid(
|
|
||||||
test_app, monkeypatch, user_id, payload, status_code, message
|
|
||||||
):
|
|
||||||
def mock_get_user_by_id(user_id):
|
|
||||||
return None
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_id", mock_get_user_by_id)
|
|
||||||
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.put(
|
|
||||||
f"/users/{user_id}",
|
|
||||||
data=json.dumps(payload),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == status_code
|
|
||||||
assert message in data["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_user_duplicate_email(test_app, monkeypatch):
|
|
||||||
class AttrDict(dict):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(AttrDict, self).__init__(*args, **kwargs)
|
|
||||||
self.__dict__ = self
|
|
||||||
|
|
||||||
def mock_get_user_by_id(user_id):
|
|
||||||
d = AttrDict()
|
|
||||||
d.update({"id": 1, "username": "me", "email": "me@itba.edu"})
|
|
||||||
return d
|
|
||||||
|
|
||||||
def mock_update_user(user, username, email):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def mock_get_user_by_email(email):
|
|
||||||
return True
|
|
||||||
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_id", mock_get_user_by_id)
|
|
||||||
monkeypatch.setattr(src.api.users, "get_user_by_email", mock_get_user_by_email)
|
|
||||||
monkeypatch.setattr(src.api.users, "update_user", mock_update_user)
|
|
||||||
|
|
||||||
client = test_app.test_client()
|
|
||||||
resp = client.put(
|
|
||||||
"/users/1",
|
|
||||||
data=json.dumps({"username": "me", "email": "me@itba.edu"}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
data = json.loads(resp.data.decode())
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert "Sorry. That email already exists." in data["message"]
|
|
|
@ -1 +0,0 @@
|
||||||
node_modules
|
|
|
@ -1,13 +0,0 @@
|
||||||
FROM node:17.9.1 AS app
|
|
||||||
ENV REACT_APP_ENDPOINT "https://api.fids.slc.ar/"
|
|
||||||
WORKDIR /app
|
|
||||||
COPY . .
|
|
||||||
RUN npm install && npm run build
|
|
||||||
|
|
||||||
FROM nginx:alpine
|
|
||||||
WORKDIR /usr/share/nginx/html
|
|
||||||
RUN rm -rf ./*
|
|
||||||
COPY --from=app /app/build .
|
|
||||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
|
||||||
EXPOSE 80
|
|
||||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
|
|
@ -1,10 +0,0 @@
|
||||||
FROM node:17.9.1 AS app
|
|
||||||
ENV REACT_APP_ENDPOINT "http://127.0.0.1:5000/"
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package.json .
|
|
||||||
COPY package-lock.json .
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN chmod +x /app/test.sh
|
|
||||||
ENTRYPOINT ["/app/test.sh"]
|
|
|
@ -1,7 +0,0 @@
|
||||||
Para correr la app basta con ejecutar
|
|
||||||
|
|
||||||
npm run start
|
|
||||||
|
|
||||||
para corerr los test
|
|
||||||
|
|
||||||
npm run test
|
|
|
@ -1,5 +0,0 @@
|
||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
||||||
module.exports = {
|
|
||||||
preset: "ts-jest",
|
|
||||||
testEnvironment: "jsdom",
|
|
||||||
};
|
|
|
@ -1,15 +0,0 @@
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index unresolvable-file-html.html;
|
|
||||||
try_files $uri @index;
|
|
||||||
}
|
|
||||||
|
|
||||||
location @index {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
add_header Cache-Control no-cache;
|
|
||||||
expires 0;
|
|
||||||
try_files /index.html =404;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,55 +0,0 @@
|
||||||
{
|
|
||||||
"name": "sample-client-users",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"antd": "^5.3.3",
|
|
||||||
"axios": "^1.3.4",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-router": "^6.10.0",
|
|
||||||
"react-router-dom": "^6.10.0",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"web-vitals": "^2.1.4"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "jest --coverage --collectCoverageFrom=\"./src/**\"",
|
|
||||||
"test:integration": "jest integration",
|
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"docker:build": "docker build -t client-users .",
|
|
||||||
"docker:run": " docker run --rm -it -p 8080:80 client-users"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
|
||||||
"@testing-library/react": "^13.4.0",
|
|
||||||
"@testing-library/user-event": "^13.5.0",
|
|
||||||
"@types/jest": "^28.1.8",
|
|
||||||
"@types/node": "^16.18.23",
|
|
||||||
"@types/react": "^18.0.32",
|
|
||||||
"@types/react-dom": "^18.0.11",
|
|
||||||
"jest": "^28.0.0",
|
|
||||||
"jest-environment-jsdom": "^28.0.0",
|
|
||||||
"ts-jest": "^28.0.0",
|
|
||||||
"typescript": "^4.9.5"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>Client Users</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
--></body>
|
|
||||||
</html>
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"short_name": "React App",
|
|
||||||
"name": "Create React App Sample",
|
|
||||||
"icons": [],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { Axios, AxiosError } from "axios";
|
|
||||||
import { Credentials, Token, User, Zone } from "./Types";
|
|
||||||
|
|
||||||
const instance = new Axios({
|
|
||||||
baseURL: process.env.REACT_APP_ENDPOINT,
|
|
||||||
headers: {
|
|
||||||
accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
instance.interceptors.response.use(
|
|
||||||
(response) => {
|
|
||||||
return JSON.parse(response.data);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
const err = error as AxiosError;
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
instance.interceptors.request.use((request) => {
|
|
||||||
request.data = JSON.stringify(request.data);
|
|
||||||
return request;
|
|
||||||
});
|
|
||||||
|
|
||||||
//Ping
|
|
||||||
export const ping = () => {
|
|
||||||
return instance.get("ping");
|
|
||||||
};
|
|
||||||
|
|
||||||
//Users
|
|
||||||
export const createUser = (
|
|
||||||
credentials: Credentials
|
|
||||||
): Promise<{ id?: string; message: string }> => {
|
|
||||||
return instance.post("users", credentials);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchUsers = (): Promise<User[]> => {
|
|
||||||
return instance.get("users");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchUserById = (id: number): Promise<User> => {
|
|
||||||
return instance.get("users/" + id);
|
|
||||||
};
|
|
||||||
|
|
||||||
//Auth
|
|
||||||
export const logIn = (
|
|
||||||
credentials: Credentials
|
|
||||||
): Promise<Token & Partial<{ message: string; user_id: number }>> => {
|
|
||||||
return instance.post("auth/login", credentials);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tokenStatus = (
|
|
||||||
token: string
|
|
||||||
): Promise<User & { message?: string }> => {
|
|
||||||
return instance.get("auth/status", {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//Zones
|
|
||||||
export const fetchZones = (): Promise<Zone[]> => {
|
|
||||||
return instance.get("zones");
|
|
||||||
};
|
|
|
@ -1,41 +0,0 @@
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { LogIn } from "./components/LogIn/LogIn";
|
|
||||||
import { useIsConnected } from "./hooks/useIsConnected";
|
|
||||||
import { useAuthenticateUser } from "./hooks/useAuthenticateUser";
|
|
||||||
import { Route, Routes } from "react-router";
|
|
||||||
import { SignUp } from "./components/SignUp/SignUp";
|
|
||||||
import { Home } from "./components/Home/Home";
|
|
||||||
import { Button } from "antd";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const { user, validateToken, logout } = useAuthenticateUser();
|
|
||||||
const connection = useIsConnected();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
validateToken();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<Routes>
|
|
||||||
<Route path="/login" element={!user ? <LogIn /> : <Home />} />
|
|
||||||
<Route path="/signup" element={<SignUp />} />
|
|
||||||
<Route path="/home" element={!user ? <Home /> : <LogIn />} />
|
|
||||||
<Route path="/" element={!user ? <LogIn /> : <Home />} />
|
|
||||||
</Routes>
|
|
||||||
<div className="FloatingStatus">{connection}</div>
|
|
||||||
<div className="LogoutButton">
|
|
||||||
{
|
|
||||||
<Button
|
|
||||||
onClick={() => logout()}
|
|
||||||
disabled={!!!localStorage.getItem("token")}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,22 +0,0 @@
|
||||||
export interface Credentials {
|
|
||||||
password: string;
|
|
||||||
email: string;
|
|
||||||
username?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Token {
|
|
||||||
refresh_token: string;
|
|
||||||
access_token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: number;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
created_date?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Zone {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import "../../matchMedia.mock";
|
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { Button } from "antd";
|
|
||||||
|
|
||||||
describe("Button Component Test", () => {
|
|
||||||
test("Display button label and clicked", async () => {
|
|
||||||
const onClick = jest.fn();
|
|
||||||
|
|
||||||
render(<Button onClick={() => onClick()}>Button</Button>);
|
|
||||||
|
|
||||||
expect(screen.getByText("Button")).toBeVisible();
|
|
||||||
await userEvent.click(screen.getByText("Button"));
|
|
||||||
expect(onClick).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,14 +0,0 @@
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import "../../../matchMedia.mock";
|
|
||||||
|
|
||||||
import { Card } from "./Card";
|
|
||||||
|
|
||||||
describe("Card Component Test", () => {
|
|
||||||
test("Display initial, name and icon", async () => {
|
|
||||||
render(<Card name="Belgrano" />);
|
|
||||||
|
|
||||||
expect(screen.getByText("Belgrano📍")).toBeVisible();
|
|
||||||
expect(screen.getByText("B")).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,16 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Avatar, Button } from "antd";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Card: React.FC<Props> = ({ name }) => {
|
|
||||||
return (
|
|
||||||
<div className="Card">
|
|
||||||
<Avatar size="large">{name.slice(0, 1).toUpperCase()}</Avatar>
|
|
||||||
{name}
|
|
||||||
📍
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
const mockedUsedNavigate = jest.fn();
|
|
||||||
|
|
||||||
jest.mock("react-router-dom", () => ({
|
|
||||||
...jest.requireActual("react-router-dom"),
|
|
||||||
useNavigate: () => mockedUsedNavigate,
|
|
||||||
}));
|
|
||||||
|
|
||||||
import "../../matchMedia.mock";
|
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { Home } from "./Home";
|
|
||||||
|
|
||||||
describe("Home View Test", () => {
|
|
||||||
test("Display initial, name and icon", async () => {
|
|
||||||
render(
|
|
||||||
<Home
|
|
||||||
zones={[
|
|
||||||
{ id: 1, name: "Belgrano" },
|
|
||||||
{ id: 2, name: "San Isidro" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText("Zones")).toBeVisible();
|
|
||||||
expect(screen.getByText("Belgrano📍")).toBeVisible();
|
|
||||||
expect(screen.getByText("San Isidro📍")).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Card } from "./Card/Card";
|
|
||||||
import { useFetchZones } from "../../hooks/useFetchZones";
|
|
||||||
import { Zone } from "../../Types";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
zones?: Zone[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Home: React.FC<Props> = (props) => {
|
|
||||||
const { zones, error } = useFetchZones();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="Box Big">
|
|
||||||
<div className="Items">
|
|
||||||
<h2>Zones</h2>
|
|
||||||
<div className="List">
|
|
||||||
{(props.zones ? props.zones : zones).map((u) => {
|
|
||||||
return <Card key={u.id} name={u.name} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{error ? <div className="Disconnected">{error}</div> : <></>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,45 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Button, Input } from "antd";
|
|
||||||
import { useAuthenticateUser } from "../../hooks/useAuthenticateUser";
|
|
||||||
|
|
||||||
export const LogIn = () => {
|
|
||||||
const { isLoading, error, authenticate } = useAuthenticateUser();
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="Box Small">
|
|
||||||
<div className="Section">
|
|
||||||
<img
|
|
||||||
alt="logo"
|
|
||||||
className="Image"
|
|
||||||
src="https://www.seekpng.com/png/full/353-3537757_logo-itba.png"
|
|
||||||
/>
|
|
||||||
<div className="Section">
|
|
||||||
<Input
|
|
||||||
placeholder="User"
|
|
||||||
onChange={(ev) => setEmail(ev.target.value)}
|
|
||||||
/>
|
|
||||||
<Input.Password
|
|
||||||
placeholder="Password"
|
|
||||||
onChange={(ev) => setPassword(ev.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
onClick={async () =>
|
|
||||||
await authenticate({ email, password })
|
|
||||||
}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
Log in
|
|
||||||
</Button>
|
|
||||||
{error ? (
|
|
||||||
<div className="Disconnected">{error}</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,63 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Button, Input } from "antd";
|
|
||||||
import { useCreateUser } from "../../hooks/useCreateUser";
|
|
||||||
|
|
||||||
export const SignUp = () => {
|
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [repeatPassword, setRepeatPassword] = useState("");
|
|
||||||
|
|
||||||
const { createUser, isLoading, error } = useCreateUser();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="Box Small">
|
|
||||||
<div className="Section">
|
|
||||||
<img
|
|
||||||
alt="logo"
|
|
||||||
className="Image"
|
|
||||||
src="https://www.seekpng.com/png/full/353-3537757_logo-itba.png"
|
|
||||||
/>
|
|
||||||
<div className="Section">
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
placeholder="Email"
|
|
||||||
onChange={(ev) => setEmail(ev.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder="Username"
|
|
||||||
onChange={(ev) => setUsername(ev.target.value)}
|
|
||||||
/>
|
|
||||||
<Input.Password
|
|
||||||
placeholder="Password"
|
|
||||||
onChange={(ev) => setPassword(ev.target.value)}
|
|
||||||
/>
|
|
||||||
<Input.Password
|
|
||||||
placeholder="Repeat password"
|
|
||||||
onChange={(ev) => setRepeatPassword(ev.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
onClick={async () =>
|
|
||||||
await createUser({ email, password, username })
|
|
||||||
}
|
|
||||||
loading={isLoading}
|
|
||||||
disabled={
|
|
||||||
email === "" ||
|
|
||||||
username === "" ||
|
|
||||||
password === "" ||
|
|
||||||
password !== repeatPassword
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Sign up
|
|
||||||
</Button>
|
|
||||||
{error ? (
|
|
||||||
<div className="Disconnected">{error}</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,66 +0,0 @@
|
||||||
const mockedUsedNavigate = jest.fn();
|
|
||||||
|
|
||||||
jest.mock("react-router-dom", () => ({
|
|
||||||
...jest.requireActual("react-router-dom"),
|
|
||||||
useNavigate: () => mockedUsedNavigate,
|
|
||||||
}));
|
|
||||||
|
|
||||||
import "../matchMedia.mock";
|
|
||||||
import { act, renderHook } from "@testing-library/react";
|
|
||||||
import { useAuthenticateUser } from "./useAuthenticateUser";
|
|
||||||
|
|
||||||
describe("UseAuthenticateUser Hook Test", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
localStorage.removeItem("token");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Hook initial state", async () => {
|
|
||||||
const { result } = renderHook(() => useAuthenticateUser());
|
|
||||||
expect(result.current.isLoading).toBeFalsy();
|
|
||||||
expect(result.current.error).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Hook fetch state - Authenticate function - Promise not resolved", async () => {
|
|
||||||
const { result } = renderHook(() => useAuthenticateUser());
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.authenticate({
|
|
||||||
email: "martin@gmail.com",
|
|
||||||
password: "password1234",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.isLoading).toBeTruthy();
|
|
||||||
expect(result.current.error).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Hook fetch state - Authenticate function - Promise success", async () => {
|
|
||||||
const { result } = renderHook(() => useAuthenticateUser());
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.authenticate({
|
|
||||||
email: "martin@gmail.com",
|
|
||||||
password: "password1234",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(localStorage.getItem("token")).not.toBeNull();
|
|
||||||
expect(result.current.isLoading).toBeFalsy();
|
|
||||||
expect(result.current.error).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Hook fetch state - Authenticate function - Promise failed", async () => {
|
|
||||||
const { result } = renderHook(() => useAuthenticateUser());
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.authenticate({
|
|
||||||
email: "notExistingUser",
|
|
||||||
password: "notExistingUser",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(localStorage.getItem("token")).toBe("undefined");
|
|
||||||
expect(result.current.isLoading).toBeFalsy();
|
|
||||||
expect(result.current.error).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,75 +0,0 @@
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Credentials, User } from "../Types";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { fetchUserById, logIn } from "../Api";
|
|
||||||
import { tokenStatus } from "../Api";
|
|
||||||
|
|
||||||
export const useAuthenticateUser = () => {
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [user, setUser] = useState<User | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
navigate("/home");
|
|
||||||
} else {
|
|
||||||
if (window.location.pathname === "/signup") {
|
|
||||||
navigate("/signup");
|
|
||||||
} else {
|
|
||||||
navigate("/login");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const authenticate = async (credentials: Credentials): Promise<void> => {
|
|
||||||
if (!user) {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const tokens = await logIn(credentials);
|
|
||||||
localStorage.setItem("token", tokens.access_token);
|
|
||||||
|
|
||||||
if (tokens.user_id) {
|
|
||||||
const user = await fetchUserById(tokens.user_id);
|
|
||||||
setUser(user);
|
|
||||||
} else {
|
|
||||||
setError(tokens.message!.split(".")[0] + ".");
|
|
||||||
setUser(null);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setError(error as string);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateToken = async () => {
|
|
||||||
try {
|
|
||||||
const existingToken = localStorage.getItem("token");
|
|
||||||
if (existingToken) {
|
|
||||||
const response = await tokenStatus(existingToken);
|
|
||||||
|
|
||||||
const { message } = response;
|
|
||||||
if (message) throw new Error("Invalid token");
|
|
||||||
|
|
||||||
const user = await fetchUserById(response.id);
|
|
||||||
setUser(user);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
localStorage.removeItem("token");
|
|
||||||
setUser(null);
|
|
||||||
navigate("/login");
|
|
||||||
};
|
|
||||||
|
|
||||||
return { user, isLoading, authenticate, validateToken, logout, error };
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Credentials } from "../Types";
|
|
||||||
import { createUser as createUserAPI } from "../Api";
|
|
||||||
import { useAuthenticateUser } from "./useAuthenticateUser";
|
|
||||||
|
|
||||||
export const useCreateUser = () => {
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const { authenticate } = useAuthenticateUser();
|
|
||||||
|
|
||||||
const createUser = async (credentials: Credentials): Promise<void> => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const createResponse = await createUserAPI(credentials);
|
|
||||||
|
|
||||||
if (createResponse.id) {
|
|
||||||
authenticate(credentials);
|
|
||||||
} else {
|
|
||||||
setError(createResponse.message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setError(error as string);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { createUser, isLoading, error };
|
|
||||||
};
|
|
|
@ -1,23 +0,0 @@
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { User, Zone } from "../Types";
|
|
||||||
import { fetchZones } from "../Api";
|
|
||||||
|
|
||||||
export const useFetchZones = () => {
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [zones, setZones] = useState<Zone[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
fetchZones()
|
|
||||||
.then((data) => {
|
|
||||||
setZones(data);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setError(error as string);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { zones, error };
|
|
||||||
};
|
|
|
@ -1,27 +0,0 @@
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { ping } from "../Api";
|
|
||||||
|
|
||||||
export const useIsConnected = () => {
|
|
||||||
const [connected, setConnected] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
ping()
|
|
||||||
.then(() => {
|
|
||||||
setConnected(true);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setConnected(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{connected ? (
|
|
||||||
<p className="Connected">Connected</p>
|
|
||||||
) : (
|
|
||||||
<p className="Disconnected">Disconnected</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,110 +0,0 @@
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
||||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
||||||
"Helvetica Neue", sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #eff2f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Box {
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0px 20px 60px rgba(0, 0, 0, 0.2);
|
|
||||||
padding: 50px;
|
|
||||||
gap: 30px;
|
|
||||||
background-color: white;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Small {
|
|
||||||
width: 250px;
|
|
||||||
height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Big {
|
|
||||||
width: 350px;
|
|
||||||
height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Section {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
padding: 30px 50px;
|
|
||||||
gap: 30px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Image {
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Connected {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Disconnected {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.FloatingStatus {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.LogoutButton {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Card {
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.2);
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
|
||||||
background-color: white;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Items {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.List {
|
|
||||||
width: 100%;
|
|
||||||
height: 500px;
|
|
||||||
gap: 30px;
|
|
||||||
padding: 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import App from "./App";
|
|
||||||
import reportWebVitals from "./reportWebVitals";
|
|
||||||
import { BrowserRouter } from "react-router-dom";
|
|
||||||
import "./index.css";
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
|
||||||
document.getElementById("root") as HTMLElement
|
|
||||||
);
|
|
||||||
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
|
|
@ -1,13 +0,0 @@
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
|
||||||
writable: true,
|
|
||||||
value: jest.fn().mockImplementation(query => ({
|
|
||||||
matches: false,
|
|
||||||
media: query,
|
|
||||||
onchange: null,
|
|
||||||
addListener: jest.fn(), // deprecated
|
|
||||||
removeListener: jest.fn(), // deprecated
|
|
||||||
addEventListener: jest.fn(),
|
|
||||||
removeEventListener: jest.fn(),
|
|
||||||
dispatchEvent: jest.fn(),
|
|
||||||
})),
|
|
||||||
});
|
|
|
@ -1 +0,0 @@
|
||||||
/// <reference types="react-scripts" />
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { ReportHandler } from 'web-vitals';
|
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
|
@ -1,5 +0,0 @@
|
||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
curl -X DELETE api:5000/ping
|
|
||||||
curl -X POST api:5000/ping
|
|
||||||
|
|
||||||
|
|
||||||
# npm test
|
|
||||||
echo "NPM TEST"
|
|
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
|
Loading…
Reference in New Issue