diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..155acdf --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +exclude = venv/* +max-line-length = 99 diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml new file mode 100644 index 0000000..f0b4b71 --- /dev/null +++ b/.github/workflows/ci-pipeline.yml @@ -0,0 +1,78 @@ +name: CI Pipeline + +on: + push: + branches: + - feature/* + - fix/* + - refactor/* + - chore/* + - develop + +jobs: + lint-project: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Build and run linter + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile.lint + + run-tests: + needs: lint-project + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Build and run tests + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile.test + + publish-test-image: + needs: run-tests + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: abbastoof + password: ${{ secrets.GH_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile.publish + push: true + tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }} + + merge-develop: + runs-on: ubuntu-latest + needs: publish-test-image + if: github.ref_name == 'develop' + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Fetch all branches + run: git fetch --all + + - name: Merge branches into develop + run: | + git checkout develop + git merge --no-ff ${{ github.ref_name }} + git push origin develop diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ba24b3..e7946a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,126 +1,240 @@ -name: CI Pipeline +# name: CI Pipeline + +# permissions: +# pull-requests: read +# contents: read +# issues: read +# deployments: read + +# # Events that trigger the workflow +# on: +# push: +# branches: [main, develop] # Trigger on push to main and develop branches +# pull_request: +# branches: [main, develop] # Trigger on pull request to main and develop branches + +# # Define jobs in the workflow +# jobs: +# setup: +# runs-on: ubuntu-latest +# steps: +# - name: Upgrade setuptools +# run: pip install --upgrade setuptools + +# - name: Checkout code +# uses: actions/checkout@v3 # Checkout the repository code + +# # Set up Python environment +# - name: Set up Python 3.11 +# uses: actions/setup-python@v3 +# with: +# python-version: 3.11 # Use Python version 3.11 + +# # Install project dependencies +# - name: Install dependencies +# run: | +# python -m pip install --upgrade pip # Upgrade pip +# pip install -r requirements.txt # Install dependencies from requirements.txt + +# # test: +# # needs: setup +# # runs-on: ubuntu-latest +# # steps: +# # - name: Checkout code +# # uses: actions/checkout@v3 + +# # - name: Set up Python 3.11 +# # uses: actions/setup-python@v3 +# # with: +# # python-version: 3.11 + +# # - name: Install dependencies +# # run: | +# # python -m pip install --upgrade pip +# # pip install -r requirements.txt + +# # # Run test suite +# # - name: Run tests +# # run: | +# # pytest # Execute tests using pytest + +# security: +# needs: setup +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v3 + +# - name: Set up Python 3.11 +# uses: actions/setup-python@v3 +# with: +# python-version: 3.11 + +# - name: Install dependencies +# run: | +# python -m pip install --upgrade pip +# pip install -r requirements.txt + +# # Run security checks +# - name: Run security checks +# run: | +# pip install bandit # Install Bandit for security checks +# bandit -r . # Run Bandit on the codebase + +# build: +# needs: [setup, security] +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v3 + +# - name: Set up Python 3.11 +# uses: actions/setup-python@v3 +# with: +# python-version: 3.11 + +# - name: Install dependencies +# run: | +# python -m pip install --upgrade pip +# pip install -r requirements.txt + +# # Build the Docker image +# - name: Build Docker image +# run: | +# docker build -t transcendence . + +# deploy: +# needs: build +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v3 + +# # Log in to Docker Hub +# - name: Deploy to Docker Hub +# env: +# DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} +# DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} +# run: | +# echo "${DOCKER_HUB_PASSWORD}" | docker login -u "${DOCKER_HUB_USERNAME}" --password-stdin +# docker tag transcendence ${DOCKER_HUB_USERNAME}/transcendence:latest +# docker push ${DOCKER_HUB_USERNAME}/transcendence:latest + +# # Deploy to the server +# - name: Deploy to server +# run: | +# ssh user@server "docker pull ${DOCKER_HUB_USERNAME}/transcendence:latest && docker-compose up --build -d" -permissions: - pull-requests: read - contents: read - issues: read - deployments: read +name: CI Pipeline -# Events that trigger the workflow on: push: - branches: [main, develop] # Trigger on push to main and develop branches - pull_request: - branches: [main, develop] # Trigger on pull request to main and develop branches + branches: + - feature/* + - develop -# Define jobs in the workflow jobs: - setup: + lint-project: runs-on: ubuntu-latest steps: - - name: Upgrade setuptools - run: pip install --upgrade setuptools - - - name: Checkout code - uses: actions/checkout@v3 # Checkout the repository code + - name: Check out code + uses: actions/checkout@v2 - # Set up Python environment - - name: Set up Python 3.11 - uses: actions/setup-python@v3 + - name: Set up Python + uses: actions/setup-python@v2 with: - python-version: 3.11 # Use Python version 3.11 + python-version: '3.11' - # Install project dependencies - name: Install dependencies run: | - python -m pip install --upgrade pip # Upgrade pip - pip install -r requirements.txt # Install dependencies from requirements.txt - - # test: - # needs: setup - # runs-on: ubuntu-latest - # steps: - # - name: Checkout code - # uses: actions/checkout@v3 - - # - name: Set up Python 3.11 - # uses: actions/setup-python@v3 - # with: - # python-version: 3.11 - - # - name: Install dependencies - # run: | - # python -m pip install --upgrade pip - # pip install -r requirements.txt - - # # Run test suite - # - name: Run tests - # run: | - # pytest # Execute tests using pytest - - security: - needs: setup + python -m venv venv + . venv/bin/activate + python -m pip install --upgrade pip + pip install setuptools==58.0.4 wheel + pip install -r requirements.txt + pip install flake8 + - name: Create flake8 configuration file + run: | + echo "[flake8]" > .flake8 + echo "exclude = venv/*" >> .flake8 + echo "max-line-length = 99" >> .flake8 + + - name: Verify installed packages + run: | + . venv/bin/activate + pip check + + - name: Run linters + run: | + . venv/bin/activate + flake8 . + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + run-tests: + needs: lint-project runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Check out code + uses: actions/checkout@v2 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 + - name: Set up Python + uses: actions/setup-python@v2 with: - python-version: 3.11 + python-version: '3.11' - name: Install dependencies run: | + python -m venv venv + . venv/bin/activate python -m pip install --upgrade pip pip install -r requirements.txt - # Run security checks - - name: Run security checks + - name: Run tests run: | - pip install bandit # Install Bandit for security checks - bandit -r . # Run Bandit on the codebase + . venv/bin/activate + pytest --maxfail=1 --disable-warnings + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - build: - needs: [setup, security] + publish-test-image: + needs: run-tests runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: 3.11 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: abbastoof + password: ${{ secrets.GH_TOKEN }} - # Build the Docker image - - name: Build Docker image - run: | - docker build -t transcendence . + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile + push: true + tags: ghcr.io/${{ github.repository }}:feature/015-game-history-microservice - deploy: - needs: build + merge-develop: runs-on: ubuntu-latest + needs: publish-test-image steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Check out code + uses: actions/checkout@v2 - # Log in to Docker Hub - - name: Deploy to Docker Hub - env: - DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} - DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} - run: | - echo "${DOCKER_HUB_PASSWORD}" | docker login -u "${DOCKER_HUB_USERNAME}" --password-stdin - docker tag transcendence ${DOCKER_HUB_USERNAME}/transcendence:latest - docker push ${DOCKER_HUB_USERNAME}/transcendence:latest + - name: Fetch all branches + run: git fetch --all - # Deploy to the server - - name: Deploy to server + - name: Merge branches into develop run: | - ssh user@server "docker pull ${DOCKER_HUB_USERNAME}/transcendence:latest && docker-compose up --build -d" + git checkout develop + git merge feature/015-game-history-microservice + # Add other branches as needed + git push origin develop diff --git a/.github/workflows/merge-develop-to-main.yml b/.github/workflows/merge-develop-to-main.yml new file mode 100644 index 0000000..a0b628f --- /dev/null +++ b/.github/workflows/merge-develop-to-main.yml @@ -0,0 +1,29 @@ +name: Merge Develop to Main + +on: + pull_request: + branches: + - develop + +jobs: + merge-to-main: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Merge develop into main + run: | + git checkout main + git merge develop --no-ff -m "Merging develop into main" + git push origin main diff --git a/.github/workflows/merge-to-develop.yml b/.github/workflows/merge-to-develop.yml new file mode 100644 index 0000000..d0bc101 --- /dev/null +++ b/.github/workflows/merge-to-develop.yml @@ -0,0 +1,34 @@ +name: Merge to Develop + +on: + push: + branches: + - 'feature/*' + - 'fix/*' + - 'refactor/*' + - 'chore/*' + +jobs: + merge-to-develop: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Merge branches into develop + run: | + BRANCHES=$(git for-each-ref --format '%(refname:short)' refs/heads/feature refs/heads/fix refs/heads/refactor refs/heads/chore) + for branch in $BRANCHES; do + git checkout develop + git merge $branch --no-ff -m "Merging $branch into develop" + done + git push origin develop diff --git a/.gitignore b/.gitignore index d9f7bfb..c20ae1a 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,12 @@ a.out # Ignore Trunk specific files .trunk/ + + +# Ignore __pycache__ +__pycache__/ + + +# Ignore Pytest cache +pytest_cache/ +.pytest_cache/ diff --git a/Backend/game_history/Dockerfile b/Backend/game_history/Dockerfile new file mode 100755 index 0000000..a198860 --- /dev/null +++ b/Backend/game_history/Dockerfile @@ -0,0 +1,49 @@ +FROM alpine:3.20 + +ENV PYTHONUNBUFFERED=1 +ENV LANG=C.UTF-8 + +# Update and install dependencies +# trunk-ignore(hadolint/DL3018) +RUN apk update && apk add --no-cache python3 py3-pip \ + postgresql16 postgresql16-client \ + bash supervisor curl openssl bash \ + build-base libffi-dev python3-dev + + +# Set work directory +RUN mkdir /run/postgresql && \ + chown postgres:postgres /run/postgresql && \ + mkdir /app && chown -R postgres:postgres /app + +WORKDIR /app/ + +# Install Python virtual environment +RUN python3 -m venv venv && chown -R postgres:postgres venv + +# Copy application code and adjust permissions +COPY --chown=postgres:postgres ./Backend/game_history/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY --chown=postgres:postgres ./Backend/game_history/requirements.txt . +COPY --chown=postgres:postgres ./Backend/game_history/game_history /app/game_history +COPY --chown=postgres:postgres ./Backend/game_history/tools.sh /app +COPY --chown=postgres:postgres ./Backend/game_history/init_database.sh /app + + +# Install Python packages +RUN . venv/bin/activate && pip install -r requirements.txt + +# Create log files and set permissions +RUN touch /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err && \ + chown -R postgres:postgres /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err + +# Ensure supervisord and scripts are executable and owned by postgres +RUN chown -R postgres:postgres /app && \ + chown -R postgres:postgres /var/log && \ + chown -R postgres:postgres /app/venv && \ + chown -R postgres:postgres /app/game_history + +USER postgres + +HEALTHCHECK --interval=30s --timeout=2s --start-period=5s --retries=3 CMD curl -sSf http://localhost:8002/game-history/ > /dev/null && echo "success" || echo "failure" + +ENTRYPOINT ["sh", "./tools.sh"] diff --git a/Backend/game_history/README.md b/Backend/game_history/README.md new file mode 100644 index 0000000..7402e06 --- /dev/null +++ b/Backend/game_history/README.md @@ -0,0 +1,166 @@ +## Game History Microservice Documentation + +### Overview + +The Game History microservice is a key component of the Transcendence project at 42 School. This service is responsible for recording and managing the history of ping pong games played by users. It supports creating, retrieving, updating, and deleting game history and game statistics records. + +### Directory Structure + +``` +. +├── Dockerfile +├── game_history +│ ├── game_data +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── migrations +│ │ │ └── __init__.py +│ │ ├── models.py +│ │ ├── serializers.py +│ │ ├── tests.py +│ │ ├── urls.py +│ │ └── views.py +│ ├── game_history +│ │ ├── __init__.py +│ │ ├── asgi.py +│ │ ├── settings.py +│ │ ├── tests +│ │ │ ├── __init__.py +│ │ │ ├── conftest.py +│ │ │ └── test_game_history.py +│ │ ├── urls.py +│ │ └── wsgi.py +│ └── manage.py +├── init_database.sh +├── requirements.txt +├── supervisord.conf +└── tools.sh +``` + +### Setup + +#### Docker Setup + +The `game_history` microservice is containerized using Docker. The `Dockerfile` sets up the environment needed to run the Django application. + +#### Environment Variables + +Environment variables should be defined in the `.env` file to configure the service. These may include database connection details, secret keys, and other configurations. + +### Models + +The `GameHistory` model represents a record of a game played between two users. It includes the following fields: + +- `game_id`: AutoField, primary key. +- `player1_id`: Integer, ID of the first player. +- `player2_id`: Integer, ID of the second player. +- `winner_id`: Integer, ID of the winning player. +- `start_time`: DateTime, the start time of the game. +- `end_time`: DateTime, the end time of the game (optional). + +The `GameStat` model represents the statistics of a game, linked to a `GameHistory` record. It includes the following fields: + +- `game_id`: OneToOneField, primary key linked to `GameHistory`. +- `player1_score`: Integer, score of the first player. +- `player2_score`: Integer, score of the second player. +- `total_hits`: Integer, total number of hits in the game. +- `longest_rally`: Integer, the longest rally in the game. + +### Serializers + +- The `GameHistorySerializer` converts `GameHistory` model instances to JSON format and validates incoming data. +- The `GameStatSerializer` converts `GameStat` model instances to JSON format and validates incoming data. + +### Views + +- The `GameHistoryViewSet` handles the CRUD operations for game history records. +- The `GameStatViewSet` handles the CRUD operations for game statistics records. + +### URLs + +The microservice defines several endpoints to interact with the game history and game statistics data. These endpoints are defined in the `game_data/urls.py` file. Here is an overview of how to access them: + +- **Game History Endpoints:** + - **List and Create Game History Records:** + ``` + GET /game-history/ + POST /game-history/ + ``` + - **Retrieve, Update, and Delete a Specific Game History Record:** + ``` + GET /game-history// + PUT /game-history// + DELETE /game-history// + ``` + +- **Game Stat Endpoints:** + - **List and Create Game Stat Records:** + ``` + GET /game-stat/ + POST /game-stat/ + ``` + - **Retrieve, Update, and Delete a Specific Game Stat Record:** + ``` + GET /game-stat// + PUT /game-stat// + DELETE /game-stat// + ``` + +### Tests + +#### Directory Structure + +The tests for the Game History microservice are located in the `game_history/game_history/tests/` directory. The tests ensure that the CRUD operations for game history and game statistics records are working correctly. + +#### Test Cases + +1. **Test Create Game History** + - Verifies that a game history record can be created successfully. + +2. **Test List Game Histories** + - Verifies that a list of game history records can be retrieved successfully. + +3. **Test Retrieve Game History** + - Verifies that a specific game history record can be retrieved successfully. + +4. **Test Update Game History** + - Verifies that a specific game history record can be updated successfully. + +5. **Test Delete Game History** + - Verifies that a specific game history record can be deleted successfully. + +6. **Test Create Game Stat** + - Verifies that a game statistics record can be created successfully. + +7. **Test List Game Stats** + - Verifies that a list of game statistics records can be retrieved successfully. + +8. **Test Retrieve Game Stat** + - Verifies that a specific game statistics record can be retrieved successfully. + +9. **Test Update Game Stat** + - Verifies that a specific game statistics record can be updated successfully. + +10. **Test Delete Game Stat** + - Verifies that a specific game statistics record can be deleted successfully. + +### Running Tests + +To run the tests, use the following commands: + +1. Build and start the Docker container: + ```sh + docker-compose up --build + ``` + +2. Execute the tests within the Docker container: + ```sh + docker exec -it game-history bash + . venv/bin/activate + pytest + ``` + +### Conclusion + +The Game History microservice is an essential part of the Transcendence project, providing a robust solution for managing game history and game statistics records. This documentation provides an overview of the setup, implementation, and testing of the service. For further details, refer to the respective source code files in the project directory. diff --git a/Backend/game_history/game_history/game_data/__init__.py b/Backend/game_history/game_history/game_data/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/Backend/game_history/game_history/game_data/admin.py b/Backend/game_history/game_history/game_data/admin.py new file mode 100755 index 0000000..8c38f3f --- /dev/null +++ b/Backend/game_history/game_history/game_data/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Backend/game_history/game_history/game_data/apps.py b/Backend/game_history/game_history/game_data/apps.py new file mode 100755 index 0000000..bbe8172 --- /dev/null +++ b/Backend/game_history/game_history/game_data/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GameDataConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'game_data' diff --git a/Backend/game_history/game_history/game_data/migrations/__init__.py b/Backend/game_history/game_history/game_data/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/Backend/game_history/game_history/game_data/models.py b/Backend/game_history/game_history/game_data/models.py new file mode 100755 index 0000000..58c6796 --- /dev/null +++ b/Backend/game_history/game_history/game_data/models.py @@ -0,0 +1,21 @@ +# models.py +from django.db import models + +class GameHistory(models.Model): + game_id = models.AutoField(primary_key=True) + player1_id = models.IntegerField() + player2_id = models.IntegerField() + winner_id = models.IntegerField() + start_time = models.DateTimeField() + end_time = models.DateTimeField(null=True, blank=True) + def __str__(self): + return f"Game {self.game_id}: {self.player1_id} vs {self.player2_id} - Winner: {self.winner_id}" + +class GameStat(models.Model): + game_id = models.OneToOneField(GameHistory, on_delete=models.CASCADE, primary_key=True) # this field is a foreign key to the GameHistory model + player1_score = models.IntegerField() + player2_score = models.IntegerField() + total_hits = models.IntegerField() + longest_rally = models.IntegerField() + def __str__(self): + return f"Stats for Game {self.game_id.game_id}: {self.player1_score} vs {self.player2_score} - Total Hits: {self.total_hits}, Longest Rally: {self.longest_rally}" diff --git a/Backend/game_history/game_history/game_data/serializers.py b/Backend/game_history/game_history/game_data/serializers.py new file mode 100755 index 0000000..f21371c --- /dev/null +++ b/Backend/game_history/game_history/game_data/serializers.py @@ -0,0 +1,13 @@ +# game_data/serializers.py + +from rest_framework import serializers +from .models import GameHistory, GameStat + +class GameHistorySerializer(serializers.ModelSerializer): + class Meta: + model = GameHistory + fields = '__all__' +class GameStatSerializer(serializers.ModelSerializer): + class Meta: + model = GameStat + fields = '__all__' diff --git a/Backend/game_history/game_history/game_data/tests.py b/Backend/game_history/game_history/game_data/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/Backend/game_history/game_history/game_data/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Backend/game_history/game_history/game_data/urls.py b/Backend/game_history/game_history/game_data/urls.py new file mode 100755 index 0000000..d540a12 --- /dev/null +++ b/Backend/game_history/game_history/game_data/urls.py @@ -0,0 +1,47 @@ +from django.urls import path +from .views import GameHistoryViewSet, GameStatViewSet + +urlpatterns = [ + path( + "game-history/", + GameHistoryViewSet.as_view( + { + "get": "list", + "post": "create" + } + ), + name="game-history-list", + ), + path( + "game-history//", + GameHistoryViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "delete": "destroy", + } + ), + name="game-history-detail", + ), + path( + "game-stat/", + GameStatViewSet.as_view( + { + "get": "list", + "post": "create" + } + ), + name="gamestat-list", + ), + path( + "game-stat//", + GameStatViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "delete": "destroy", + } + ), + name="gamestat-detail", + ), +] diff --git a/Backend/game_history/game_history/game_data/views.py b/Backend/game_history/game_history/game_data/views.py new file mode 100755 index 0000000..39816e7 --- /dev/null +++ b/Backend/game_history/game_history/game_data/views.py @@ -0,0 +1,113 @@ +from rest_framework import viewsets, status +from .models import GameHistory, GameStat +from .serializers import GameHistorySerializer, GameStatSerializer +from rest_framework.response import Response +from django.shortcuts import get_object_or_404 + +class GameHistoryViewSet(viewsets.ModelViewSet): + """ + A viewset for viewing and editing game history instances. + """ + serializer_class = GameHistorySerializer + queryset = GameHistory.objects.all() + + def create(self, request, *args, **kwargs): + """ + Create a new game history record. + """ + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def list(self, request, *args, **kwargs): + """ + List all game history records. + """ + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) # serializer.data is a list of dictionaries containing the serialized data + + def retrieve(self, request, pk=None, *args, **kwargs): + """ + Retrieve a specific game history record. + """ + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance) + return Response(serializer.data, status=status.HTTP_200_OK) + + def update(self, request, pk=None, *args, **kwargs): + """ + Update a specific game history record. + """ + partial = kwargs.pop('partial', False) # this line will remove the 'partial' key from the kwargs dictionary and return its value (False by default), because the partial argument is not needed in the update method + instance = get_object_or_404(self.get_queryset(), pk=pk) # get the instance of the game history record + serializer = self.get_serializer(instance, data=request.data, partial=partial) # create a serializer instance with the instance and the request data as arguments + if serializer.is_valid(): + self.perform_update(serializer) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, pk=None, *args, **kwargs): + """ + Delete a specific game history record. + """ + instance = get_object_or_404(self.get_queryset(), pk=pk) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + +class GameStatViewSet(viewsets.ModelViewSet): + """ + A viewset for viewing and editing game stat instances. + """ + serializer_class = GameStatSerializer + queryset = GameStat.objects.all() + + def create(self, request, *args, **kwargs): + """ + Create a new game stat record. + """ + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def list(self, request, *args, **kwargs): + """ + List all game stat records. + """ + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def retrieve(self, request, pk=None, *args, **kwargs): + """ + Retrieve a specific game stat record. + """ + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance) + return Response(serializer.data, status=status.HTTP_200_OK) + + def update(self, request, pk=None, *args, **kwargs): + """ + Update a specific game stat record. + """ + partial = kwargs.pop('partial', False) + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance, data=request.data, partial=partial) + if serializer.is_valid(): + self.perform_update(serializer) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, pk=None, *args, **kwargs): + """ + Delete a specific game stat record. + """ + instance = get_object_or_404(self.get_queryset(), pk=pk) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/Backend/game_history/game_history/game_history/__init__.py b/Backend/game_history/game_history/game_history/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/Backend/game_history/game_history/game_history/asgi.py b/Backend/game_history/game_history/game_history/asgi.py new file mode 100755 index 0000000..0465109 --- /dev/null +++ b/Backend/game_history/game_history/game_history/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for game_history project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_history.settings') + +application = get_asgi_application() diff --git a/Backend/game_history/game_history/game_history/settings.py b/Backend/game_history/game_history/game_history/settings.py new file mode 100755 index 0000000..fb5c915 --- /dev/null +++ b/Backend/game_history/game_history/game_history/settings.py @@ -0,0 +1,170 @@ +""" +Django settings for game_history project. + +Generated by 'django-admin startproject' using Django 5.0.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" +import os +from datetime import timedelta +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +RABBITMQ_HOST = os.getenv("RABBITMQ_HOST") +RABBITMQ_USER = os.getenv("RABBITMQ_USER") +RABBITMQ_PASS = os.getenv("RABBITMQ_PASS") +RABBITMQ_PORT = os.getenv("RABBITMQ_PORT") + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-woftd2en2**zr(b%#*2vit2v%s@(k54gb^c(ots0abo7(wsmo%" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [ + 'localhost', + '127.0.0.1', + '[::1]', + 'game-history', + 'game-history:8002', + 'testserver', +] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'game_data', + 'rest_framework', + 'rest_framework_simplejwt', + 'corsheaders', +] + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": False, # If True, refresh tokens will rotate, meaning that a new token is returned with each request to the refresh endpoint. for example, if a user is logged in on multiple devices, rotating refresh tokens will cause all devices to be logged out when the user logs out on one device. + "BLACKLIST_AFTER_ROTATION": True, # If True, the refresh token will be blacklisted after it is used to obtain a new access token. This means that if a refresh token is stolen, it can only be used once to obtain a new access token. This is useful if rotating refresh tokens is enabled, but can cause problems if a refresh token is shared between multiple clients. + "AUTH_HEADER_TYPES": ("Bearer",), + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), +} + +# Add REST framework settings for JWT authentication +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + # 'DEFAULT_RENDERER_CLASSES': [ + # 'rest_framework.renderers.JSONRenderer', + # ], + # 'DEFAULT_PARSER_CLASSES': [ + # 'rest_framework.parsers.JSONParser', + # ], +} + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = 'game_history.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +ASGI_APPLICATION = 'game_history.asgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "postgres", + "USER": "root", + "PASSWORD": "root", + "PORT": "5432", + } +} + + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + diff --git a/Backend/game_history/game_history/game_history/tests/__init__.py b/Backend/game_history/game_history/game_history/tests/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/Backend/game_history/game_history/game_history/tests/conftest.py b/Backend/game_history/game_history/game_history/tests/conftest.py new file mode 100755 index 0000000..2ed71aa --- /dev/null +++ b/Backend/game_history/game_history/game_history/tests/conftest.py @@ -0,0 +1,105 @@ +import os +import django +from django.conf import settings +import pytest +from datetime import timedelta +from django.core.management import call_command + + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_history.settings') + +if not settings.configured: + settings.configure( + DEBUG=True, + DATABASES={ + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "test_game_history_db", + "USER": "root", + "PASSWORD": "root", + "PORT": "5432", + "HOST": "localhost", + "ATOMIC_REQUESTS": True, + } + }, + INSTALLED_APPS=[ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + 'game_data', + ], + MIDDLEWARE=[ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ], + ROOT_URLCONF='game_history.urls', + TEMPLATES=[ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, + ], + WSGI_APPLICATION='game_history.wsgi.application', + SECRET_KEY='django-insecure-woftd2en2**zr(b%#*2vit2v%s@(k54gb^c(ots0abo7(wsmo%', + ALLOWED_HOSTS=['localhost', '127.0.0.1', '[::1]', 'game-history', 'game-history:8002'], + RABBITMQ_HOST='localhost', + RABBITMQ_USER='user', + RABBITMQ_PASS='pass', + RABBITMQ_PORT='5672', + REST_FRAMEWORK={ + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + }, + SIMPLE_JWT={ + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_OBTAIN_SERIALIZER': 'user_auth.serializers.CustomTokenObtainPairSerializer', + }, + LANGUAGE_CODE='en-us', + TIME_ZONE='UTC', + USE_I18N=True, + USE_L10N=True, + USE_TZ=True, + STATIC_URL='/static/', + DEFAULT_AUTO_FIELD='django.db.models.BigAutoField', + CORS_ORIGIN_ALLOW_ALL=True, + ) + +django.setup() + +@pytest.fixture(scope='session', autouse=True) +def django_db_setup(): + settings.DATABASES['default'] = { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'test_game_history_db', + 'USER': 'postgres', + 'PASSWORD': 'postgres', + 'HOST': 'localhost', + 'PORT': '5432', + 'ATOMIC_REQUESTS': True, + } + call_command('migrate') diff --git a/Backend/game_history/game_history/game_history/tests/test_game_history.py b/Backend/game_history/game_history/game_history/tests/test_game_history.py new file mode 100755 index 0000000..e1e67d3 --- /dev/null +++ b/Backend/game_history/game_history/game_history/tests/test_game_history.py @@ -0,0 +1,213 @@ +import pytest +from django.urls import reverse +from rest_framework.test import APIClient +from rest_framework import status +from game_data.models import GameHistory, GameStat +from django.utils.timezone import now +from datetime import datetime + +@pytest.fixture +def api_client(): + return APIClient() + +@pytest.mark.django_db +def test_create_game_history(api_client): + url = reverse('game-history-list') # reverse() is used to generate the URL for the 'game-history-list' view + data = { + 'player1_id': 1, + 'player2_id': 2, + 'winner_id': 1, + 'start_time': '2024-07-03T12:00:00Z' + } + response = api_client.post(url, data, format='json') # send a POST request to the URL with the data as the request body + assert response.status_code == status.HTTP_201_CREATED + assert GameHistory.objects.count() == 1 # Objects count should be 1 after creating a new GameHistory object in the database + game_history = GameHistory.objects.first() # Get the first GameHistory object from the database (there should be only one) because we just created it + assert game_history.player1_id == 1 # The player1_id should be 1 because we set it to 1 in the data + assert game_history.player2_id == 2 # The player2_id should be 2 because we set it to 2 in the data + assert game_history.winner_id == 1 # The winner_id should be 1 because we set it to 1 in the data + assert game_history.start_time.isoformat() == '2024-07-03T12:00:00+00:00' # The start_time should be '2024-07-03T12:00:00+00:00' because we set it to that value in the data + assert game_history.end_time is None + +@pytest.mark.django_db +def test_list_game_histories(api_client): + game1 = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + game2 = GameHistory.objects.create(player1_id=3, player2_id=4, winner_id=4, start_time=now()) + + url = reverse('game-history-list') + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 # The response should contain two GameHistory objects in the data field because we created two GameHistory objects in the database + assert response.data[0]['player1_id'] == game1.player1_id + assert response.data[0]['player2_id'] == game1.player2_id + assert response.data[0]['winner_id'] == game1.winner_id + + start_time_response = datetime.fromisoformat(response.data[0]['start_time'].replace('Z', '+00:00')) + start_time_expected = game1.start_time + assert start_time_response == start_time_expected + + assert response.data[1]['player1_id'] == game2.player1_id + assert response.data[1]['player2_id'] == game2.player2_id + assert response.data[1]['winner_id'] == game2.winner_id + + start_time_response = datetime.fromisoformat(response.data[1]['start_time'].replace('Z', '+00:00')) + start_time_expected = game2.start_time + assert start_time_response == start_time_expected + +@pytest.mark.django_db +def test_retrieve_game_history(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + + url = reverse('game-history-detail', args=[game.pk]) + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['player1_id'] == game.player1_id + assert response.data['player2_id'] == game.player2_id + assert response.data['winner_id'] == game.winner_id + assert response.data['start_time'] == game.start_time.isoformat().replace('+00:00', 'Z') + +@pytest.mark.django_db +def test_update_game_history(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + + url = reverse('game-history-detail', args=[game.pk]) + data = { + 'player1_id': 1, + 'player2_id': 2, + 'winner_id': 2, + 'start_time': game.start_time.isoformat().replace('+00:00', 'Z') + } + response = api_client.put(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + game.refresh_from_db() + assert game.winner_id == 2 + +@pytest.mark.django_db +def test_delete_game_history(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + + url = reverse('game-history-detail', args=[game.pk]) + response = api_client.delete(url) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert GameHistory.objects.count() == 0 + +@pytest.mark.django_db +def test_create_game_history_validation_error(api_client): + url = reverse('game-history-list') + data = { + 'player1_id': 1, + # 'player2_id' is missing + 'winner_id': 1, + 'start_time': '2024-07-03T12:00:00Z' + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert 'player2_id' in response.data + +@pytest.mark.django_db +def test_primary_key_increment(api_client): + initial_count = GameHistory.objects.count() + + # Create a new game history entry + data = { + 'player1_id': 1, + 'player2_id': 2, + 'winner_id': 1, + 'start_time': '2024-07-03T12:00:00Z', + 'end_time': '2024-07-03T12:30:00Z' + } + response = api_client.post('/game-history/', data, format='json') + assert response.status_code == 201 + + # Check the count after insertion + new_count = GameHistory.objects.count() + assert new_count == initial_count + 1 + + # Get the latest entry and check the primary key + latest_entry = GameHistory.objects.latest('game_id') + print(latest_entry.game_id) # This will print the latest primary key value + +@pytest.mark.django_db +def test_create_game_stat(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + url = reverse('gamestat-list') + data = { + 'game_id': game.game_id, + 'player1_score': 10, + 'player2_score': 5, + 'total_hits': 15, + 'longest_rally': 4 + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + assert GameStat.objects.count() == 1 + game_stat = GameStat.objects.first() + assert game_stat.player1_score == 10 + assert game_stat.player2_score == 5 + assert game_stat.total_hits == 15 + assert game_stat.longest_rally == 4 + +@pytest.mark.django_db +def test_list_game_stat(api_client): + game1 = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + GameStat.objects.create(game_id=game1, player1_score=10, player2_score=5, total_hits=15, longest_rally=4) + + game2 = GameHistory.objects.create(player1_id=3, player2_id=4, winner_id=4, start_time=now()) + GameStat.objects.create(game_id=game2, player1_score=8, player2_score=7, total_hits=20, longest_rally=5) + + url = reverse('gamestat-list') + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 + assert response.data[0]['player1_score'] == 10 + assert response.data[0]['player2_score'] == 5 + assert response.data[0]['total_hits'] == 15 + assert response.data[0]['longest_rally'] == 4 + assert response.data[1]['player1_score'] == 8 + assert response.data[1]['player2_score'] == 7 + assert response.data[1]['total_hits'] == 20 + assert response.data[1]['longest_rally'] == 5 + +@pytest.mark.django_db +def test_retrieve_game_stat(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + game_stat = GameStat.objects.create(game_id=game, player1_score=10, player2_score=5, total_hits=15, longest_rally=4) + + url = reverse('gamestat-detail', args=[game_stat.pk]) + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['player1_score'] == game_stat.player1_score + assert response.data['player2_score'] == game_stat.player2_score + assert response.data['total_hits'] == game_stat.total_hits + assert response.data['longest_rally'] == game_stat.longest_rally + +@pytest.mark.django_db +def test_update_game_stat(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + game_stat = GameStat.objects.create(game_id=game, player1_score=10, player2_score=5, total_hits=15, longest_rally=4) + + url = reverse('gamestat-detail', args=[game_stat.pk]) + data = { + 'game_id': game.game_id, + 'player1_score': 12, + 'player2_score': 6, + 'total_hits': 18, + 'longest_rally': 5 + } + response = api_client.put(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + game_stat.refresh_from_db() + assert game_stat.player1_score == 12 + assert game_stat.player2_score == 6 + assert game_stat.total_hits == 18 + assert game_stat.longest_rally == 5 + +@pytest.mark.django_db +def test_delete_game_stat(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + game_stat = GameStat.objects.create(game_id=game, player1_score=10, player2_score=5, total_hits=15, longest_rally=4) + + url = reverse('gamestat-detail', args=[game_stat.pk]) + response = api_client.delete(url) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert GameStat.objects.count() == 0 diff --git a/Backend/game_history/game_history/game_history/urls.py b/Backend/game_history/game_history/game_history/urls.py new file mode 100755 index 0000000..5c561c0 --- /dev/null +++ b/Backend/game_history/game_history/game_history/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("admin/", admin.site.urls), + path("", include("game_data.urls")), +] diff --git a/Backend/game_history/game_history/game_history/wsgi.py b/Backend/game_history/game_history/game_history/wsgi.py new file mode 100755 index 0000000..b238610 --- /dev/null +++ b/Backend/game_history/game_history/game_history/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for game_history project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_history.settings') + +application = get_wsgi_application() diff --git a/Backend/game_history/game_history/manage.py b/Backend/game_history/game_history/manage.py new file mode 100755 index 0000000..a05f77a --- /dev/null +++ b/Backend/game_history/game_history/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_history.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/Backend/game_history/init_database.sh b/Backend/game_history/init_database.sh new file mode 100755 index 0000000..6f964f0 --- /dev/null +++ b/Backend/game_history/init_database.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Create necessary directories with appropriate permissions +cd / +mkdir -p /run/postgresql +chown postgres:postgres /run/postgresql/ + +# Initialize the database +initdb -D /var/lib/postgresql/data + +# Switch to the postgres user and run the following commands +mkdir -p /var/lib/postgresql/data +initdb -D /var/lib/postgresql/data + +# Append configurations to pg_hba.conf and postgresql.conf as the postgres user +echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf +echo "listen_addresses='*'" >> /var/lib/postgresql/data/postgresql.conf + +# Remove the unix_socket_directories line from postgresql.conf as the postgres user +sed -i "/^unix_socket_directories = /d" /var/lib/postgresql/data/postgresql.conf + +# Ensure the postgres user owns the data directory +chown -R postgres:postgres /var/lib/postgresql/data + +# Start the PostgreSQL server as the postgres user, keeping it in the foreground +exec postgres -D /var/lib/postgresql/data & + +# Wait for PostgreSQL to start (you may need to adjust the sleep time) +sleep 5 + +# # Create a new PostgreSQL user and set the password +psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" +# psql -c "GRANT ALL PRIVILEGES ON SCHEMA public TO ${DB_USER};" +# psql -c "GRANT ALL PRIVILEGES ON DATABASE postgres TO ${DB_USER};" + +# # Create the database named test_game_history_db. +# psql -c "CREATE DATABASE test_game_history_db;" +# psql -c "GRANT ALL PRIVILEGES ON DATABASE test_game_history_db TO ${DB_USER};" + +# Grant all necessary privileges to the user +psql -c "ALTER USER ${DB_USER} CREATEDB;" +psql -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ${DB_USER};" + +# Create the database named test_game_history_db. +psql -c "CREATE DATABASE test_game_history_db OWNER ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON DATABASE test_game_history_db TO ${DB_USER};" + + +# Run Django migrations +cd /app +source venv/bin/activate +python manage.py migrate + +# Stop the PostgreSQL server after setting the password +pg_ctl stop -D /var/lib/postgresql/data + +sleep 5 + +# Start the PostgreSQL server as the postgres user, keeping it in the foreground +pg_ctl start -D /var/lib/postgresql/data diff --git a/Backend/game_history/requirements.txt b/Backend/game_history/requirements.txt new file mode 100755 index 0000000..a2c5fde --- /dev/null +++ b/Backend/game_history/requirements.txt @@ -0,0 +1,24 @@ +-i https://pypi.org/simple +asgiref==3.8.1; python_version >= '3.8' +django==5.0.6; python_version >= '3.10' +django-cors-headers==4.3.1; python_version >= '3.8' +django-mysql==4.13.0; python_version >= '3.8' +djangorestframework==3.15.1; python_version >= '3.6' +djangorestframework-simplejwt; python_version >= '3.6' +pgsql; python_version >= '3.7' +djangorestframework-simplejwt[crypto]; python_version >= '3.6' +exceptiongroup==1.2.1; python_version <= '3.12' +iniconfig==2.0.0; python_version >= '3.7' +packaging==24.0; python_version >= '3.7' +pika==1.3.2; python_version >= '3.7' +pluggy==1.5.0; python_version >= '3.8' +psycopg2-binary==2.9.9; python_version >= '3.7' +pytest==8.2.1; python_version >= '3.8' +sqlparse==0.5.0; python_version >= '3.8' +tomli==2.0.1; python_version <= '3.12' +typing-extensions==4.12.1; python_version <= '3.12' +# gunicorn==20.1.0; python_version <= '3.12' +pytest-django>=4.4.0 +django-environ + +daphne diff --git a/Backend/game_history/supervisord.conf b/Backend/game_history/supervisord.conf new file mode 100755 index 0000000..b69d57a --- /dev/null +++ b/Backend/game_history/supervisord.conf @@ -0,0 +1,21 @@ +[supervisord] +nodaemon=true + +[program:django] +command=/app/venv/bin/python /app/game_history/manage.py runserver 0.0.0.0:8000 +user=postgres +autostart=true +autorestart=true +stderr_logfile=/var/log/django.err.log +stdout_logfile=/var/log/django.out.log +user=postgres + + +[program:init_database] +command=/bin/bash /app/init_database.sh +user=postgres +autostart=true +autorestart=true +startsecs=0 +stdout_logfile=/var/log/init_database.log +stderr_logfile=/var/log/init_database.err diff --git a/Backend/game_history/tools.sh b/Backend/game_history/tools.sh new file mode 100755 index 0000000..1aba6e2 --- /dev/null +++ b/Backend/game_history/tools.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Initialize the database +sh /app/init_database.sh + +# Activate the virtual environment +source venv/bin/activate +pip install -r requirements.txt +pip install tzdata + +# Wait for PostgreSQL to be available +while ! pg_isready -q -U "${DB_USER}" -d "postgres"; do + echo >&2 "Postgres is unavailable - sleeping" + sleep 5 +done + +# Export Django settings and PYTHONPATH +export DJANGO_SETTINGS_MODULE=game_history.settings +export PYTHONPATH=/app + +# Debugging steps +echo "PYTHONPATH: $PYTHONPATH" +echo "Contents of /app:" +ls /app +echo "Contents of /app/game_history:" +ls /app/game_history + +# Apply Django migrations +python3 /app/game_history/manage.py makemigrations +python3 /app/game_history/manage.py migrate + +# Run pytest with explicit PYTHONPATH +PYTHONPATH=/app pytest -vv + +# Start the Django application +cd /app/game_history +daphne -b 0.0.0.0 -p 8002 game_history.asgi:application diff --git a/Backend/game_stats_service/Dockerfile b/Backend/game_stats_service/Dockerfile new file mode 100644 index 0000000..b8c2f2f --- /dev/null +++ b/Backend/game_stats_service/Dockerfile @@ -0,0 +1,49 @@ +FROM alpine:3.20 + +ENV PYTHONUNBUFFERED=1 +ENV LANG=C.UTF-8 + +# Update and install dependencies + +RUN apk update && apk add --no-cache python3 py3-pip \ + postgresql16 postgresql16-client \ + bash supervisor curl openssl bash \ + build-base libffi-dev python3-dev + +# Set work directory +RUN mkdir /run/postgresql && \ + chown postgres:postgres /run/postgresql && \ + mkdir /app && chown -R postgres:postgres /app + +WORKDIR /app/ + +# Install Python virtual environment +RUN python3 -m venv venv && chown -R postgres:postgres venv + +# Copy application code and adjust permissions +COPY --chown=postgres:postgres ./Backend/game_stats_service/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY --chown=postgres:postgres ./Backend/game_stats_service/requirements.txt . +COPY --chown=postgres:postgres ./Backend/game_stats_service/game_stats_service /app/game_stats_service +COPY --chown=postgres:postgres ./Backend/game_stats_service/tools.sh /app +COPY --chown=postgres:postgres ./Backend/game_stats_service/init_database.sh /app + +# Install Python packages +RUN . venv/bin/activate && pip install -r requirements.txt + +# Create log files and set permissions +RUN touch /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err && \ + chown -R postgres:postgres /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err + +# Ensure supervisord and scripts are executable and owned by postgres +RUN chown -R postgres:postgres /app && \ + chown -R postgres:postgres /var/log && \ + chown -R postgres:postgres /app/venv && \ + chown -R postgres:postgres /app/game_stats_service + +USER postgres + +HEALTHCHECK --interval=30s --timeout=2s --start-period=5s --retries=3 CMD curl -sSf http://localhost:8003/game-stats/ > /dev/null && echo "success" || echo "failure" + +ENTRYPOINT ["sh", "./tools.sh"] + + diff --git a/Backend/game_stats_service/README.md b/Backend/game_stats_service/README.md new file mode 100644 index 0000000..7b0ea93 --- /dev/null +++ b/Backend/game_stats_service/README.md @@ -0,0 +1,112 @@ +## Game Stats Service Microservice Documentation + +### Overview + +The Game Stats Service microservice is a crucial part of the Transcendence project at 42 School. This service is designed to handle the recording and management of game statistics for ping pong games played by users. It includes capabilities for creating, retrieving, updating, and deleting game statistics records. + +### Directory Structure + +``` +. +├── Dockerfile +├── game_stats_service +│ ├── game_stats +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── __init__.py +│ │ ├── migrations +│ │ │ └── __init__.py +│ │ ├── models.py +│ │ ├── serializers.py +│ │ ├── tests.py +│ │ ├── urls.py +│ │ └── views.py +│ ├── game_stats_service +│ │ ├── asgi.py +│ │ ├── __init__.py +│ │ ├── settings.py +│ │ ├── tests +│ │ │ ├── conftest.py +│ │ │ ├── __init__.py +│ │ │ └── test_game_stats_service.py +│ │ ├── urls.py +│ │ └── wsgi.py +│ └── manage.py +├── init_database.sh +├── requirements.txt +├── supervisord.conf +└── tools.sh +``` + +### Setup + +#### Docker Setup + +The `game_stats_service` microservice is containerized using Docker. The `Dockerfile` sets up the environment required to run the Django application. + +### Models + +The `GameHistory` model represents a record of a game, including details like players, winner, and game timing. + +### Serializers + +The `GameHistorySerializer` converts model instances to JSON format and validates incoming data. + +### Views + +The `GameHistoryViewSet` manages CRUD operations for game history records. + +### URLs + +The microservice defines several endpoints to interact with game history data, specified in the `game_stats/urls.py` file. + +- **List and Create Game History Records:** + ``` + GET /game-history/ + POST /game-history/ + ``` +- **Retrieve, Update, and Delete a Specific Game History Record:** + ``` + GET /game-history// + PUT /game-history// + DELETE /game-history// + ``` + +### Tests + +#### Directory Structure + +The tests for the Game Stats Service microservice are located in the `game_stats_service/game_stats_service/tests/` directory. These tests verify the correct functioning of CRUD operations. + +#### Test Cases + +1. **Test Create Game History** +2. **Test List Game Histories** +3. **Test Retrieve Game History** +4. **Test Update Game History** +5. **Test Delete Game History** +6. **Test Create Game History Validation Error** + +### Running Tests + +To run the tests, follow these steps: + +1. Ensure the Docker containers are up and running: + ```sh + docker-compose up --build + ``` + +2. Access the `game-stats-service` container: + ```sh + docker exec -it game-stats-service bash + ``` + +3. Activate the virtual environment and run the tests: + ```sh + . venv/bin/activate + pytest + ``` + +### Conclusion + +The Game Stats Service microservice is an essential component of the Transcendence project, offering robust functionality for managing game statistics. This documentation provides an overview of the setup, implementation, and testing of the service. For further details, refer to the respective source code files in the project directory. diff --git a/Backend/game_stats_service/game_stats_service/game_stats/__init__.py b/Backend/game_stats_service/game_stats_service/game_stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_stats_service/game_stats_service/game_stats/admin.py b/Backend/game_stats_service/game_stats_service/game_stats/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Backend/game_stats_service/game_stats_service/game_stats/apps.py b/Backend/game_stats_service/game_stats_service/game_stats/apps.py new file mode 100644 index 0000000..1d3853c --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GameStatsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'game_stats' diff --git a/Backend/game_stats_service/game_stats_service/game_stats/migrations/__init__.py b/Backend/game_stats_service/game_stats_service/game_stats/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_stats_service/game_stats_service/game_stats/models.py b/Backend/game_stats_service/game_stats_service/game_stats/models.py new file mode 100644 index 0000000..c89ce9f --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/models.py @@ -0,0 +1,11 @@ +from django.db import models + +class game_dataGameStats(models.Model): + game_id = models.AutoField(primary_key=True) + player1_score = models.IntegerField() + player2_score = models.IntegerField() + total_hits = models.IntegerField() + longest_rally = models.IntegerField() + + def __str__(self): + return f"Game {self.game_id}: {self.player1_score} vs {self.player2_score}" diff --git a/Backend/game_stats_service/game_stats_service/game_stats/serializers.py b/Backend/game_stats_service/game_stats_service/game_stats/serializers.py new file mode 100644 index 0000000..1762763 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from .models import game_dataGameStats + +class GameStatsSerializer(serializers.ModelSerializer): + class Meta: + model = game_dataGameStats + fields = '__all__' diff --git a/Backend/game_stats_service/game_stats_service/game_stats/tests.py b/Backend/game_stats_service/game_stats_service/game_stats/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Backend/game_stats_service/game_stats_service/game_stats/urls.py b/Backend/game_stats_service/game_stats_service/game_stats/urls.py new file mode 100644 index 0000000..b6c3d29 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/urls.py @@ -0,0 +1,26 @@ +from django.urls import path +from .views import GameStatsViewSet + +urlpatterns = [ + path( + "game-stats/", + GameStatsViewSet.as_view( + { + "get": "list", + "post": "create" + } + ), + name="gamestats-list", + ), + path( + "game-stats//", + GameStatsViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "delete": "destroy", + } + ), + name="gamestats-detail", + ), +] diff --git a/Backend/game_stats_service/game_stats_service/game_stats/views.py b/Backend/game_stats_service/game_stats_service/game_stats/views.py new file mode 100644 index 0000000..ac697cb --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/views.py @@ -0,0 +1,41 @@ +from rest_framework import viewsets, status +from .models import game_dataGameStats +from .serializers import GameStatsSerializer +from rest_framework.response import Response +from django.shortcuts import get_object_or_404 +from rest_framework.request import Request + +class GameStatsViewSet(viewsets.ModelViewSet): + queryset = game_dataGameStats.objects.all() + serializer_class = GameStatsSerializer + + def create(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + self.perform_create(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def list(self, request: Request, *args: tuple, **kwargs: dict) -> Response: + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def retrieve(self, request: Request, pk=None, *args: tuple, **kwargs: dict) -> Response: + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance) + return Response(serializer.data, status=status.HTTP_200_OK) + + def update(self, request: Request, pk=None, *args: tuple, **kwargs: dict) -> Response: + partial = kwargs.pop('partial', False) + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance, data=request.data, partial=partial) + if serializer.is_valid(): + self.perform_update(serializer) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request: Request, pk=None, *args: tuple, **kwargs: dict) -> Response: + instance = get_object_or_404(self.get_queryset(), pk=pk) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/__init__.py b/Backend/game_stats_service/game_stats_service/game_stats_service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/asgi.py b/Backend/game_stats_service/game_stats_service/game_stats_service/asgi.py new file mode 100644 index 0000000..51b8c15 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for game_stats_service project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_stats_service.settings') + +application = get_asgi_application() diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/settings.py b/Backend/game_stats_service/game_stats_service/game_stats_service/settings.py new file mode 100644 index 0000000..75cb2ed --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/settings.py @@ -0,0 +1,159 @@ +""" +Django settings for game_stats_service project. + +Generated by 'django-admin startproject' using Django 5.0.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +from pathlib import Path +from datetime import timedelta +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-u7!$h!m^swv%n2dtm+(ccqjo6q+j2mpqoi@^o=dq#24=*jef42' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [ + 'localhost', + '127.0.0.1', + '[::1]', + 'game-stats-service', + 'game-stats-service:8003', + 'testserver', +] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'game_stats', + 'rest_framework', + 'rest_framework_simplejwt', + 'corsheaders', +] + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": False, + "BLACKLIST_AFTER_ROTATION": True, + "AUTH_HEADER_TYPES": ("Bearer",), + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), +} + +# Add REST framework settings for JWT authentication +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), +} + + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', # Add this line to the MIDDLEWARE list to enable CORS, Cross-Origin Resource Sharing is a mechanism that allows many resources (e.g., fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain from which the resource originated. + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'game_stats_service.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'game_stats_service.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'postgres', + 'USER': 'root', + 'PASSWORD': 'root', + 'PORT': '5432', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/tests/__init__.py b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py new file mode 100755 index 0000000..a314d57 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py @@ -0,0 +1,110 @@ +import os +import django +from django.conf import settings +import pytest +from datetime import timedelta +from django.core.management import call_command + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_stats_service.settings') + +if not settings.configured: + settings.configure( + DEBUG=True, + DATABASES={ + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "test_game_stats_db", + "USER": "root", + "PASSWORD": "root", + "PORT": "5432", + "HOST": "localhost", + "ATOMIC_REQUESTS": True, + } + }, + INSTALLED_APPS=[ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + 'game_stats', + ], + MIDDLEWARE=[ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ], + ROOT_URLCONF='game_stats_service.urls', + TEMPLATES=[ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, + ], + WSGI_APPLICATION='game_history.wsgi.application', + SECRET_KEY='django-insecure-woftd2en2**zr(b%#*2vit2v%s@(k54gb^c(ots0abo7(wsmo%', + ALLOWED_HOSTS = [ + 'localhost', + '127.0.0.1', + '[::1]', + 'game-stats-service', + 'game-stats-service:8003', + ], + RABBITMQ_HOST='localhost', + RABBITMQ_USER='user', + RABBITMQ_PASS='pass', + RABBITMQ_PORT='5672', + REST_FRAMEWORK={ + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + }, + SIMPLE_JWT={ + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_OBTAIN_SERIALIZER': 'user_auth.serializers.CustomTokenObtainPairSerializer', + }, + LANGUAGE_CODE='en-us', + TIME_ZONE='UTC', + USE_I18N=True, + USE_L10N=True, + USE_TZ=True, + STATIC_URL='/static/', + DEFAULT_AUTO_FIELD='django.db.models.BigAutoField', + CORS_ORIGIN_ALLOW_ALL=True, + ) + +django.setup() + +@pytest.fixture(scope='session', autouse=True) +def django_db_setup(): + settings.DATABASES['default'] = { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'test_game_stats_db', + 'USER': 'postgres', + 'PASSWORD': 'postgres', + 'HOST': 'localhost', + 'PORT': '5432', + 'ATOMIC_REQUESTS': True, + } + call_command('migrate') diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/tests/test_game_stats_service.py b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/test_game_stats_service.py new file mode 100644 index 0000000..f52e7c5 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/test_game_stats_service.py @@ -0,0 +1,68 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient +from game_stats.models import game_dataGameStats + +@pytest.fixture # This decorator is used to create fixtures, which are reusable components that can be used in multiple test functions +def api_client(): + return APIClient() + +@pytest.mark.django_db # This decorator is used to tell pytest to use a Django test database +def test_create_game_stat(api_client): + url = reverse('gamestats-list') + data = { + 'player1_score': 10, + 'player2_score': 8, + 'total_hits': 50, + 'longest_rally': 12 + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + assert game_dataGameStats.objects.count() == 1 + assert response.data['player1_score'] == 10 + assert response.data['player2_score'] == 8 + +@pytest.mark.django_db +def test_list_game_stats(api_client): + game1 = game_dataGameStats.objects.create(player1_score=10, player2_score=5, total_hits=40, longest_rally=15) + game2 = game_dataGameStats.objects.create(player1_score=20, player2_score=10, total_hits=80, longest_rally=20) + + url = reverse('gamestats-list') + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 # Check if the response contains two game stats objects in the list because we created two game stats objects + +@pytest.mark.django_db +def test_retrieve_game_stat(api_client): + game = game_dataGameStats.objects.create(player1_score=15, player2_score=7, total_hits=60, longest_rally=18) + + url = reverse('gamestats-detail', kwargs={'pk': game.game_id}) + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['player1_score'] == 15 + +@pytest.mark.django_db +def test_update_game_stat(api_client): + game = game_dataGameStats.objects.create(player1_score=10, player2_score=5, total_hits=40, longest_rally=15) + + url = reverse('gamestats-detail', kwargs={'pk': game.game_id}) + data = { + 'player1_score': 20, + 'player2_score': 15, + 'total_hits': 85, + 'longest_rally': 25 + } + response = api_client.put(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + game.refresh_from_db() + assert game.player1_score == 20 + +@pytest.mark.django_db +def test_delete_game_stat(api_client): + game = game_dataGameStats.objects.create(player1_score=10, player2_score=5, total_hits=40, longest_rally=15) # Create a game stats object + + url = reverse('gamestats-detail', kwargs={'pk': game.game_id}) # Get the URL for the game stats object + response = api_client.delete(url) # Send a DELETE request to the URL + assert response.status_code == status.HTTP_204_NO_CONTENT # Check if the response status code is 204 (No Content) + assert game_dataGameStats.objects.count() == 0 # Check if the game stats object has been deleted from the database diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py b/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py new file mode 100644 index 0000000..a9a98ec --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for game_stats_service project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('game_stats.urls')), +] diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/wsgi.py b/Backend/game_stats_service/game_stats_service/game_stats_service/wsgi.py new file mode 100644 index 0000000..981e094 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for game_stats_service project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_stats_service.settings') + +application = get_wsgi_application() diff --git a/Backend/game_stats_service/game_stats_service/manage.py b/Backend/game_stats_service/game_stats_service/manage.py new file mode 100755 index 0000000..b054a38 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_stats_service.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/Backend/game_stats_service/init_database.sh b/Backend/game_stats_service/init_database.sh new file mode 100644 index 0000000..be77565 --- /dev/null +++ b/Backend/game_stats_service/init_database.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Create necessary directories with appropriate permissions +cd / +mkdir -p /run/postgresql +chown postgres:postgres /run/postgresql/ + +# Initialize the database +initdb -D /var/lib/postgresql/data + +# Switch to the postgres user and run the following commands +mkdir -p /var/lib/postgresql/data +initdb -D /var/lib/postgresql/data # This command initializes the database cluster in the specified directory. + +# Append configurations to pg_hba.conf and postgresql.conf as the postgres user +echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf # This command appends the specified line to the pg_hba.conf file. +echo "listen_addresses='*'" >> /var/lib/postgresql/data/postgresql.conf # This command appends the specified line to the postgresql.conf file. + +# Remove the unix_socket_directories line from postgresql.conf as the postgres user +sed -i "/^unix_socket_directories = /d" /var/lib/postgresql/data/postgresql.conf # Because the unix_socket_directories line is not needed in this context, this command removes it from the postgresql.conf file. + +# Ensure the postgres user owns the data directory +chown -R postgres:postgres /var/lib/postgresql/data + +# Start the PostgreSQL server as the postgres user, keeping it in the foreground +exec postgres -D /var/lib/postgresql/data & + +# Wait for PostgreSQL to start (you may need to adjust the sleep time) +sleep 5 + +# Create a new PostgreSQL user and set the password +psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" +# psql -c "GRANT ALL PRIVILEGES ON SCHEMA public TO ${DB_USER};" +# psql -c "GRANT ALL PRIVILEGES ON DATABASE postgres TO ${DB_USER};" + +# Grant all necessary privileges to the user +psql -c "ALTER USER ${DB_USER} CREATEDB;" +psql -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ${DB_USER};" + +# Create the database named test_game_stats_db. +psql -c "CREATE DATABASE test_game_stats_db OWNER ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON DATABASE test_game_stats_db TO ${DB_USER};" + +# Run Django migrations +cd /app +source venv/bin/activate +python manage.py migrate + +# Stop the PostgreSQL server after setting the password +pg_ctl stop -D /var/lib/postgresql/data + +sleep 5 + +# Start the PostgreSQL server as the postgres user, keeping it in the foreground +pg_ctl start -D /var/lib/postgresql/data diff --git a/Backend/game_stats_service/requirements.txt b/Backend/game_stats_service/requirements.txt new file mode 100644 index 0000000..5330c9d --- /dev/null +++ b/Backend/game_stats_service/requirements.txt @@ -0,0 +1,25 @@ +-i https://pypi.org/simple +asgiref==3.8.1; python_version >= '3.8' +django==5.0.6; python_version >= '3.10' +django-cors-headers==4.3.1; python_version >= '3.8' +django-mysql==4.13.0; python_version >= '3.8' +djangorestframework==3.15.1; python_version >= '3.6' +djangorestframework-simplejwt; python_version >= '3.6' +pgsql; python_version >= '3.7' +djangorestframework-simplejwt[crypto]; python_version >= '3.6' +exceptiongroup==1.2.1; python_version <= '3.12' +iniconfig==2.0.0; python_version >= '3.7' +packaging==24.0; python_version >= '3.7' +pika==1.3.2; python_version >= '3.7' +pluggy==1.5.0; python_version >= '3.8' +psycopg2-binary==2.9.9; python_version >= '3.7' +pytest==8.2.1; python_version >= '3.8' +sqlparse==0.5.0; python_version >= '3.8' +tomli==2.0.1; python_version <= '3.12' +typing-extensions==4.12.1; python_version <= '3.12' +django-stubs==1.9.0; python_version >= '3.8' +# gunicorn==20.1.0; python_version <= '3.12' +pytest-django>=4.4.0 +django-environ + +daphne diff --git a/Backend/game_stats_service/supervisord.conf b/Backend/game_stats_service/supervisord.conf new file mode 100644 index 0000000..159792c --- /dev/null +++ b/Backend/game_stats_service/supervisord.conf @@ -0,0 +1,20 @@ +[supervisord] +nodaemon=true + +[program:django] +command=/app/venv/bin/python /app/game_stats_service/manage.py runserver +user=postgres +autostart=true +autorestart=true +stderr_logfile=/var/log/django.err.log +stdout_logfile=/var/log/django.out.log +user=postgres + +[program:init_database] +command=/bin/bash /app/init_database.sh +user=postgres +autostart=true +autorestart=true +startsecs=0 +stdout_logfile=/var/log/init_database.log +stderr_logfile=/var/log/init_database.err diff --git a/Backend/game_stats_service/tools.sh b/Backend/game_stats_service/tools.sh new file mode 100644 index 0000000..34c5660 --- /dev/null +++ b/Backend/game_stats_service/tools.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Initialize the database +sh /app/init_database.sh + +# Activate the virtual environment +source venv/bin/activate +pip install -r requirements.txt +pip install tzdata + +# Wait for PostgreSQL to be available +while ! pg_isready -q -U "${DB_USER}" -d "postgres"; do + echo >&2 "Postgres is unavailable - sleeping" + sleep 5 +done + +# Export Django settings and PYTHONPATH +export DJANGO_SETTINGS_MODULE=game_stats_service.settings +export PYTHONPATH=/app + +# Debugging steps +echo "PYTHONPATH: $PYTHONPATH" +echo "Contents of /app:" +ls /app +echo "Contents of /app/game_stats_service:" +ls /app/game_stats_service + +# Apply Django migrations +python3 /app/game_stats_service/manage.py makemigrations +python3 /app/game_stats_service/manage.py migrate + +# Run pytest with explicit PYTHONPATH +PYTHONPATH=/app pytest -vv + +# Start the Django application +cd /app/game_stats_service +daphne -b 0.0.0.0 -p 8003 game_stats_service.asgi:application diff --git a/Backend/profile_service/profile_service/user_session/rabbitmq_utils.py b/Backend/profile_service/profile_service/user_session/rabbitmq_utils.py index 68ba21d..56b8c24 100644 --- a/Backend/profile_service/profile_service/user_session/rabbitmq_utils.py +++ b/Backend/profile_service/profile_service/user_session/rabbitmq_utils.py @@ -16,8 +16,7 @@ def get_connection(cls): def _create_connection(cls): credentials = pika.PlainCredentials(settings.RABBITMQ_USER, settings.RABBITMQ_PASS) parameters = pika.ConnectionParameters( - settings.RABBITMQ_HOST, settings.RABBITMQ_PORT, "/", credentials - ) + settings.RABBITMQ_HOST, settings.RABBITMQ_PORT, "/", credentials=credentials) return pika.BlockingConnection(parameters) diff --git a/Backend/profile_service/requirements.txt b/Backend/profile_service/requirements.txt index fe1b436..0302f9c 100644 --- a/Backend/profile_service/requirements.txt +++ b/Backend/profile_service/requirements.txt @@ -17,8 +17,8 @@ pytest==8.2.1; python_version >= '3.8' sqlparse==0.5.0; python_version >= '3.8' tomli==2.0.1; python_version <= '3.12' typing-extensions==4.12.1; python_version <= '3.12' -gunicorn==20.1.0; python_version <= '3.12' +# gunicorn==20.1.0; python_version <= '3.12' django-environ daphne -pytest -pytest-django \ No newline at end of file +pytest-django +postgresql \ No newline at end of file diff --git a/Backend/user_service/requirements.txt b/Backend/user_service/requirements.txt index b880d53..90390e8 100644 --- a/Backend/user_service/requirements.txt +++ b/Backend/user_service/requirements.txt @@ -22,4 +22,4 @@ django-environ daphne pytest pytest-django -pytest-mock \ No newline at end of file +pytest-mock diff --git a/Backend/user_service/user_service/user_app/views.py b/Backend/user_service/user_service/user_app/views.py index c1236a0..473b3be 100644 --- a/Backend/user_service/user_service/user_app/views.py +++ b/Backend/user_service/user_service/user_app/views.py @@ -25,7 +25,7 @@ class UserViewSet(viewsets.ViewSet): Attributes: authentication_classes: The list of authentication classes to use for the view. permission_classes: The list of permission classes to use for the view. - + Methods: users_list: Method to get the list of users. retrieve_user: Method to retrieve a user. @@ -33,7 +33,7 @@ class UserViewSet(viewsets.ViewSet): destroy_user: Method to delete a user. handle_rabbitmq_request: Method to handle the RabbitMQ request. start_consumer: Method to start the RabbitMQ consumer. - + """ authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] @@ -41,9 +41,9 @@ class UserViewSet(viewsets.ViewSet): def users_list(self, request) -> Response: """ Method to get the list of users. - + This method gets the list of users from the database and returns the list of users. - + Args: request: The request object. @@ -70,7 +70,7 @@ def retrieve_user(self, request, pk=None) -> Response: Args: request: The request object. pk: The primary key of the user. - + Returns: Response: The response object containing the user data. """ @@ -85,13 +85,13 @@ def retrieve_user(self, request, pk=None) -> Response: def update_user(self, request, pk=None) -> Response: """ Method to update a user. - + This method updates a user in the database using the user id and the data in the request. - + Args: request: The request object containing the user data. pk: The primary key of the user. - + Returns: Response: The response object containing the updated user data. """ @@ -111,13 +111,13 @@ def update_user(self, request, pk=None) -> Response: def destroy_user(self, request, pk=None) -> Response: """ Method to delete a user. - + This method deletes a user from the database using the user id. - + Args: request: The request object. pk: The primary key of the user. - + Returns: Response: The response object containing the status of the deletion. """ @@ -161,18 +161,18 @@ def handle_response(ch, method, properties, body): def handle_login_request(ch, method, properties, body) -> None: """ Method to handle the RabbitMQ request. - + This method handles the RabbitMQ request by authenticating the user and sending the response message. - + Args: ch: The channel object. method: The method object. properties: The properties object. body: The body of the message. - + Returns: None - + """ payload = json.loads(body) username = payload.get("username") @@ -200,7 +200,7 @@ def start_consumer(self) -> None: class RegisterViewSet(viewsets.ViewSet): """ RegisterViewSet class to handle user registration. - + This class inherits from ViewSet. It defines the method to handle user registration. Attributes: @@ -219,7 +219,6 @@ def create_user(self, request) -> Response: Args: request: The request object containing the user data. - Returns: Response: The response object containing the user data. """ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7021f85 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM alpine:3.20 + +# Install Python and pip +RUN apk add --no-cache python3 py3-pip && \ + python3 -m ensurepip && \ + ln -sf python3 /usr/bin/python + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY ./requirements.txt /requirements.txt +RUN python -m pip install --upgrade pip +RUN pip install setuptools==58.0.4 wheel +RUN pip --timeout=1000 install -r /requirements.txt + +WORKDIR /app + +COPY . /app + +RUN addgroup -S www-data && adduser -S www-data -G www-data && \ + chown -R www-data:www-data /app + +USER www-data + +EXPOSE 8000 + +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/Dockerfile.lint b/Dockerfile.lint new file mode 100644 index 0000000..ee26f45 --- /dev/null +++ b/Dockerfile.lint @@ -0,0 +1,25 @@ +FROM alpine:3.20 + +# Install Python and pip +RUN apk add --no-cache python3 py3-pip && \ + python3 -m ensurepip && \ + ln -sf python3 /usr/bin/python + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY ./requirements.txt /requirements.txt +RUN python -m pip install --upgrade pip +RUN pip install setuptools==58.0.4 wheel +RUN pip install flake8 + +WORKDIR /app + +COPY . /app + +RUN addgroup -S www-data && adduser -S www-data -G www-data && \ + chown -R www-data:www-data /app + +USER www-data + +CMD ["flake8", "."] diff --git a/Dockerfile.publish b/Dockerfile.publish new file mode 100644 index 0000000..7bc9f01 --- /dev/null +++ b/Dockerfile.publish @@ -0,0 +1,27 @@ +FROM alpine:3.20 + +# Install Python and pip +RUN apk add --no-cache python3 py3-pip && \ + python3 -m ensurepip && \ + ln -sf python3 /usr/bin/python + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY ./requirements.txt /requirements.txt +RUN python -m pip install --upgrade pip +RUN pip install setuptools==58.0.4 wheel +RUN pip install -r /requirements.txt + +WORKDIR /app + +COPY . /app + +RUN addgroup -S www-data && adduser -S www-data -G www-data && \ + chown -R www-data:www-data /app + +USER www-data + +EXPOSE 8000 + +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..68ecf80 --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,25 @@ +FROM alpine:3.20 + +# Install Python and pip +RUN apk add --no-cache python3 py3-pip && \ + python3 -m ensurepip && \ + ln -sf python3 /usr/bin/python + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY ./requirements.txt /requirements.txt +RUN python -m pip install --upgrade pip +RUN pip install setuptools==58.0.4 wheel +RUN pip install -r /requirements.txt + +WORKDIR /app + +COPY . /app + +RUN addgroup -S www-data && adduser -S www-data -G www-data && \ + chown -R www-data:www-data /app + +USER www-data + +CMD ["pytest", "--maxfail=1", "--disable-warnings"] diff --git a/Frontend/nginx.conf b/Frontend/nginx.conf index 941a386..6429d80 100644 --- a/Frontend/nginx.conf +++ b/Frontend/nginx.conf @@ -28,6 +28,9 @@ http { server profile-service:8004; } + upstream game-history { + server game-history:8002; + } server { listen 80; server_name localhost; @@ -77,5 +80,13 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } + + location /game-history/ { + proxy_pass http://game-history; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } } } diff --git a/Makefile b/Makefile index 695d433..b1a9b80 100644 --- a/Makefile +++ b/Makefile @@ -40,19 +40,6 @@ clean: docker volume prune -f docker network prune -f -# Free up the port if it's already allocated -# .PHONY: free-port -# free-port: -# @echo "Checking for allocated port 15672..." -# @PIDS=$$(lsof -ti:15672 || netstat -nlp | grep :15672 | awk '{print $$7}' | cut -d'/' -f1 || ss -tuln | grep :15672 | awk '{print $$6}' | cut -d',' -f2); \ -# if [ -n "$$PIDS" ]; then \ -# echo "Port 15672 is in use by PIDs $$PIDS. Attempting to free it..."; \ -# echo "$$PIDS" | xargs kill -9; \ -# echo "Port 15672 has been freed."; \ -# else \ -# echo "Port 15672 is not in use."; \ -# fi - # Display the status of all services .PHONY: status status: @@ -72,6 +59,5 @@ help: @echo " logs - Show logs for all services" @echo " pull - Pull latest images for all services" @echo " clean - Remove stopped containers and unused images, networks, and volumes" - @echo " free-port - Free up the port if it's already allocated" @echo " status - Display the status of all services" @echo " help - Display this help message" diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 index 07ee486..4db41b2 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,8 +28,8 @@ services: dockerfile: Backend/profile_service/Dockerfile env_file: - .env - # ports: - # - 8001:8001 + ports: + - 8001:8001 networks: - transcendence_network depends_on: @@ -60,8 +60,36 @@ services: dockerfile: Backend/token_service/Dockerfile env_file: - .env - # ports: - # - 8000:8000 + ports: + - 8000:8000 + networks: + - transcendence_network + depends_on: + - rabbitmq + + game-history: + container_name: game-history + image: game-history + build: + context: . + dockerfile: Backend/game_history/Dockerfile + env_file: + - .env + ports: + - 8002:8002 + networks: + - transcendence_network + + game-stats-service: + container_name: game-stats-service + image: game-stats-service + build: + context: . + dockerfile: Backend/game_stats_service/Dockerfile + env_file: + - .env + ports: + - 8003:8003 networks: - transcendence_network depends_on: diff --git a/pytest.ini b/pytest.ini new file mode 100755 index 0000000..022f0a4 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +DJANGO_SETTINGS_MODULE = + game_history.settings + game_stats_service.settings +python_files = + test_game_history.py + test_game_stats_service.py diff --git a/requirements.txt b/requirements.txt index 71b61c8..0db0bdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,35 +1,36 @@ -# # # Application dependencies -# Django==4.0.6 -# djangorestframework==3.12.4 -# fastapi==0.75.1 -# uvicorn==0.17.0 -# mysqlclient==2.0.3 -# requests==2.26.0 +# Application dependencies +Django==4.0.6 +djangorestframework==3.12.4 +fastapi==0.75.1 +uvicorn==0.17.0 +mysqlclient==2.0.3 +requests==2.26.0 -# # # Testing dependencies -# pytest==7.1.2 +# Testing dependencies +pytest==7.1.2 -# # # Security dependencies -# bandit==1.7.0 +# Security dependencies +bandit==1.7.0 -# # # Other dependencies -# docker==5.0.3 -# Pillow==9.0.0 -# django-redis==5.0.0 -# asgiref==3.5.0 -# channels==3.0.4 -# idna==3.3 -# pytz==2021.3 -# python-dateutil==2.8.2 -# simplejson==3.17.5 -# urllib3==1.26.8 -# sqlparse==0.4.2 -# PyYAML==6.0 -# typing_extensions==4.0.1 -# django-rest-swagger==2.2.0 -# django-rest-knox==4.1.0 -# django-rest-auth==0.9.5 -# django-allauth==0.47.0 +# Other dependencies +docker==5.0.3 +Pillow==9.0.0 +django-redis==5.0.0 +asgiref==3.5.0 +channels==3.0.4 +idna==3.3 +pytz==2021.3 +python-dateutil==2.8.2 +simplejson==3.17.5 +urllib3==1.26.8 +sqlparse==0.4.2 +PyYAML==6.0 +typing_extensions==4.0.1 +django-rest-swagger==2.2.0 +django-rest-knox==4.1.0 +django-rest-auth==0.9.5 +django-allauth==0.47.0 -# # # Build dependencies -# setuptools==58.0.4 +# Build dependencies +setuptools==58.0.4 +wheel==0.37.0 diff --git a/sample env b/sample env index cc44359..11308ae 100644 --- a/sample env +++ b/sample env @@ -9,4 +9,4 @@ RABBITMQ_USER="user" RABBITMQ_PASS="pass" RABBITMQ_PORT="5672" -PGPASSWORD='root' \ No newline at end of file +PGPASSWORD='root'