diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..f7b518c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,57 @@ +name: CI + +on: + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + + services: + db: + image: postgres:15-alpine + env: + POSTGRES_DB: expedition_db + POSTGRES_USER: expedition_user + POSTGRES_PASSWORD: expedition_pass + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U expedition_user -d expedition_db" + --health-interval 5s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 5s + --health-retries 5 + + env: + DATABASE_URL: postgres://expedition_user:expedition_pass@localhost:5432/expedition_db + REDIS_URL: redis://localhost:6379 + SECRET_KEY: test-secret-key + DEBUG: "True" + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run migrations + run: python manage.py migrate --noinput + + - name: Run tests + run: pytest tests/ diff --git a/authentication/authentication.py b/authentication/authentication.py index 5d1d6b2..0571dd6 100644 --- a/authentication/authentication.py +++ b/authentication/authentication.py @@ -8,6 +8,8 @@ from .models import Session, User class SessionAuthentication(BaseAuthentication): + def authenticate_header(self, request): + return 'Bearer' def authenticate(self, request: HttpRequest): header = request.headers.get("Authorization") if not header: diff --git a/tests/test_cycles.py b/tests/test_cycles.py new file mode 100644 index 0000000..0c4d0f9 --- /dev/null +++ b/tests/test_cycles.py @@ -0,0 +1,19 @@ +import pytest +import os +from tools.check_cycles import CycleDetector + +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + +class TestDependencyCycles: + @pytest.fixture(autouse=True) + def setup(self): + self.cycle_detector = CycleDetector(PROJECT_ROOT) + + def test_no_cycles_exist(self): + self.cycle_detector.add_directory('/expeditions/') + self.cycle_detector.add_directory('/authentication/') + cycles = self.cycle_detector.detect() + assert len(cycles) == 0, ( + f"Circular dependencies detected:\n" + + "\n".join(" -> ".join(c) for c in cycles) + ) \ No newline at end of file diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/check_cycles.py b/tools/check_cycles.py new file mode 100644 index 0000000..28165b9 --- /dev/null +++ b/tools/check_cycles.py @@ -0,0 +1,73 @@ +import ast +import os +from collections import defaultdict + + +class CycleDetector: + def __init__(self, root): + self._root = os.path.abspath(root) + self._dirs = set() + + def add_directory(self, directory): + self._dirs.add(self._root+directory) + + @staticmethod + def _find_imports(filepath): + with open(filepath, 'r') as f: + tree = ast.parse(f.read()) + + imports = [] + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + imports.append(alias.name) + elif isinstance(node, ast.ImportFrom): + if node.module: + imports.append(node.module) + return imports + + @staticmethod + def _detect_cycles(graph): + visited = set() + in_stack = set() + cycles = [] + + def dfs(node, path): + visited.add(node) + in_stack.add(node) + path.append(node) + + for neighbor in graph.get(node, []): + if neighbor not in visited: + dfs(neighbor, path) + elif neighbor in in_stack: + cycle_start = path.index(neighbor) + cycles.append(path[cycle_start:] + [neighbor]) + + path.pop() + in_stack.remove(node) + + for node in graph: + if node not in visited: + dfs(node, []) + + return cycles + + def _build_dependency_graph(self, project_path): + graph = defaultdict(set) + + for root, dirs, files in os.walk(project_path): + for f in files: + if f.endswith('.py'): + filepath = os.path.join(root, f) + module = filepath.replace(self._root, '').replace('/', '.').replace('.py', '').lstrip('.') + imports = self._find_imports(filepath) + for imp in imports: + graph[module].add(imp) + return graph + + def detect(self): + graph = defaultdict(set) + for directory in self._dirs: + graph.update(self._build_dependency_graph(directory)) + return self._detect_cycles(graph) \ No newline at end of file