Skip to content
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
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.11-slim

WORKDIR /code

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
22 changes: 22 additions & 0 deletions QA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
app_type: web-api
coverage_applies: true
coverage_source: app
coverage_threshold: 70
coverage_tool: pytest-cov
install_steps:
- pip install --upgrade pip
- pip install -r /tmp/forge-repos/hello-world-fastapi-1ca57c11/requirements.txt
lint_tool: ruff check .
notes: Verify that all tests in tests/ pass, code coverage for app/ is at least 70%,
and ruff reports no lint errors.
stack: Python/FastAPI
test_files:
- tests/__init__.py
- tests/conftest.py
- tests/test_hello.py
- tests/test_main.py
- tests/test_models.py
- tests/test_storage.py
- tests/test_todos.py
test_runner: pytest tests/
workspace: /tmp/forge-repos/hello-world-fastapi-1ca57c11
79 changes: 67 additions & 12 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,88 @@
# Running the Todo API
# Hello World FastAPI Application

## TEAM_BRIEF
stack: Python/FastAPI
test_runner: pytest tests/
lint_tool: ruff check .
coverage_tool: pytest-cov
coverage_threshold: 70
coverage_applies: true

## Prerequisites

- Python 3.10 or later
- Python 3.11+ (for local development)
- Docker and Docker Compose (for containerised execution)

## Local Development

## Install dependencies
### 1. Install dependencies

```bash
pip install fastapi uvicorn pydantic
pip install -r requirements.txt
```

For running the test suite you will also need:
### 2. Run the application

```bash
pip install httpx pytest
uvicorn app.main:app --host 0.0.0.0 --port 8000
```

## Start the server
The API will be available at: **http://localhost:8000/hello**

### 3. Run the test suite

```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
pytest tests/
```

The API will be available at <http://localhost:8000>.
To run with verbose output:

```bash
pytest tests/ -v
```

Interactive docs are served at <http://localhost:8000/docs>.
## Docker

## Run the tests
### Build and run with Docker Compose

```bash
pytest tests/
docker compose up --build
```

The application will be accessible at: **http://localhost:8000/hello**

To stop the application:

```bash
docker compose down
```

### Build and run with Docker only

```bash
docker build -t hello-api .
docker run -p 8000:8000 hello-api
```

## API Endpoints

| Method | Path | Description | Response |
|--------|----------|--------------------------------------|-----------------------------------|
| GET | `/hello` | Returns a JSON greeting message | `{"message": "Hello, World!"}` |

## Project Structure

```
.
├── app/
│ ├── __init__.py
│ └── main.py # FastAPI application with /hello endpoint
├── tests/
│ ├── __init__.py
│ └── test_hello.py # Comprehensive test suite
├── conftest.py # Root pytest configuration
├── requirements.txt # Python dependencies
├── Dockerfile # Container image definition
├── docker-compose.yml # Docker Compose orchestration
└── RUNNING.md # This file
```
Empty file added app/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""FastAPI application entry point.

Creates the FastAPI app instance and defines the GET /hello endpoint.
"""

from __future__ import annotations

from fastapi import FastAPI

app = FastAPI(
title="Hello World API",
description="A minimal FastAPI application with a /hello endpoint.",
version="1.0.0",
)


@app.get("/hello", tags=["hello"])
async def hello() -> dict:
"""Return a JSON greeting message.

Returns:
A dictionary with a single 'message' key.
"""
return {"message": "Hello, World!"}
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: "3.9"

services:
web:
build: .
ports:
- "8000:8000"
restart: unless-stopped
10 changes: 4 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
fastapi>=0.100.0
uvicorn>=0.23.0
pydantic>=2.0.0
pytest>=7.0.0
pytest-timeout>=2.1.0
httpx>=0.24.0
fastapi>=0.110.0
uvicorn>=0.29.0
pytest>=8.0.0
httpx>=0.27.0
100 changes: 100 additions & 0 deletions tests/test_hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Comprehensive test suite for the /hello endpoint.

Verifies correct behaviour of the FastAPI application's GET /hello
endpoint, including status codes, response payloads, headers, and
error handling for unsupported methods and non-existent routes.
"""

from __future__ import annotations

from fastapi.testclient import TestClient

from app.main import app

client: TestClient = TestClient(app)


class TestHelloEndpoint:
"""Tests for the GET /hello endpoint."""

def test_hello_returns_200(self) -> None:
"""GET /hello should return HTTP 200 status code."""
response = client.get("/hello")
assert response.status_code == 200

def test_hello_returns_correct_json(self) -> None:
"""GET /hello should return {'message': 'Hello, World!'}."""
response = client.get("/hello")
assert response.json() == {"message": "Hello, World!"}

def test_hello_content_type_is_json(self) -> None:
"""GET /hello should return a JSON content-type header."""
response = client.get("/hello")
assert "application/json" in response.headers["content-type"]

def test_hello_message_key_present(self) -> None:
"""GET /hello response must contain the 'message' key."""
response = client.get("/hello")
data = response.json()
assert "message" in data

def test_hello_message_value(self) -> None:
"""GET /hello 'message' value must be exactly 'Hello, World!'."""
response = client.get("/hello")
data = response.json()
assert data["message"] == "Hello, World!"

def test_hello_response_has_single_key(self) -> None:
"""GET /hello response should contain exactly one key."""
response = client.get("/hello")
data = response.json()
assert len(data) == 1


class TestHelloMethodNotAllowed:
"""Tests verifying that unsupported HTTP methods return 405."""

def test_post_hello_returns_405(self) -> None:
"""POST /hello should return HTTP 405 Method Not Allowed."""
response = client.post("/hello")
assert response.status_code == 405

def test_put_hello_returns_405(self) -> None:
"""PUT /hello should return HTTP 405 Method Not Allowed."""
response = client.put("/hello")
assert response.status_code == 405

def test_delete_hello_returns_405(self) -> None:
"""DELETE /hello should return HTTP 405 Method Not Allowed."""
response = client.delete("/hello")
assert response.status_code == 405

def test_patch_hello_returns_405(self) -> None:
"""PATCH /hello should return HTTP 405 Method Not Allowed."""
response = client.patch("/hello")
assert response.status_code == 405


class TestNonExistentRoutes:
"""Tests verifying that requests to unknown paths return 404."""

def test_unknown_path_returns_404(self) -> None:
"""GET /nonexistent should return HTTP 404 Not Found."""
response = client.get("/nonexistent")
assert response.status_code == 404

def test_root_path_returns_404(self) -> None:
"""GET / should return HTTP 404 when no root route is defined."""
response = client.get("/")
assert response.status_code == 404


class TestHelloIdempotency:
"""Tests verifying that repeated calls return consistent results."""

def test_multiple_calls_return_same_result(self) -> None:
"""Consecutive GET /hello calls should return identical responses."""
response1 = client.get("/hello")
response2 = client.get("/hello")
assert response1.json() == response2.json()
assert response1.status_code == response2.status_code