diff --git a/README.md b/README.md index 85e1c0f69..ab4054b1d 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ At submission time, no matter where you are, submit the project via Learn. This project is designed to fulfill the features described in detail in each wave. The tests are meant to only guide your development. 1. [Setup](ada-project-docs/setup.md) -1. [Testing](ada-project-docs/testing.md) +1. [Testing](ada-project-docs/testing.md) +code coverage: pytest --cov=app --cov-report html --cov-report term 1. [Wave 1: CRUD for one model](ada-project-docs/wave_01.md) 1. [Wave 2: Using query params](ada-project-docs/wave_02.md) 1. [Wave 3: Creating custom endpoints](ada-project-docs/wave_03.md) diff --git a/app/__init__.py b/app/__init__.py index 3c581ceeb..d2cf87ed4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,10 @@ from flask import Flask from .db import db, migrate from .models import task, goal +from .routes.task_routes import bp as tasks_bp +from .routes.goal_routes import bp as goals_bp +from dotenv import load_dotenv +load_dotenv() import os def create_app(config=None): @@ -10,13 +14,14 @@ def create_app(config=None): app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI') if config: - # Merge `config` into the app's configuration - # to override the app's default settings for testing + app.config.update(config) db.init_app(app) migrate.init_app(app, db) # Register Blueprints here + app.register_blueprint(tasks_bp) + app.register_blueprint(goals_bp) return app diff --git a/app/models/goal.py b/app/models/goal.py index 44282656b..251a393fd 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -1,5 +1,26 @@ -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .task import Task from ..db import db class Goal(db.Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + title: Mapped[str] + tasks: Mapped[list["Task"]] = relationship("Task", back_populates="goal") + + def to_dict(self, include_tasks=False): + goal_dict = { + "id": self.id, + "title": self.title, + } + + if include_tasks: + goal_dict["tasks"] = [task.to_dict(include_goal_id=True) for task in self.tasks] + + return goal_dict + + @classmethod + def from_dict(cls, goal_data): + new_goal = cls(title=goal_data["title"]) + return new_goal \ No newline at end of file diff --git a/app/models/task.py b/app/models/task.py index 5d99666a4..a9cbe1c43 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1,5 +1,37 @@ -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import ForeignKey +from typing import Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .goal import Goal from ..db import db +from datetime import datetime class Task(db.Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + title: Mapped[str] = mapped_column(nullable=False) + description: Mapped[str] + completed_at: Mapped[datetime] = mapped_column(nullable=True) + goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id")) + goal: Mapped[Optional["Goal"]] = relationship("Goal", back_populates="tasks") + + def to_dict(self, include_goal_id=False): + task_dict = { + "id": self.id, + "title": self.title, + "description": self.description, + "is_complete": self.completed_at is not None + } + + if include_goal_id and self.goal_id is not None: + task_dict["goal_id"] = self.goal_id + + return task_dict + + @classmethod + def from_dict(cls, task_data): + new_task = cls(title=task_data["title"], + description=task_data["description"], + completed_at=task_data.get("completed_at"), + goal_id=task_data.get("goal_id")) + + return new_task diff --git a/app/routes/goal_routes.py b/app/routes/goal_routes.py index 3aae38d49..9acc3e520 100644 --- a/app/routes/goal_routes.py +++ b/app/routes/goal_routes.py @@ -1 +1,89 @@ -from flask import Blueprint \ No newline at end of file +from flask import Blueprint, abort, make_response, request, Response +from dotenv import load_dotenv +from app.models.goal import Goal +from app.models.task import Task +from .route_utilities import validate_model, create_model_from_dict +import os +import requests +from ..db import db +load_dotenv() + + +bp = Blueprint("goals_bp", __name__, url_prefix="/goals") + +# create a new goal +@bp.post("") +def create_goal(): + request_body = request.get_json() + + try: + new_goal = create_model_from_dict(Goal, request_body) + except KeyError as e: + response = {"details": "Invalid data"} + abort(make_response(response, 400)) + + return {"goal": new_goal.to_dict()}, 201 + +# get all goals +@bp.get("") +def get_all_goals(): + query = db.select(Goal) + + goals = db.session.scalars(query) + + return [goal.to_dict() for goal in goals] + +# get one goal +@bp.get("/") +def get_one_goal(goal_id): + goal = validate_model(Goal, goal_id) + + return {"goal": goal.to_dict()}, 200 + +# update one goal +@bp.put("/") +def update_goal(goal_id): + goal = validate_model(Goal, goal_id) + + request_body = request.get_json() + + goal.title = request_body["title"] + + db.session.commit() + + return Response(status=204, mimetype="application/json") + +# delete one goal +@bp.delete("/") +def delete_goal(goal_id): + goal = validate_model(Goal, goal_id) + db.session.delete(goal) + db.session.commit() + + return Response(status=204, mimetype="application/json") + +# add list of tasks to a goal +@bp.post("//tasks") +def add_tasks_to_goal(goal_id): + goal = validate_model(Goal, goal_id) + request_body = request.get_json() + + task_ids = request_body.get("task_ids", []) + + for task in goal.tasks: + task.goal_id = None + + for task_id in task_ids: + task = validate_model(Task, task_id) + task.goal_id = goal.id + + db.session.commit() + + return {"id": goal.id, "task_ids": task_ids}, 200 + +# getting tasks of one goal +@bp.get("//tasks") +def get_tasks_of_goal(goal_id): + goal = validate_model(Goal, goal_id) + + return goal.to_dict(include_tasks=True), 200 \ No newline at end of file diff --git a/app/routes/route_utilities.py b/app/routes/route_utilities.py new file mode 100644 index 000000000..993514c90 --- /dev/null +++ b/app/routes/route_utilities.py @@ -0,0 +1,25 @@ +from flask import abort, make_response +from ..db import db + +def validate_model(cls, model_id): + try: + model_id = int(model_id) + except: + response = {"message": f"{cls.__name__} {model_id} is invalid"} + abort(make_response(response, 400)) + + query = db.select(cls).where(cls.id == model_id) + model = db.session.scalar(query) + + if not model: + response = {"message": f"{cls.__name__} {model_id} is not found"} + abort(make_response(response, 404)) + + return model + +def create_model_from_dict(cls, data): + model = cls.from_dict(data) + db.session.add(model) + db.session.commit() + return model + diff --git a/app/routes/task_routes.py b/app/routes/task_routes.py index 3aae38d49..9d2b43d94 100644 --- a/app/routes/task_routes.py +++ b/app/routes/task_routes.py @@ -1 +1,112 @@ -from flask import Blueprint \ No newline at end of file +from flask import Blueprint, abort, make_response, request, Response +from dotenv import load_dotenv +from app.models.task import Task +from .route_utilities import validate_model, create_model_from_dict +from datetime import datetime, timezone +import os +import requests +from ..db import db +load_dotenv() + +bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") + +# create a new task +@bp.post("") +def create_task(): + request_body = request.get_json() + + try: + new_task = create_model_from_dict(Task, request_body) + + except KeyError as error: + response = {"details": "Invalid data"} + abort(make_response(response, 400)) + + return {"task": new_task.to_dict()}, 201 + +# read all tasks +@bp.get("") +def get_all_tasks(): + query = db.select(Task) + + sort = request.args.get("sort") + + if sort == "asc": + query = query.order_by(Task.title.asc()) + elif sort == "desc": + query = query.order_by(Task.title.desc()) + + tasks = db.session.scalars(query) + + tasks_response = [task.to_dict() for task in tasks] + + return tasks_response + +# read one task +@bp.get("/") +def get_one_task(task_id): + task = validate_model(Task, task_id) + + return {"task": task.to_dict(include_goal_id=True)}, 200 + +# update task +@bp.put("/") +def update_one_task(task_id): + task = validate_model(Task, task_id) + request_body = request.get_json() + + task.title = request_body["title"] + task.description = request_body["description"] + + db.session.commit() + + return Response(status=204, mimetype="application/json") + +# delete task +@bp.delete("/") +def delete_one_task(task_id): + task = validate_model(Task, task_id) + + db.session.delete(task) + db.session.commit() + + return Response(status=204, mimetype="application/json") + +@bp.patch("//mark_complete") +def mark_task_complete(task_id): + task = validate_model(Task, task_id) + + task.completed_at = datetime.now(timezone.utc) + + db.session.commit() + + # send slack message + slack_token = os.environ.get("SLACK_BOT_TOKEN") + slack_channel = os.environ.get("SLACK_CHANNEL") + + if slack_token and slack_channel: + slack_message = { + "channel": slack_channel, + "text": f"Someone just completed the task {task.title}" + } + + headers = { + "Authorization": f"Bearer {slack_token}", + "Content-Type": "application/json" + } + + # sends request to slack + slack_response = requests.post("https://slack.com/api/chat.postMessage", json=slack_message, headers=headers) + + return Response(status=204, mimetype="application/json") + +@bp.patch("//mark_incomplete") +def mark_task_incomplete(task_id): + task = validate_model(Task, task_id) + + task.completed_at = None + + db.session.commit() + + return Response(status=204, mimetype="application/json") + diff --git a/migrations/README b/migrations/README new file mode 100644 index 000000000..0e0484415 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 000000000..4c9709271 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/31f79ab68e45_adds_goalsmodel.py b/migrations/versions/31f79ab68e45_adds_goalsmodel.py new file mode 100644 index 000000000..40f3a91ca --- /dev/null +++ b/migrations/versions/31f79ab68e45_adds_goalsmodel.py @@ -0,0 +1,32 @@ +"""adds goalsmodel + +Revision ID: 31f79ab68e45 +Revises: b303441af36e +Create Date: 2025-05-08 12:42:37.977182 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '31f79ab68e45' +down_revision = 'b303441af36e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('goal', schema=None) as batch_op: + batch_op.add_column(sa.Column('title', sa.String(), nullable=False)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('goal', schema=None) as batch_op: + batch_op.drop_column('title') + + # ### end Alembic commands ### diff --git a/migrations/versions/3db3cdec4a89_adds_relationship_between_tasks_and_goal.py b/migrations/versions/3db3cdec4a89_adds_relationship_between_tasks_and_goal.py new file mode 100644 index 000000000..2acbd72d1 --- /dev/null +++ b/migrations/versions/3db3cdec4a89_adds_relationship_between_tasks_and_goal.py @@ -0,0 +1,34 @@ +"""adds relationship between tasks and goal + +Revision ID: 3db3cdec4a89 +Revises: 31f79ab68e45 +Create Date: 2025-05-08 14:38:15.240889 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3db3cdec4a89' +down_revision = '31f79ab68e45' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('task', schema=None) as batch_op: + batch_op.add_column(sa.Column('goal_id', sa.Integer(), nullable=True)) + batch_op.create_foreign_key(None, 'goal', ['goal_id'], ['id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('task', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_column('goal_id') + + # ### end Alembic commands ### diff --git a/migrations/versions/b303441af36e_create_task_model.py b/migrations/versions/b303441af36e_create_task_model.py new file mode 100644 index 000000000..e6136bef2 --- /dev/null +++ b/migrations/versions/b303441af36e_create_task_model.py @@ -0,0 +1,39 @@ +"""create task model + +Revision ID: b303441af36e +Revises: +Create Date: 2025-05-02 00:22:15.246418 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b303441af36e' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('goal', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('task', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=False), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('task') + op.drop_table('goal') + # ### end Alembic commands ### diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index 55475db79..1545e294f 100644 --- a/tests/test_wave_01.py +++ b/tests/test_wave_01.py @@ -3,7 +3,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_no_saved_tasks(client): # Act response = client.get("/tasks") @@ -14,7 +14,7 @@ def test_get_tasks_no_saved_tasks(client): assert response_body == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_one_saved_tasks(client, one_task): # Act response = client.get("/tasks") @@ -33,7 +33,7 @@ def test_get_tasks_one_saved_tasks(client, one_task): ] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_task(client, one_task): # Act response = client.get("/tasks/1") @@ -52,7 +52,7 @@ def test_get_task(client, one_task): } -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_task_not_found(client): # Act response = client.get("/tasks/1") @@ -60,14 +60,9 @@ def test_get_task_not_found(client): # Assert assert response.status_code == 404 + assert response_body == {"message": "Task 1 is not found"} - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - - -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task(client): # Act response = client.post("/tasks", json={ @@ -97,7 +92,7 @@ def test_create_task(client): assert new_task.completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_update_task(client, one_task): # Act response = client.put("/tasks/1", json={ @@ -117,7 +112,7 @@ def test_update_task(client, one_task): -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_update_task_not_found(client): # Act response = client.put("/tasks/1", json={ @@ -128,14 +123,10 @@ def test_update_task_not_found(client): # Assert assert response.status_code == 404 + assert response_body == {"message": "Task 1 is not found"} + - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - - -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_delete_task(client, one_task): # Act response = client.delete("/tasks/1") @@ -146,7 +137,7 @@ def test_delete_task(client, one_task): query = db.select(Task).where(Task.id == 1) assert db.session.scalar(query) == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_delete_task_not_found(client): # Act response = client.delete("/tasks/1") @@ -154,16 +145,12 @@ def test_delete_task_not_found(client): # Assert assert response.status_code == 404 - - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - + assert response_body == {"message": "Task 1 is not found"} + assert db.session.scalars(db.select(Task)).all() == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task_must_contain_title(client): # Act response = client.post("/tasks", json={ @@ -180,7 +167,7 @@ def test_create_task_must_contain_title(client): assert db.session.scalars(db.select(Task)).all() == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task_must_contain_description(client): # Act response = client.post("/tasks", json={ diff --git a/tests/test_wave_02.py b/tests/test_wave_02.py index a087e0909..651e3aebd 100644 --- a/tests/test_wave_02.py +++ b/tests/test_wave_02.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_sorted_asc(client, three_tasks): # Act response = client.get("/tasks?sort=asc") @@ -29,7 +29,7 @@ def test_get_tasks_sorted_asc(client, three_tasks): ] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_sorted_desc(client, three_tasks): # Act response = client.get("/tasks?sort=desc") diff --git a/tests/test_wave_03.py b/tests/test_wave_03.py index d7d441695..3bab34ee6 100644 --- a/tests/test_wave_03.py +++ b/tests/test_wave_03.py @@ -6,7 +6,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_complete_on_incomplete_task(client, one_task): # Arrange """ @@ -34,7 +34,7 @@ def test_mark_complete_on_incomplete_task(client, one_task): assert db.session.scalar(query).completed_at -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_incomplete_on_complete_task(client, completed_task): # Act response = client.patch("/tasks/1/mark_incomplete") @@ -46,7 +46,7 @@ def test_mark_incomplete_on_complete_task(client, completed_task): assert db.session.scalar(query).completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_complete_on_completed_task(client, completed_task): # Arrange """ @@ -74,7 +74,7 @@ def test_mark_complete_on_completed_task(client, completed_task): query = db.select(Task).where(Task.id == 1) assert db.session.scalar(query).completed_at -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_incomplete_on_incomplete_task(client, one_task): # Act response = client.patch("/tasks/1/mark_incomplete") @@ -86,7 +86,7 @@ def test_mark_incomplete_on_incomplete_task(client, one_task): assert db.session.scalar(query).completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_complete_missing_task(client): # Act response = client.patch("/tasks/1/mark_complete") @@ -94,14 +94,11 @@ def test_mark_complete_missing_task(client): # Assert assert response.status_code == 404 + assert {'message': 'Task 1 is not found'} + # OR: assert response_body == {'message': 'Task 1 is not found'} - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_incomplete_missing_task(client): # Act response = client.patch("/tasks/1/mark_incomplete") @@ -109,8 +106,4 @@ def test_mark_incomplete_missing_task(client): # Assert assert response.status_code == 404 - - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** + assert {'message': 'Task 1 is invalid'} \ No newline at end of file diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 222d10cf0..698e64218 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -1,7 +1,9 @@ +from app.models.goal import Goal +from app.db import db import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goals_no_saved_goals(client): # Act response = client.get("/goals") @@ -12,7 +14,7 @@ def test_get_goals_no_saved_goals(client): assert response_body == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goals_one_saved_goal(client, one_goal): # Act response = client.get("/goals") @@ -29,7 +31,7 @@ def test_get_goals_one_saved_goal(client, one_goal): ] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goal(client, one_goal): # Act response = client.get("/goals/1") @@ -46,22 +48,18 @@ def test_get_goal(client, one_goal): } -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_get_goal_not_found(client): pass # Act response = client.get("/goals/1") response_body = response.get_json() - raise Exception("Complete test") - # Assert - # ---- Complete Test ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Test ---- + assert response.status_code == 404 + assert response_body == {"message": "Goal 1 is not found"} -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_goal(client): # Act response = client.post("/goals", json={ @@ -80,34 +78,32 @@ def test_create_goal(client): } -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_update_goal(client, one_goal): - raise Exception("Complete test") - # Act - # ---- Complete Act Here ---- + response = client.put("goals/1", json={ + "title": "This is my updated Task Title" + }) - # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # assertion 3 goes here - # ---- Complete Assertions Here ---- + assert response.status_code == 204 + + query = db.select(Goal).where(Goal.id == 1) + goal = db.session.scalar(query) + + assert goal.title == "This is my updated Task Title" -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_update_goal_not_found(client): - raise Exception("Complete test") - # Act - # ---- Complete Act Here ---- + response = client.put("/goals/1", json={ + "title": "updated task title" + }) + response_body = response.get_json() - # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Assertions Here ---- + assert response.status_code == 404 + assert response_body == {"message": "Goal 1 is not found"} -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_delete_goal(client, one_goal): # Act response = client.delete("/goals/1") @@ -121,28 +117,20 @@ def test_delete_goal(client, one_goal): response_body = response.get_json() assert "message" in response_body - - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** + assert {"message": "Goal 1 is not found"} -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_delete_goal_not_found(client): - raise Exception("Complete test") - - # Act - # ---- Complete Act Here ---- + response = client.delete("/goals/1") + response_body = response.get_json() - # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Assertions Here ---- + assert response.status_code == 404 + assert response_body == {"message": "Goal 1 is not found"} + assert db.session.scalars(db.select(Goal)).all() == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_goal_missing_title(client): # Act response = client.post("/goals", json={}) diff --git a/tests/test_wave_06.py b/tests/test_wave_06.py index 0317f835a..097c7cfd6 100644 --- a/tests/test_wave_06.py +++ b/tests/test_wave_06.py @@ -3,7 +3,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_post_task_ids_to_goal(client, one_goal, three_tasks): # Act response = client.post("/goals/1/tasks", json={ @@ -25,7 +25,7 @@ def test_post_task_ids_to_goal(client, one_goal, three_tasks): assert len(db.session.scalar(query).tasks) == 3 -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_post_task_ids_to_goal_already_with_goals(client, one_task_belongs_to_one_goal, three_tasks): # Act response = client.post("/goals/1/tasks", json={ @@ -45,7 +45,7 @@ def test_post_task_ids_to_goal_already_with_goals(client, one_task_belongs_to_on assert len(db.session.scalar(query).tasks) == 2 -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_for_specific_goal_no_goal(client): # Act response = client.get("/goals/1/tasks") @@ -53,14 +53,11 @@ def test_get_tasks_for_specific_goal_no_goal(client): # Assert assert response.status_code == 404 + assert response_body == {"message": "Goal 1 is not found"} + - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_for_specific_goal_no_tasks(client, one_goal): # Act response = client.get("/goals/1/tasks") @@ -77,7 +74,7 @@ def test_get_tasks_for_specific_goal_no_tasks(client, one_goal): } -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_for_specific_goal(client, one_task_belongs_to_one_goal): # Act response = client.get("/goals/1/tasks") @@ -102,7 +99,7 @@ def test_get_tasks_for_specific_goal(client, one_task_belongs_to_one_goal): } -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_task_includes_goal_id(client, one_task_belongs_to_one_goal): response = client.get("/tasks/1") response_body = response.get_json()