Skip to content

Add test/fix json error #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
Binary file added flaskr.db
Binary file not shown.
27 changes: 27 additions & 0 deletions project/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
from pathlib import Path

basedir = Path(__file__).resolve().parent.parent

app = Flask(__name__)

# Basic Config
app.config['SECRET_KEY'] = 'change_me'
app.config['USERNAME'] = 'admin'
app.config['PASSWORD'] = 'admin'

# Database Config
DATABASE = "flaskr.db"
url = os.getenv("DATABASE_URL", f"sqlite:///{basedir / DATABASE}")
if url.startswith("postgres://"):
url = url.replace("postgres://", "postgresql://", 1)
app.config['SQLALCHEMY_DATABASE_URI'] = url
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Initialize DB
db = SQLAlchemy(app)

# Register models
from project import models
124 changes: 72 additions & 52 deletions project/app.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,9 @@
import os
from functools import wraps
from pathlib import Path

from flask import (
Flask,
render_template,
request,
session,
flash,
redirect,
url_for,
abort,
jsonify,
render_template, request, session, flash,
redirect, url_for, abort, jsonify
)
from flask_sqlalchemy import SQLAlchemy


basedir = Path(__file__).resolve().parent

# configuration
DATABASE = "flaskr.db"
USERNAME = "admin"
PASSWORD = "admin"
SECRET_KEY = "change_me"
url = os.getenv("DATABASE_URL", f"sqlite:///{Path(basedir).joinpath(DATABASE)}")

if url.startswith("postgres://"):
url = url.replace("postgres://", "postgresql://", 1)

SQLALCHEMY_DATABASE_URI = url
SQLALCHEMY_TRACK_MODIFICATIONS = False


# create and initialize a new Flask app
app = Flask(__name__)
# load the config
app.config.from_object(__name__)
# init sqlalchemy
db = SQLAlchemy(app)

from functools import wraps
from project import app, db
from project import models


Expand All @@ -49,20 +14,17 @@ def decorated_function(*args, **kwargs):
flash("Please log in.")
return jsonify({"status": 0, "message": "Please log in."}), 401
return f(*args, **kwargs)

return decorated_function


@app.route("/")
def index():
"""Searches the database for entries, then displays them."""
entries = db.session.query(models.Post)
entries = db.session.query(models.Post).all()
return render_template("index.html", entries=entries)


@app.route("/add", methods=["POST"])
def add_entry():
"""Adds new post to the database."""
if not session.get("logged_in"):
abort(401)
new_entry = models.Post(request.form["title"], request.form["text"])
Expand All @@ -74,12 +36,11 @@ def add_entry():

@app.route("/login", methods=["GET", "POST"])
def login():
"""User login/authentication/session management."""
error = None
if request.method == "POST":
if request.form["username"] != app.config["USERNAME"]:
if request.form["username"] != "admin": # or get from config
error = "Invalid username"
elif request.form["password"] != app.config["PASSWORD"]:
elif request.form["password"] != "admin": # or get from config
error = "Invalid password"
else:
session["logged_in"] = True
Expand All @@ -90,7 +51,6 @@ def login():

@app.route("/logout")
def logout():
"""User logout/authentication/session management."""
session.pop("logged_in", None)
flash("You were logged out")
return redirect(url_for("index"))
Expand All @@ -99,11 +59,9 @@ def logout():
@app.route("/delete/<int:post_id>", methods=["GET"])
@login_required
def delete_entry(post_id):
"""Deletes post from database."""
result = {"status": 0, "message": "Error"}
try:
new_id = post_id
db.session.query(models.Post).filter_by(id=new_id).delete()
db.session.query(models.Post).filter_by(id=post_id).delete()
db.session.commit()
result = {"status": 1, "message": "Post Deleted"}
flash("The entry was deleted.")
Expand All @@ -117,9 +75,71 @@ def search():
query = request.args.get("query")
entries = db.session.query(models.Post)
if query:
return render_template("search.html", entries=entries, query=query)
entries = entries.filter(models.Post.title.contains(query))
return render_template("search.html", entries=entries.all(), query=query)
return render_template("search.html")


# === REST API for Notes ===

@app.route("/api/notes", methods=["GET"])
def get_notes():
notes = db.session.query(models.Note).all()
return jsonify([note.to_dict() for note in notes]), 200


@app.route("/api/notes/<int:note_id>", methods=["GET"])
def get_note(note_id):
note = db.session.get(models.Note, note_id)
if note:
return jsonify(note.to_dict()), 200
return jsonify({"error": "Note not found"}), 404


@app.route("/api/notes", methods=["POST"])
def create_note():
try:
data = request.get_json(force=True)
except Exception:
return jsonify({"error": "Invalid JSON"}), 400

if not data or "content" not in data:
return jsonify({"error": "Content is required"}), 400

note = models.Note(content=data["content"])
db.session.add(note)
db.session.commit()
return jsonify(note.to_dict()), 201


@app.route("/api/notes/<int:note_id>", methods=["PUT"])
def update_note(note_id):
note = db.session.get(models.Note, note_id)
if not note:
return jsonify({"error": "Note not found"}), 404

try:
data = request.get_json(force=True)
except Exception:
return jsonify({"error": "Invalid JSON"}), 400

if not data or "content" not in data:
return jsonify({"error": "Content is required"}), 400

note.content = data["content"]
db.session.commit()
return jsonify(note.to_dict()), 200


@app.route("/api/notes/<int:note_id>", methods=["DELETE"])
def delete_note(note_id):
note = db.session.get(models.Note, note_id)
if not note:
return jsonify({"error": "Note not found"}), 404
db.session.delete(note)
db.session.commit()
return jsonify({"message": "Note deleted"}), 200


if __name__ == "__main__":
app.run()
app.run(debug=True)
5 changes: 5 additions & 0 deletions project/create_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from app import app, db

with app.app_context():
db.create_all()
print("Tables created successfully.")
Binary file modified project/flaskr.db
Binary file not shown.
6 changes: 6 additions & 0 deletions project/init_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from project import create_app, db

app = create_app()

with app.app_context():
db.create_all()
15 changes: 12 additions & 3 deletions project/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from project.app import db

from project import db

class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
Expand All @@ -11,4 +10,14 @@ def __init__(self, title, text):
self.text = text

def __repr__(self):
return f"<title {self.title}>"
return f"<Post {self.title}>"

class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String, nullable=False)

def __init__(self, content):
self.content = content

def to_dict(self):
return {"id": self.id, "content": self.content}
59 changes: 59 additions & 0 deletions project/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import pytest
import json
from project import app, db, models

@pytest.fixture
def client():
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
with app.test_client() as client:
with app.app_context():
db.create_all()
yield client
with app.app_context():
db.drop_all()

def test_create_note_success(client):
response = client.post("/api/notes", json={"content": "Test note"})
assert response.status_code == 201
data = response.get_json()
assert data["content"] == "Test note"
assert "id" in data

def test_create_note_no_content(client):
response = client.post("/api/notes", json={})
assert response.status_code == 400

def test_get_notes_empty(client):
response = client.get("/api/notes")
assert response.status_code == 200
data = response.get_json()
assert data == []

def test_get_note_not_found(client):
response = client.get("/api/notes/999")
assert response.status_code == 404

def test_update_note_success(client):
# Create a note first
client.post("/api/notes", json={"content": "Old content"})
response = client.put("/api/notes/1", json={"content": "New content"})
assert response.status_code == 200
data = response.get_json()
assert data["content"] == "New content"

def test_update_note_not_found(client):
response = client.put("/api/notes/999", json={"content": "Test"})
assert response.status_code == 404

def test_delete_note_success(client):
# Create a note first
client.post("/api/notes", json={"content": "To delete"})
response = client.delete("/api/notes/1")
assert response.status_code == 200
data = response.get_json()
assert "deleted" in data["message"].lower()

def test_delete_note_not_found(client):
response = client.delete("/api/notes/999")
assert response.status_code == 404
70 changes: 70 additions & 0 deletions project/test_app_api_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import json
from project import app, db, models


def setup_module(module):
"""Set up test client and clean DB."""
app.config["TESTING"] = True
app.config["WTF_CSRF_ENABLED"] = False
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
with app.app_context():
db.create_all()
module.client = app.test_client()


def teardown_module(module):
with app.app_context():
db.drop_all()


def test_search_without_query():
"""Covers: search view without query (uncovered path)."""
response = client.get("/search/")
assert response.status_code == 200
assert b"Search" in response.data # assuming template title


def test_login_invalid_username():
"""Covers: login route with invalid username."""
response = client.post("/login", data={
"username": "wrong",
"password": "admin"
}, follow_redirects=True)
assert b"Invalid username" in response.data


def test_login_invalid_password():
"""Covers: login route with invalid password."""
response = client.post("/login", data={
"username": "admin",
"password": "wrong"
}, follow_redirects=True)
assert b"Invalid password" in response.data


def test_create_note_missing_json():
"""Covers: missing JSON in create_note."""
response = client.post("/api/notes", data="not-json", content_type="application/json")
assert response.status_code == 400
assert response.json["error"] == "Invalid JSON"


def test_update_note_missing_json():
"""Covers: missing JSON in update_note."""
# Add a note first
with app.app_context():
note = models.Note(content="sample")
db.session.add(note)
db.session.commit()
note_id = note.id

response = client.put(f"/api/notes/{note_id}", data="not-json", content_type="application/json")
assert response.status_code == 400
assert response.json["error"] == "Invalid JSON"


def test_delete_note_not_found():
"""Covers: trying to delete a nonexistent note."""
response = client.delete("/api/notes/9999")
assert response.status_code == 404
assert response.json["error"] == "Note not found"