diff --git a/ada-project-docs/wave_04.md b/ada-project-docs/wave_04.md index acfc78989..3a64e226d 100644 --- a/ada-project-docs/wave_04.md +++ b/ada-project-docs/wave_04.md @@ -99,10 +99,11 @@ Visit https://api.slack.com/methods/chat.postMessage to read about the Slack API Answer the following questions. These questions will help you become familiar with the API, and make working with it easier. -- What is the responsibility of this endpoint? -- What is the URL and HTTP method for this endpoint? -- What are the _two_ _required_ arguments for this endpoint? -- How does this endpoint relate to the Slackbot API key (token) we just created? +- What is the responsibility of this endpoint? This endpoint sends a message to a channel +- What is the URL and HTTP method for this endpoint? POST +https://slack.com/api/chat.postMessage +- What are the _two_ _required_ arguments for this endpoint? token and channel +- How does this endpoint relate to the Slackbot API key (token) we just created? This is what we will need to pass into the headers of the request so that we can be authenticated and granted access within scope of the token. In this case, to be able to send a message. Now, visit https://api.slack.com/methods/chat.postMessage/test. @@ -121,9 +122,9 @@ Press the "Test Method" button! Scroll down to see the HTTP response. Answer the following questions: -- Did we get a success message? If so, did we see the message in our actual Slack workspace? -- Did we get an error message? If so, why? -- What is the shape of this JSON? Is it a JSON object or array? What keys and values are there? +- Did we get a success message? If so, did we see the message in our actual Slack workspace? The value of "ok" is true in the response that I received. I can see the message in #test-slack-api. +- Did we get an error message? If so, why? No +- What is the shape of this JSON? Is it a JSON object or array? What keys and values are there? I get a JSON object with four keys: ok, channel, ts, and message. The message and blocks keys have nested JSON objects. ### Verify with Postman diff --git a/app/__init__.py b/app/__init__.py index 3c581ceeb..c97ff2dde 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,8 @@ from flask import Flask from .db import db, migrate from .models import task, goal +from .routes.task_routes import bp as task_bp +from .routes.goal_routes import bp as goal_bp import os def create_app(config=None): @@ -18,5 +20,7 @@ def create_app(config=None): migrate.init_app(app, db) # Register Blueprints here + app.register_blueprint(task_bp) + app.register_blueprint(goal_bp) return app diff --git a/app/models/goal.py b/app/models/goal.py index 44282656b..9cd47197d 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -1,5 +1,22 @@ -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship from ..db import db +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .task import Task class Goal(db.Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + title: Mapped[str] + tasks: Mapped[list["Task"]] = relationship(back_populates="goal") + + def to_dict(self): + return { + "id":self.id, + "title":self.title + } + + @classmethod + def from_dict(cls, goal_data): + return cls( + title=goal_data["title"] + ) \ No newline at end of file diff --git a/app/models/task.py b/app/models/task.py index 5d99666a4..15ae643ed 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 ..db import db +from sqlalchemy import ForeignKey +from datetime import datetime +from typing import TYPE_CHECKING, Optional +if TYPE_CHECKING: + from .goal import Goal class Task(db.Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + title: Mapped[str] + description: Mapped[str] + completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) + goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id")) + goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks") + + def to_dict(self): + task = { + "id":self.id, + "title":self.title, + "description":self.description, + "is_complete": self.is_complete() + } + if self.goal_id: + task["goal_id"] = self.goal_id + return task + + def is_complete(self): + return self.completed_at is not None + + @classmethod + def from_dict(cls, task_data): + return cls( + title=task_data["title"], + description=task_data["description"], + completed_at=task_data.get("completed_at") + ) diff --git a/app/routes/goal_routes.py b/app/routes/goal_routes.py index 3aae38d49..8ba5ffde7 100644 --- a/app/routes/goal_routes.py +++ b/app/routes/goal_routes.py @@ -1 +1,75 @@ -from flask import Blueprint \ No newline at end of file +from flask import Blueprint, request, Response, abort, make_response +import requests +import os +from .route_utilities import validate_model, create_model +from ..models.goal import Goal +from ..models.task import Task +from ..db import db + + +bp=Blueprint("goal_bp", __name__, url_prefix="/goals") + +@bp.get("") +def get_goals(): + query=db.select(Goal) + goals = db.session.scalars(query.order_by(Goal.id)) + goals_response=[] + for goal in goals: + goals_response.append(goal.to_dict()) + return goals_response + +@bp.get("/") +def get_one_goals(id): + goal = validate_model(Goal, id) + return {"goal": goal.to_dict()},200 + +@bp.get("//tasks") +def get_one_goal_tasks(goal_id): + goal = validate_model(Goal,goal_id) + response_body = goal.to_dict() + + if goal.tasks: + tasks = [task.to_dict() for task in goal.tasks] + response_body["tasks"] = tasks + else: + response_body["tasks"] = [] + + return response_body + +@bp.post("") +def create_goal(): + request_body = request.get_json() + response = create_model(Goal, request_body) + return {"goal": response[0]}, response[1] + +@bp.post("//tasks") +def create_tasks_with_goal_id(goal_id): + + goal = validate_model(Goal,goal_id) + request_body = request.get_json() + tasks = request_body.get("task_ids",[]) + + if goal.tasks: + goal.tasks.clear() + + goal.tasks = [validate_model(Task, task) for task in tasks] + + db.session.commit() + + return make_response({"id":goal.id, "task_ids":tasks},200) + +@bp.put("/") +def update_goal(id): + goal = validate_model(Goal, id) + request_body = request.get_json() + goal.title=request_body["title"] + db.session.commit() + return Response(status=204, mimetype="application/json") + +@bp.delete("/") +def delete_goal(id): + goal = validate_model(Goal, id) + db.session.delete(goal) + db.session.commit() + return Response(status=204, mimetype="application/json") + diff --git a/app/routes/route_utilities.py b/app/routes/route_utilities.py new file mode 100644 index 000000000..e8b7ff004 --- /dev/null +++ b/app/routes/route_utilities.py @@ -0,0 +1,43 @@ +from flask import abort, make_response +from ..db import db + +def validate_model(cls, model_id): + try: + model_id = int(model_id) + except ValueError: + invalid_response = {"message": f"{cls.__name__} id ({model_id}) is invalid."} + abort(make_response(invalid_response, 400)) + + query = db.select(cls).where(cls.id == model_id) + model = db.session.scalar(query) + + if not model: + not_found = {"message": f"{cls.__name__} with id ({model_id}) not found."} + abort(make_response(not_found, 404)) + + return model + +def create_model(cls, model_data): + try: + new_model = cls.from_dict(model_data) + except KeyError as e: + response = {"details": "Invalid data"} + abort(make_response(response, 400)) + + db.session.add(new_model) + db.session.commit() + + return new_model.to_dict(), 201 + +def get_models_with_filters(cls, filters=None): + query = db.select(cls) + + if filters: + for attribute, value in filters.items(): + if hasattr(cls, attribute): + query = query.where(getattr(cls, attribute).ilike(f"%{value}%")) + + models = db.session.scalars(query.order_by(cls.id)) + models_response = [model.to_dict() for model in models] + + return models_response, 200 diff --git a/app/routes/task_routes.py b/app/routes/task_routes.py index 3aae38d49..ce4797c30 100644 --- a/app/routes/task_routes.py +++ b/app/routes/task_routes.py @@ -1 +1,98 @@ -from flask import Blueprint \ No newline at end of file +from flask import Blueprint, request, Response, abort, make_response +import requests +import os +from .route_utilities import validate_model, create_model +from datetime import datetime +from ..models.task import Task +from ..db import db + +bp=Blueprint("tasks_bp", __name__, url_prefix="/tasks") + +@bp.post("") +def create_task(): + request_body = request.get_json() + response = create_model(Task, request_body) + return {"task": response[0]}, response[1] + +@bp.get("") +def get_tasks(): + query = db.select(Task) + + sort_param = request.args.get("sort") + + if sort_param == "asc": + tasks = db.session.scalars(query.order_by(Task.title.asc())) + elif sort_param == "desc": + tasks = db.session.scalars(query.order_by(Task.title.desc())) + else: + tasks = db.session.scalars(query.order_by(Task.id)) + + tasks_response = [] + for task in tasks: + valid_task = task.to_dict() + if not has_special_chars(valid_task["title"]): + tasks_response.append(valid_task) + return tasks_response + +def has_special_chars(title): + special_chars = "#$%()*+,-./:;<=>?@[\]^_`{|}~" + for char in title: + if char in special_chars: + raise ValueError + return False + +@bp.get("/") +def get_one_task(id): + task = validate_model(Task, id) + return {"task": task.to_dict()},200 + +@bp.put("/") +def update_task(id): + task = validate_model(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") + +@bp.patch("//mark_complete") +def mark_task_complete(id): + task = validate_model(Task, id) + + data = { + "token": f"{os.environ.get('SLACK_API_TOKEN')}", + "channel":"test-slack-api", + "text":"Someone just completed the task My Beautiful Task" + } + response = requests.post("https://slack.com/api/chat.postMessage", data=data, + headers={ + "Authorization": f"Bearer {os.environ.get('SLACK_API_TOKEN')}" + }) + + if not task.completed_at: + task.completed_at = datetime.now() + + db.session.commit() + return Response(status=204, mimetype="application/json") + +@bp.patch("//mark_incomplete") +def mark_task_incomplete(id): + task = validate_model(Task, id) + + if task.completed_at: + task.completed_at = None + + db.session.commit() + return Response(status=204, mimetype="application/json") + +@bp.delete("/") +def delete_task(id): + task = validate_model(Task, id) + db.session.delete(task) + 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/376f2253ec12_adds_task_model.py b/migrations/versions/376f2253ec12_adds_task_model.py new file mode 100644 index 000000000..c8c389fe7 --- /dev/null +++ b/migrations/versions/376f2253ec12_adds_task_model.py @@ -0,0 +1,39 @@ +"""adds Task model + +Revision ID: 376f2253ec12 +Revises: +Create Date: 2025-05-08 10:25:45.769864 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '376f2253ec12' +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/migrations/versions/e4417e105134_.py b/migrations/versions/e4417e105134_.py new file mode 100644 index 000000000..8b2a88c6f --- /dev/null +++ b/migrations/versions/e4417e105134_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: e4417e105134 +Revises: 376f2253ec12 +Create Date: 2025-05-08 18:56:24.274855 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e4417e105134' +down_revision = '376f2253ec12' +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/f7038f873033_.py b/migrations/versions/f7038f873033_.py new file mode 100644 index 000000000..a7ac15594 --- /dev/null +++ b/migrations/versions/f7038f873033_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: f7038f873033 +Revises: e4417e105134 +Create Date: 2025-05-09 10:06:38.560096 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f7038f873033' +down_revision = 'e4417e105134' +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/tests/conftest.py b/tests/conftest.py index a01499583..bc69bf072 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,7 +47,7 @@ def one_task(app): completed_at=None) db.session.add(new_task) db.session.commit() - + return new_task # This fixture gets called in every test that # references "three_tasks" @@ -67,7 +67,7 @@ def three_tasks(app): completed_at=None) ]) db.session.commit() - + # return array of 3 tasks? # This fixture gets called in every test that # references "completed_task" @@ -104,4 +104,35 @@ def one_task_belongs_to_one_goal(app, one_goal, one_task): task = db.session.scalar(task_query) goal = db.session.scalar(goal_query) goal.tasks.append(task) - db.session.commit() \ No newline at end of file + db.session.commit() + +@pytest.fixture +def no_title_task(app): + db.session.add_all([ + Task(title="Water the garden 🌷", + description="", + completed_at=None), + Task(title="", + description="", + completed_at=None), + Task(title="Pay my outstanding tickets 😭", + description="", + completed_at=None) + ]) + db.session.commit() + +@pytest.fixture +def title_starts_special_char(app): + db.session.add_all([ + Task(title="$ater the garden 🌷", + description="Plants seeds near companions.", + completed_at=None), + Task(title="Walk the dog", + description="Go around the park.", + completed_at=None), + Task(title="Pay my outstanding tickets 😭", + description="Make sure that the deadline hasn't passed.", + completed_at=None) + ]) + db.session.commit() + \ No newline at end of file diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index 55475db79..d46ff9bc7 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") @@ -51,8 +51,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 +59,10 @@ def test_get_task_not_found(client): # Assert assert response.status_code == 404 + assert response_body == {"message": "Task with id (1) 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={ @@ -116,8 +111,7 @@ def test_update_task(client, one_task): assert 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_not_found(client): # Act response = client.put("/tasks/1", json={ @@ -128,14 +122,10 @@ def test_update_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 with id (1) 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_task(client, one_task): # Act response = client.delete("/tasks/1") @@ -146,7 +136,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 +144,11 @@ 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 with id (1) 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 +165,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..86f6aeb96 100644 --- a/tests/test_wave_02.py +++ b/tests/test_wave_02.py @@ -1,7 +1,6 @@ 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 +28,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") @@ -55,3 +54,41 @@ def test_get_tasks_sorted_desc(client, three_tasks): "is_complete": False, "title": "Answer forgotten email 📧"}, ] + +# @pytest.mark.skip(reason="Need to complete") +# Add a fixture that includes task with empty string, replace three_tasks +def test_task_no_title_empty_string(client, no_title_task): + # Act + response = client.get("/tasks?sort=desc") + response_body = response.get_json() + + # Assert + assert response.status_code == 200 + assert len(response_body) == 3 + assert response_body == [ + { + "description": "", + "id": 1, + "is_complete": False, + "title": "Water the garden 🌷"}, + { + "description": "", + "id": 3, + "is_complete": False, + "title": "Pay my outstanding tickets 😭"}, + { + "description": "", + "id": 2, + "is_complete": False, + "title": ""}, + ] + +# @pytest.mark.skip(reason="Need to complete") +# Add a fixture that includes title with special char, replace three_tasks +def test_title_beginning_with_special_char(client, title_starts_special_char): + with pytest.raises(ValueError): + response = client.get("/tasks") + response_body = response.get_json() + +def test_title_exceeds_char_limit(client): + pass diff --git a/tests/test_wave_03.py b/tests/test_wave_03.py index d7d441695..b2b08d17d 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,10 @@ def test_mark_complete_missing_task(client): # Assert assert response.status_code == 404 + assert response_body == {"message": "Task with id (1) 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 +105,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 response_body == {"message": "Task with id (1) not found."} diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 222d10cf0..fbaae8ade 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,19 @@ 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 with id (1) 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 +79,35 @@ 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": "Updated Goal 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) + task = db.session.scalar(query) -@pytest.mark.skip(reason="test to be completed by student") + assert task.title == "Updated Goal Title" + +# @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 Goal 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 with id (1) 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 +121,21 @@ 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 response_body == {"message": "Goal with id (1) 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("/goas/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 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..23cbcca6a 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={ @@ -20,12 +20,11 @@ def test_post_task_ids_to_goal(client, one_goal, three_tasks): "task_ids": [1, 2, 3] } - # Check that Goal was updated in the db query = db.select(Goal).where(Goal.id == 1) 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 +44,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 +52,17 @@ def test_get_tasks_for_specific_goal_no_goal(client): # Assert assert response.status_code == 404 - - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** + ''' + I disagree with this assertion. How can I return a response_body if the goal is not found? I made the assertion that the response body should equal the output message when the model is not found + assert response_body == { + "id": 1, + "task_ids": [2, 4] + } + ''' + assert response_body == {'message': 'Goal with id (1) 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_get_tasks_for_specific_goal_no_tasks(client, one_goal): # Act response = client.get("/goals/1/tasks") @@ -77,7 +79,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") @@ -96,13 +98,13 @@ def test_get_tasks_for_specific_goal(client, one_task_belongs_to_one_goal): "goal_id": 1, "title": "Go on my daily walk 🏞", "description": "Notice something new every day", - "is_complete": False + "is_complete": False } ] } -@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()