diff --git a/003-team-sprint-board/app.py b/003-team-sprint-board/app.py new file mode 100644 index 0000000..b4f56ac --- /dev/null +++ b/003-team-sprint-board/app.py @@ -0,0 +1,108 @@ +from flask import Flask, request, jsonify, abort +import uuid + +app = Flask(__name__) + +# In-memory storage +# board = { "Backlog": [], "In Progress": [], "Review": [], "Done": [] } +# cards = { card_id: { ...card_data... } } +board_columns = ["Backlog", "In Progress", "Review", "Done"] +cards = {} + +VALID_STORY_POINTS = {None, 1, 2, 3, 5, 8, 13} + +def get_user(): + user = request.headers.get('X-User-Name') + if not user: + abort(400, description="Display name is required in X-User-Name header") + return user + +@app.route('/board', methods=['GET']) +def get_board(): + get_user() + result = {col: [] for col in board_columns} + for card_id, card in cards.items(): + col = card['status'] + if col in result: + card_copy = card.copy() + card_copy['id'] = card_id + result[col].append(card_copy) + return jsonify(result) + +@app.route('/cards', methods=['POST']) +def create_card(): + get_user() + data = request.json or {} + column = data.get('column') + title = data.get('title') + description = data.get('description', '') + story_points = data.get('story_points') + + if column not in board_columns: + abort(400, description="Invalid column") + if not title: + abort(400, description="Title is required") + + # Validate story points + if story_points is not None and story_points not in VALID_STORY_POINTS: + abort(400, description="Invalid story points. Allowed: 1, 2, 3, 5, 8, 13 or blank") + + card_id = str(uuid.uuid4()) + cards[card_id] = { + 'title': title, + 'description': description, + 'story_points': story_points, + 'status': column + } + return jsonify({'id': card_id}), 201 + +@app.route('/cards/', methods=['PUT']) +def update_card(card_id): + get_user() + if card_id not in cards: + abort(404, description="Card not found") + + data = request.json or {} + card = cards[card_id] + + title = data.get('title', card['title']) + if not title: + abort(400, description="Title is required") + + description = data.get('description', card['description']) + story_points = data.get('story_points', card['story_points']) + status = data.get('status', card['status']) + + if status not in board_columns: + abort(400, description="Invalid status") + + if story_points is not None and story_points not in VALID_STORY_POINTS: + abort(400, description="Invalid story points. Allowed: 1, 2, 3, 5, 8, 13 or blank") + + card.update({ + 'title': title, + 'description': description, + 'story_points': story_points, + 'status': status + }) + + return jsonify(card) + +@app.route('/cards/', methods=['DELETE']) +def delete_card(card_id): + get_user() + if card_id not in cards: + abort(404, description="Card not found") + del cards[card_id] + return '', 204 + +@app.route('/board/done', methods=['DELETE']) +def clear_done(): + get_user() + to_delete = [card_id for card_id, card in cards.items() if card['status'] == 'Done'] + for card_id in to_delete: + del cards[card_id] + return '', 204 + +if __name__ == '__main__': + app.run(debug=True, port=5000) diff --git a/003-team-sprint-board/requirements.txt b/003-team-sprint-board/requirements.txt new file mode 100644 index 0000000..95fef4e --- /dev/null +++ b/003-team-sprint-board/requirements.txt @@ -0,0 +1 @@ +Flask==3.0.3 diff --git a/003-team-sprint-board/run_tests.sh b/003-team-sprint-board/run_tests.sh new file mode 100755 index 0000000..1cc9a25 --- /dev/null +++ b/003-team-sprint-board/run_tests.sh @@ -0,0 +1,8 @@ +pip install -r requirements.txt +python3 app.py > app.log 2>&1 & +APP_PID=$! +sleep 2 +python3 test_api.py +RESULT=$? +kill $APP_PID +exit $RESULT diff --git a/003-team-sprint-board/test_api.py b/003-team-sprint-board/test_api.py new file mode 100644 index 0000000..fe6c45a --- /dev/null +++ b/003-team-sprint-board/test_api.py @@ -0,0 +1,113 @@ +import requests +import json + +BASE_URL = "http://127.0.0.1:5000" +HEADERS = {"X-User-Name": "TestUser", "Content-Type": "application/json"} + +def test_user_identity(): + print("Testing User Identity...") + # Request without header should fail + resp = requests.get(f"{BASE_URL}/board") + assert resp.status_code == 400 + print("OK: Request without identity rejected") + +def test_create_cards(): + print("Testing Create Cards...") + # Valid card + payload = {"column": "Backlog", "title": "Task 1", "description": "Desc 1", "story_points": 3} + resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS) + assert resp.status_code == 201 + card_id = resp.json()['id'] + + # Card with only title + payload = {"column": "In Progress", "title": "Task 2"} + resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS) + assert resp.status_code == 201 + card_id_2 = resp.json()['id'] + + # Invalid: No title + payload = {"column": "Backlog", "description": "No title"} + resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS) + assert resp.status_code == 400 + + # Invalid: Wrong story points + payload = {"column": "Backlog", "title": "Task 3", "story_points": 4} + resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS) + assert resp.status_code == 400 + print("OK: Card creation and validation working") + return card_id, card_id_2 + +def test_get_board(card_id, card_id_2): + print("Testing Get Board...") + resp = requests.get(f"{BASE_URL}/board", headers=HEADERS) + assert resp.status_code == 200 + board = resp.json() + assert "Backlog" in board and "In Progress" in board + # Verify card 1 is in Backlog + assert any(c['id'] == card_id for c in board['Backlog']) + # Verify card 2 is in In Progress + assert any(c['id'] == card_id_2 for c in board['In Progress']) + print("OK: Board retrieval working") + +def test_edit_and_move_card(card_id): + print("Testing Edit and Move Card...") + # Update title and move to Review + payload = {"title": "Updated Task 1", "status": "Review", "story_points": 5} + resp = requests.put(f"{BASE_URL}/cards/{card_id}", json=payload, headers=HEADERS) + assert resp.status_code == 200 + assert resp.json()['title'] == "Updated Task 1" + assert resp.json()['status'] == "Review" + + # Verify board reflects move + resp = requests.get(f"{BASE_URL}/board", headers=HEADERS) + board = resp.json() + assert any(c['id'] == card_id for c in board['Review']) + assert not any(c['id'] == card_id for c in board['Backlog']) + print("OK: Card editing and moving working") + +def test_delete_card(card_id): + print("Testing Delete Card...") + resp = requests.delete(f"{BASE_URL}/cards/{card_id}", headers=HEADERS) + assert resp.status_code == 204 + + resp = requests.get(f"{BASE_URL}/board", headers=HEADERS) + board = resp.json() + for col in board: + assert not any(c['id'] == card_id for c in board[col]) + print("OK: Card deletion working") + +def test_clear_done(): + print("Testing Clear Done...") + # Create a card in Done + payload = {"column": "Done", "title": "Done Task"} + resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS) + card_id = resp.json()['id'] + + # Create a card in Backlog + payload = {"column": "Backlog", "title": "Backlog Task"} + resp = requests.post(f"{BASE_URL}/cards", json=payload, headers=HEADERS) + backlog_id = resp.json()['id'] + + # Clear Done + resp = requests.delete(f"{BASE_URL}/board/done", headers=HEADERS) + assert resp.status_code == 204 + + # Verify + resp = requests.get(f"{BASE_URL}/board", headers=HEADERS) + board = resp.json() + assert len(board['Done']) == 0 + assert any(c['id'] == backlog_id for c in board['Backlog']) + print("OK: Clear Done working") + +if __name__ == "__main__": + try: + test_user_identity() + c1, c2 = test_create_cards() + test_get_board(c1, c2) + test_edit_and_move_card(c1) + test_delete_card(c2) # deleting c2 + test_clear_done() + print("\nALL TESTS PASSED!") + except Exception as e: + print(f"\nTEST FAILED: {e}") + exit(1)