Skip to content
Merged
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
30 changes: 30 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Test

on:
push:
branches: ["**"]
pull_request:
branches: ["**"]

jobs:
pytest:
name: Pytest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
working-directory: example/config-app
run: uv sync

- name: Run tests
working-directory: example/config-app
run: uv run pytest tests/ -v
3 changes: 3 additions & 0 deletions example/config-app/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
REDIS_HOST=host.production
REDIS_PORT=1111
REDIS_DB=1
3 changes: 3 additions & 0 deletions example/config-app/.env.testing
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
REDIS_HOST=host.testing
REDIS_PORT=2222
REDIS_DB=2
1 change: 1 addition & 0 deletions example/config-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv
8 changes: 8 additions & 0 deletions example/config-app/artisan
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env python3

import sys
from bootstrap.application import app

if __name__ == "__main__":
status = app.handle_command()
sys.exit(status if isinstance(status, int) else 0)
7 changes: 7 additions & 0 deletions example/config-app/bootstrap/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pathlib import Path

from fastapi_startkit import Application

app: Application = Application(
base_path=Path(__file__).resolve().parent.parent,
)
14 changes: 14 additions & 0 deletions example/config-app/config/redis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from dataclasses import dataclass, field

from fastapi_startkit.environment import env


@dataclass
class RedisConfig:
host: str = field(default_factory=lambda: env('REDIS_HOST'))
port: int = field(default_factory=lambda: env('REDIS_PORT'))
db: int = field(default_factory=lambda: env('REDIS_DB'))

options: dict = field(default_factory=lambda: {
'decode_responses': True
})
21 changes: 21 additions & 0 deletions example/config-app/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[project]
name = "console-app"
version = "0.1.0"
description = "A simple application to demonstrate the console application."
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"fastapi-startkit",
]

[tool.uv.sources]
fastapi-startkit = { path = "../../fastapi_startkit", editable=true }

[dependency-groups]
dev = [
"dumpdie>=1.5.0",
"pytest>=8.0.0",
]

[tool.pytest.ini_options]
pythonpath = ["."]
97 changes: 97 additions & 0 deletions example/config-app/tests/test_config_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import os
import unittest
from pathlib import Path

from config.redis import RedisConfig

from fastapi_startkit import Config


class ConfigTest(unittest.TestCase):
def test_it_loads_env_from_arguments(self):
os.environ.pop('PYTEST_CURRENT_TEST', None)
from fastapi_startkit import Application

# We initialize the application with no env, so it should load the default env
Application(base_path=Path(__file__).resolve().parent.parent)

self.assertEqual(RedisConfig().host, "host.default")
self.assertEqual(RedisConfig().port, 0000)
self.assertEqual(RedisConfig().db, 0)

# We initialize the application with production env, so it should load the production env
Application(base_path=Path(__file__).resolve().parent.parent, env="production")

self.assertEqual(RedisConfig().host, "host.production")
self.assertEqual(RedisConfig().port, 1111)
self.assertEqual(RedisConfig().db, 1)

# We initialize the application with testing env, so it should load the testing env
Application(base_path=Path(__file__).resolve().parent.parent, env="testing")
self.assertEqual(RedisConfig().host, "host.testing")
self.assertEqual(RedisConfig().port, 2222)
self.assertEqual(RedisConfig().db, 2)

# We initialize the application with wrong env, so it should load the default env
Application(base_path=Path(__file__).resolve().parent.parent, env="wrong-env")
self.assertEqual(RedisConfig().host, "host.default")
self.assertEqual(RedisConfig().port, 0000)
self.assertEqual(RedisConfig().db, 0)

def test_app_loads_env_data(self):
os.environ.pop('PYTEST_CURRENT_TEST', None)
from bootstrap.application import app

self.assertEqual(RedisConfig().host, "host.default")
self.assertEqual(RedisConfig().port, 0000)
self.assertEqual(RedisConfig().db, 0)

app.set_environment('testing')
app.load_environment()
self.assertEqual(RedisConfig().host, "host.testing")
self.assertEqual(RedisConfig().port, 2222)
self.assertEqual(RedisConfig().db, 2)

app.set_environment('production')
app.load_environment()

self.assertEqual(RedisConfig().host, "host.production")
self.assertEqual(RedisConfig().port, 1111)
self.assertEqual(RedisConfig().db, 1)


class ConfigFacadeTest(unittest.TestCase):
def setUp(self):
os.environ.pop('PYTEST_CURRENT_TEST', None)
from fastapi_startkit import Application
self.app = Application(base_path=Path(__file__).resolve().parent.parent)
Config.set('redis', RedisConfig())

def test_get_string_field(self):
self.assertEqual(Config.get('redis.host'), 'host.default')

def test_get_int_field(self):
self.assertEqual(Config.get('redis.port'), 0)
self.assertIsInstance(Config.get('redis.port'), int)

def test_get_dict_field(self):
self.assertEqual(Config.get('redis.options'), {'decode_responses': True})

def test_get_nested_dotted_key(self):
self.assertIs(Config.get('redis.options.decode_responses'), True)

def test_has_existing_key(self):
self.assertTrue(Config.has('redis'))

def test_has_missing_key(self):
self.assertFalse(Config.has('redis.nonexistent'))

def test_get_missing_key_returns_none(self):
self.assertIsNone(Config.get('redis.nonexistent'))

def test_get_missing_key_returns_default(self):
self.assertEqual(Config.get('redis.nonexistent', 'fallback'), 'fallback')

def test_set_overrides_value_at_runtime(self):
Config.set('redis.host', 'overridden-host')
self.assertEqual(Config.get('redis.host'), 'overridden-host')
Loading
Loading