Initial commit: lightweight CLI ticket tracker
A JSON-backed ticket management system with full CRUD operations, status/priority filtering, prefix-based ID resolution, and test suite.
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
154
tests/test_tickets.py
Normal file
154
tests/test_tickets.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Tests for the tickets package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tickets.cli import main
|
||||
from tickets.models import Priority, Status, Ticket
|
||||
from tickets.store import TicketStore
|
||||
|
||||
|
||||
class TestTicketModel:
|
||||
def test_create_minimal_ticket(self) -> None:
|
||||
t = Ticket(title="Fix login bug")
|
||||
assert t.title == "Fix login bug"
|
||||
assert t.description == ""
|
||||
assert t.priority == Priority.MEDIUM
|
||||
assert t.status == Status.TODO
|
||||
assert t.id and len(t.id) == 8
|
||||
|
||||
def test_create_full_ticket(self) -> None:
|
||||
t = Ticket(
|
||||
title="Deploy v2.0",
|
||||
description="Ship the new release",
|
||||
priority=Priority.CRITICAL,
|
||||
status=Status.IN_PROGRESS,
|
||||
assignee="alice",
|
||||
tags=["deploy", "urgent"],
|
||||
)
|
||||
assert t.priority == Priority.CRITICAL
|
||||
assert t.status == Status.IN_PROGRESS
|
||||
assert t.assignee == "alice"
|
||||
assert "urgent" in t.tags
|
||||
|
||||
def test_roundtrip_dict(self) -> None:
|
||||
t = Ticket(title="Test", description="desc", tags=["a", "b"])
|
||||
rt = Ticket.from_dict(t.to_dict())
|
||||
assert rt.title == t.title
|
||||
assert rt.description == t.description
|
||||
assert rt.tags == t.tags
|
||||
assert rt.priority == t.priority
|
||||
|
||||
def test_update_fields(self) -> None:
|
||||
t = Ticket(title="Old")
|
||||
t.update(title="New", status="done", priority="high")
|
||||
assert t.title == "New"
|
||||
assert t.status == Status.DONE
|
||||
assert t.priority == Priority.HIGH
|
||||
assert t.updated_at != t.created_at
|
||||
|
||||
def test_accept_enum_or_string(self) -> None:
|
||||
t = Ticket(title="X", priority=Priority.LOW, status=Status.DONE)
|
||||
assert t.priority == Priority.LOW
|
||||
t.update(priority="critical", status="cancelled")
|
||||
assert t.priority == Priority.CRITICAL
|
||||
assert t.status == Status.CANCELLED
|
||||
|
||||
|
||||
class TestTicketStore:
|
||||
def test_add_and_retrieve(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
fp = Path(d) / "t.json"
|
||||
store = TicketStore(fp)
|
||||
t = Ticket(title="Hello")
|
||||
store.add(t)
|
||||
assert t.id in store
|
||||
assert store[t.id].title == "Hello"
|
||||
|
||||
def test_delete(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
fp = Path(d) / "t.json"
|
||||
store = TicketStore(fp)
|
||||
store.add(Ticket(title="rm me"))
|
||||
tid = next(iter(store))
|
||||
del store[tid]
|
||||
assert tid not in store
|
||||
|
||||
def test_list_filtering(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
fp = Path(d) / "t.json"
|
||||
store = TicketStore(fp)
|
||||
store.add(Ticket("a", status=Status.TODO, priority=Priority.LOW))
|
||||
store.add(Ticket("b", status=Status.DONE, priority=Priority.HIGH))
|
||||
store.add(Ticket("c", status=Status.DONE, priority=Priority.LOW))
|
||||
assert len(store.list_tickets(status="done")) == 2
|
||||
assert len(store.list_tickets(priority="high")) == 1
|
||||
|
||||
def test_persists_to_disk(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
fp = Path(d) / "t.json"
|
||||
store = TicketStore(fp)
|
||||
store.add(Ticket("persist"))
|
||||
assert fp.exists()
|
||||
# Reload
|
||||
store2 = TicketStore(fp)
|
||||
assert len(store2) == 1
|
||||
|
||||
|
||||
class TestCLI:
|
||||
def test_create_and_view(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fp = tmp_path / "t.json"
|
||||
monkeypatch.setattr("tickets.cli.TicketStore", lambda: TicketStore(fp))
|
||||
|
||||
main(["create", "My ticket", "-d", "desc", "-p", "high"])
|
||||
store = TicketStore(fp)
|
||||
assert len(store) == 1
|
||||
tid = next(iter(store))
|
||||
|
||||
main(["view", tid])
|
||||
main(["list", "-s", "todo"])
|
||||
main(["list"])
|
||||
|
||||
def test_update(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fp = tmp_path / "t.json"
|
||||
monkeypatch.setattr("tickets.cli.TicketStore", lambda: TicketStore(fp))
|
||||
|
||||
main(["create", "Old title"])
|
||||
store = TicketStore(fp)
|
||||
tid = next(iter(store))
|
||||
|
||||
main(["update", tid, "-t", "New title", "-s", "done"])
|
||||
store2 = TicketStore(fp)
|
||||
assert store2[tid].title == "New title"
|
||||
assert store2[tid].status == Status.DONE
|
||||
|
||||
def test_delete_with_force(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fp = tmp_path / "t.json"
|
||||
monkeypatch.setattr("tickets.cli.TicketStore", lambda: TicketStore(fp))
|
||||
|
||||
main(["create", "To delete"])
|
||||
store = TicketStore(fp)
|
||||
assert len(store) == 1
|
||||
tid = next(iter(store))
|
||||
|
||||
main(["delete", tid, "--force"])
|
||||
store2 = TicketStore(fp)
|
||||
assert len(store2) == 0
|
||||
|
||||
def test_prefix_match(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fp = tmp_path / "t.json"
|
||||
monkeypatch.setattr("tickets.cli.TicketStore", lambda: TicketStore(fp))
|
||||
|
||||
main(["create", "Ticket 1"])
|
||||
store = TicketStore(fp)
|
||||
tid = next(iter(store))
|
||||
prefix = tid[:4]
|
||||
|
||||
# Should resolve via prefix
|
||||
main(["view", prefix])
|
||||
|
||||
Reference in New Issue
Block a user