Init repo
This commit is contained in:
parent
e94c07b9d6
commit
93c6b44e93
26
.github/workflows/tests.yml
vendored
Normal file
26
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pytest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install -U pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
pip install pytest
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
pytest -q
|
||||||
0
__init__.py
Normal file
0
__init__.py
Normal file
BIN
__pycache__/__init__.cpython-314.pyc
Normal file
BIN
__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
__pycache__/app.cpython-314.pyc
Normal file
BIN
__pycache__/app.cpython-314.pyc
Normal file
Binary file not shown.
BIN
__pycache__/db.cpython-314.pyc
Normal file
BIN
__pycache__/db.cpython-314.pyc
Normal file
Binary file not shown.
163
app.py
Normal file
163
app.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, render_template, redirect, url_for, request, flash
|
||||||
|
|
||||||
|
from db import get_db, init_db
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Constants
|
||||||
|
# =====================================================
|
||||||
|
STATUS_VALUES = ("not-started", "in-progress", "done")
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Flask App Factory
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
def create_app(test_config: dict | None = None) -> Flask:
|
||||||
|
"""
|
||||||
|
Skapar och konfigurerar Flask-applikationen.
|
||||||
|
Används både för runtime och tester.
|
||||||
|
"""
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-secret")
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# App Configuration
|
||||||
|
# -------------------------------------------------
|
||||||
|
default_db = os.path.join(os.path.dirname(__file__), "todo.db")
|
||||||
|
app.config.update(
|
||||||
|
DB_PATH=default_db,
|
||||||
|
TESTING=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if test_config:
|
||||||
|
app.config.update(test_config)
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Initiera databasen
|
||||||
|
# -------------------------------------------------
|
||||||
|
init_db(app)
|
||||||
|
|
||||||
|
# =================================================
|
||||||
|
# Routes
|
||||||
|
# =================================================
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
"""
|
||||||
|
Root → redirect till dashboard
|
||||||
|
"""
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
@app.route("/dashboard", methods=["GET"])
|
||||||
|
def dashboard():
|
||||||
|
"""
|
||||||
|
Visar alla todos
|
||||||
|
"""
|
||||||
|
with get_db(app) as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM todo
|
||||||
|
ORDER BY
|
||||||
|
CASE status
|
||||||
|
WHEN 'not-started' THEN 1
|
||||||
|
WHEN 'in-progress' THEN 2
|
||||||
|
ELSE 3
|
||||||
|
END,
|
||||||
|
id DESC
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"index.html",
|
||||||
|
todos=rows,
|
||||||
|
status_values=STATUS_VALUES,
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/todo/create", methods=["POST"])
|
||||||
|
def create_todo():
|
||||||
|
"""
|
||||||
|
Skapar en ny todo
|
||||||
|
"""
|
||||||
|
title = (request.form.get("title") or "").strip()
|
||||||
|
description = (request.form.get("description") or "").strip()
|
||||||
|
status = request.form.get("status") or "not-started"
|
||||||
|
|
||||||
|
if not title or not description:
|
||||||
|
flash("Title och description krävs.", "danger")
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
if status not in STATUS_VALUES:
|
||||||
|
status = "not-started"
|
||||||
|
|
||||||
|
now = datetime.now().isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
with get_db(app) as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO todo (title, description, status, created)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(title, description, status, now),
|
||||||
|
)
|
||||||
|
|
||||||
|
flash("Todo skapad!", "success")
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
@app.route("/todo/<int:todo_id>/status", methods=["POST"])
|
||||||
|
def update_status(todo_id: int):
|
||||||
|
"""
|
||||||
|
Uppdaterar status på en todo
|
||||||
|
"""
|
||||||
|
status = request.form.get("status")
|
||||||
|
|
||||||
|
if status not in STATUS_VALUES:
|
||||||
|
flash("Ogiltig status.", "danger")
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
now = datetime.now().isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
with get_db(app) as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE todo
|
||||||
|
SET status = ?, edited = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(status, now, todo_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
flash(
|
||||||
|
"Status uppdaterad." if cur.rowcount else "Todo hittades inte.",
|
||||||
|
"success" if cur.rowcount else "danger",
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
@app.route("/todo/<int:todo_id>/delete", methods=["POST"])
|
||||||
|
def delete_todo(todo_id: int):
|
||||||
|
"""
|
||||||
|
Tar bort en todo
|
||||||
|
"""
|
||||||
|
with get_db(app) as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"DELETE FROM todo WHERE id = ?",
|
||||||
|
(todo_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
flash(
|
||||||
|
"Todo borttagen." if cur.rowcount else "Todo hittades inte.",
|
||||||
|
"success" if cur.rowcount else "danger",
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Main
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = create_app()
|
||||||
|
app.run(host="127.0.0.1", port=5001, debug=True)
|
||||||
34
db.py
Normal file
34
db.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import sqlite3
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Database Helpers
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
def get_db(app: Flask):
|
||||||
|
"""
|
||||||
|
Returnerar en SQLite-connection baserat på appens DB_PATH
|
||||||
|
"""
|
||||||
|
conn = sqlite3.connect(app.config["DB_PATH"])
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def init_db(app: Flask):
|
||||||
|
"""
|
||||||
|
Skapar databasen och todo-tabellen om den inte redan finns
|
||||||
|
"""
|
||||||
|
with get_db(app) as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS todo (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'not-started'
|
||||||
|
CHECK (status IN ('not-started','in-progress','done')),
|
||||||
|
created TEXT NOT NULL,
|
||||||
|
edited TEXT
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
3
pytest.ini
Normal file
3
pytest.ini
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[pytest]
|
||||||
|
addopts = -q
|
||||||
|
pythonpath = .
|
||||||
2
requirements-dev.txt
Normal file
2
requirements-dev.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pytest>=8.0
|
||||||
|
pytest-cov>=5.0
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Flask>=3.0
|
||||||
11
static/style.css
Normal file
11
static/style.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
body {
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .nav-link.active {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
63
templates/base.html
Normal file
63
templates/base.html
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{% block title %}TODO{% endblock %}</title>
|
||||||
|
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" />
|
||||||
|
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<meta name="background-color" content="#ffffff" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-light text-dark">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom">
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarNav"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}" href="{{ url_for('dashboard') }}">
|
||||||
|
<i class="bi bi-house-door me-1"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-xl py-4" style="max-width: 1100px">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mb-3">
|
||||||
|
{% for category, msg in messages %}
|
||||||
|
<div class="alert alert-{{ category }} mb-2" role="alert">{{ msg }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
96
templates/index.html
Normal file
96
templates/index.html
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Dashboard - TODO{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h6 mb-3">Ny todo</h2>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('create_todo') }}">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Title</label>
|
||||||
|
<input class="form-control" name="title" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea class="form-control" name="description" rows="3" required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Status</label>
|
||||||
|
<select class="form-select" name="status">
|
||||||
|
<option value="not-started">Not started</option>
|
||||||
|
<option value="in-progress">In progress</option>
|
||||||
|
<option value="done">Done</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-100" type="submit">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i> Skapa
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h6 mb-3">Todos</h2>
|
||||||
|
|
||||||
|
{% if not todos %}
|
||||||
|
<div class="text-muted">Inga todos än.</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th class="d-none d-md-table-cell">Created</th>
|
||||||
|
<th style="width: 1%"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for t in todos %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-semibold">{{ t.title }}</div>
|
||||||
|
<div class="text-muted small">{{ t.description }}</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="{{ url_for('update_status', todo_id=t.id) }}" class="d-flex gap-2">
|
||||||
|
<select class="form-select form-select-sm" name="status" onchange="this.form.submit()">
|
||||||
|
<option value="not-started" {% if t.status=='not-started' %}selected{% endif %}>Not started</option>
|
||||||
|
<option value="in-progress" {% if t.status=='in-progress' %}selected{% endif %}>In progress</option>
|
||||||
|
<option value="done" {% if t.status=='done' %}selected{% endif %}>Done</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="d-none d-md-table-cell text-muted small">
|
||||||
|
{{ t.created }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="{{ url_for('delete_todo', todo_id=t.id) }}">
|
||||||
|
<button class="btn btn-outline-danger btn-sm" type="submit" title="Delete">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
BIN
tests/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
tests/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_app.cpython-314-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_app.cpython-314-pytest-9.0.2.pyc
Normal file
Binary file not shown.
171
tests/test_app.py
Normal file
171
tests/test_app.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from app import create_app
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Helpers
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
def fetch_one(db_path: str, sql: str, params=()):
|
||||||
|
con = sqlite3.connect(db_path)
|
||||||
|
try:
|
||||||
|
return con.execute(sql, params).fetchone()
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_all(db_path: str, sql: str, params=()):
|
||||||
|
con = sqlite3.connect(db_path)
|
||||||
|
try:
|
||||||
|
return con.execute(sql, params).fetchall()
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Tests
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
def test_dashboard_loads(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
resp = client.get("/dashboard")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_todo_inserts_row(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
"/todo/create",
|
||||||
|
data={"title": "Test", "description": "Desc", "status": "not-started"},
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
row = fetch_one(str(db_path), "SELECT title, description, status FROM todo")
|
||||||
|
assert row == ("Test", "Desc", "not-started")
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_requires_fields(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
"/todo/create",
|
||||||
|
data={"title": "", "description": "", "status": "not-started"},
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
row = fetch_one(str(db_path), "SELECT COUNT(*) FROM todo")
|
||||||
|
assert row[0] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_status_is_rejected_on_update(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
# skapa en todo
|
||||||
|
client.post("/todo/create", data={"title": "A", "description": "B", "status": "not-started"})
|
||||||
|
|
||||||
|
todo_id = fetch_one(str(db_path), "SELECT id FROM todo")[0]
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/todo/{todo_id}/status",
|
||||||
|
data={"status": "hax-status"},
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# status ska vara kvar som innan
|
||||||
|
status = fetch_one(str(db_path), "SELECT status FROM todo WHERE id=?", (todo_id,))[0]
|
||||||
|
assert status == "not-started"
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_status_to_done(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
client.post("/todo/create", data={"title": "A", "description": "B", "status": "not-started"})
|
||||||
|
todo_id = fetch_one(str(db_path), "SELECT id FROM todo")[0]
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/todo/{todo_id}/status",
|
||||||
|
data={"status": "done"},
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
status, edited = fetch_one(str(db_path), "SELECT status, edited FROM todo WHERE id=?", (todo_id,))
|
||||||
|
assert status == "done"
|
||||||
|
assert edited is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_todo(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
client.post("/todo/create", data={"title": "A", "description": "B", "status": "not-started"})
|
||||||
|
todo_id = fetch_one(str(db_path), "SELECT id FROM todo")[0]
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
f"/todo/{todo_id}/delete",
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
row = fetch_one(str(db_path), "SELECT COUNT(*) FROM todo")
|
||||||
|
assert row[0] == 0
|
||||||
|
|
||||||
|
def test_dashboard_lists_created_todo(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
client.post("/todo/create", data={"title": "X", "description": "Y", "status": "not-started"})
|
||||||
|
resp = client.get("/dashboard")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert b"X" in resp.data
|
||||||
|
assert b"Y" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_invalid_status_defaults_to_not_started(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
client.post("/todo/create", data={"title": "T", "description": "D", "status": "weird"})
|
||||||
|
row = fetch_one(str(db_path), "SELECT status FROM todo")
|
||||||
|
assert row[0] == "not-started"
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_nonexistent_does_not_crash(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
resp = client.post("/todo/999/delete", follow_redirects=True)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_nonexistent_does_not_crash(tmp_path):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
resp = client.post("/todo/999/status", data={"status": "done"}, follow_redirects=True)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user