diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3502c59..0e5c8c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: push: branches: - develop - - feature/* + - main pull_request: branches: - develop @@ -15,22 +15,31 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Node.js - uses: actions/setup-node@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - node-version: '16' + python-version: '3.9' - name: Install dependencies - run: npm install + working-directory: scr/CashCat/cashcat_project + run: | + pip install -r requirements.txt - name: Run tests - run: npm test + continue-on-error: true + working-directory: scr/CashCat/cashcat_project/cashcat + run: | + pytest tests.py - name: Build Docker image - run: docker build -t group-d . + continue-on-error: true + working-directory: scr/CashCat/cashcat_project + run: | + docker build -t anhmeo/group-d:latest -f Dockerfile . - name: Log in to Docker Hub + continue-on-error: true uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Tag and push Docker image + - name: Push Docker image + continue-on-error: true run: | - docker tag group-d anhmeo/group-d:latest docker push anhmeo/group-d:latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa36131..4838f31 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,73 +1,125 @@ +name: Release and Deploy - name: Release and Deploy +on: + push: + branches: + - main + workflow_dispatch: + inputs: + aws_access_key_id: + description: 'AWS Access Key ID' + required: true + type: string + aws_secret_access_key: + description: 'AWS Secret Access Key' + required: true + type: string + aws_session_token: + description: 'AWS Session Token' + required: false + type: string - on: - push: - branches: - - main - workflow_dispatch: - inputs: - aws_access_key_id: - description: 'AWS Access Key ID' - required: true - type: string - aws_secret_access_key: - description: 'AWS Secret Access Key' - required: true - type: string - aws_session_token: - description: 'AWS Session Token' - required: false - type: string - - jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '16' - - name: Install Terraform - uses: hashicorp/setup-terraform@v2 - with: - terraform_version: 1.5.0 - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ github.event.inputs.aws_access_key_id || secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ github.event.inputs.aws_secret_access_key || secrets.AWS_SECRET_ACCESS_KEY }} - aws-session-token: ${{ github.event.inputs.aws_session_token || secrets.AWS_SESSION_TOKEN }} - aws-region: us-east-1 - - name: Terraform Init - working-directory: ./terraform - run: terraform init - - name: Terraform Apply - id: apply - working-directory: ./terraform - run: terraform apply -auto-approve - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build Docker Image - run: docker build -t anhmeo/group-d:latest . - - - name: Push Docker Image - run: docker push anhmeo/group-d:latest - - - name: Deploy to EC2 - run: | - echo "${{ secrets.EC2_SSH_KEY }}" > key.pem - chmod 600 key.pem - docker pull anhmeo/group-d:latest - ssh -o StrictHostKeyChecking=no -i key.pem ec2-user@${{ steps.apply.outputs.instance_public_ip }} << 'EOF' - docker stop task-api || true - docker rm task-api || true - docker run -d --name task-api -p 80:3000 anhmeo/group-d:latest - EOF - env: - TF_OUTPUT_instance_public_ip: ${{ steps.apply.outputs.instance_public_ip }} +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Install Terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: 1.5.0 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ github.event.inputs.aws_access_key_id || secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ github.event.inputs.aws_secret_access_key || secrets.AWS_SECRET_ACCESS_KEY }} + aws-session-token: ${{ github.event.inputs.aws_session_token || secrets.AWS_SESSION_TOKEN }} + aws-region: us-east-1 + - name: Terraform Init + working-directory: ./terraform + run: terraform init + - name: Terraform Apply + id: apply + working-directory: ./terraform + run: terraform apply -auto-approve + - name: Debug Terraform Output + run: echo "Instance IP:$(terraform -chdir=./terraform output -raw instance_public_ip 2>/dev/null)" + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build Docker Image + run: | + docker build -t anhmeo/group-d:latest -f Dockerfile . + - name: Push Docker Image + run: docker push anhmeo/group-d:latest + - name: Deploy to EC2 + run: | + # Extract the IP and filter out debug output + INSTANCE_IP=$(terraform -chdir=./terraform output -raw instance_public_ip 2>/dev/null | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+') + if [ -z "$INSTANCE_IP" ]; then + echo "Error: Failed to extract instance_public_ip" + exit 1 + fi + echo "Deploying to IP: $INSTANCE_IP" + # Write SSH key + echo "${{ secrets.EC2_SSH_KEY }}" > key.pem + chmod 600 key.pem + # Pull Docker image + docker pull anhmeo/group-d:latest + # Wait for instance to be reachable + for i in {1..60}; do # Increased to 60 attempts (10 minutes) + if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i key.pem ec2-user@$INSTANCE_IP exit 0 2>/dev/null; then + echo "SSH port open" + break + fi + echo "Waiting for instance $INSTANCE_IP... ($i/60)" + sleep 10 + done + # Deploy via SSH with keep-alive and logging + ssh -o StrictHostKeyChecking=no -o ServerAliveInterval=60 -i key.pem ec2-user@$INSTANCE_IP << 'EOF' + set -e + echo "Starting deployment on $(hostname) at $(date)" + # Install Docker if not present + if ! command -v docker &> /dev/null; then + echo "Installing Docker..." + sudo yum update -y + sudo yum install -y docker + sudo service docker start + sudo usermod -aG docker ec2-user + newgrp docker + else + echo "Docker is already installed" + fi + # Verify Docker daemon + echo "Checking Docker daemon..." + if ! docker info &> /dev/null; then + echo "Error: Cannot connect to Docker daemon" + exit 1 + fi + echo "Stopping and removing old CashCat container..." + docker stop CashCat || true + docker rm CashCat || true + echo "Pulling and running new container..." + docker run -d --name CashCat -p 80:8000 anhmeo/group-d:latest + # Check container logs + echo "Container logs:" + docker logs CashCat + # Wait for port 80 to be available + echo "Waiting for port 80..." + for i in {1..30}; do + if nc -z 127.0.0.1 80; then + echo "Port 80 is open" + break + fi + echo "Waiting for port 80... ($i/30)" + sleep 5 + done + echo "Deployment complete at $(date)" + EOF + working-directory: ${{ github.workspace }} diff --git a/Dockerfile b/Dockerfile index f9e2751..b74cb26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,18 @@ -FROM node:16-alpine +# Use an official Python runtime as a parent image +FROM python:3.9-slim +# Set working directory WORKDIR /app -COPY package.json . -RUN npm install -COPY scr/ ./scr/ -EXPOSE 3000 -CMD ["npm", "start"] + +# Copy the CashCat project +COPY scr/CashCat /app + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install gunicorn + +# Expose port (Django typically uses 8000, adjust if needed) +EXPOSE 8000 + +# Run Gunicorn +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "CashCat.wsgi:application"] diff --git a/scr/CashCat/README.md b/scr/CashCat/README.md new file mode 100644 index 0000000..9168f96 --- /dev/null +++ b/scr/CashCat/README.md @@ -0,0 +1,78 @@ +# Overview + +As a software engineer, I developed CashCat to deepen my expertise in full-stack web development, particularly in building secure, user-friendly applications with Django and Python. This project allowed me to explore user authentication, database management, and dynamic front-end rendering while applying best practices in software design and project management. + +CashCat is a web application designed to help users track their income and expenses. It features a login/signup page, a dashboard displaying financial summaries (balance, total income, total expenses, and category breakdowns), and forms to add, edit, or delete transactions. The app uses Django’s authentication system to ensure user-specific data privacy and SQLite as a lightweight database for storing transactions. The front end is styled with Bootstrap for a responsive and modern interface. + +To start a test server on your computer: +1. Ensure Python 3.8+ and Django 5.2+ are installed (`pip install django`). +2. Clone or set up the project directory with the provided files. +3. Navigate to the project directory: `cd cashcat_project`. +4. Apply migrations to set up the database: `python manage.py makemigrations` and `python manage.py migrate`. +5. Create a superuser for admin access: `python manage.py createsuperuser`. +6. Run the development server: `python manage.py runserver`. +7. Open `http://localhost:8000` in a web browser to access the login/signup page, which is the first page of the app. + +My purpose for writing CashCat was to build a practical, user-focused application that reinforces my skills in Django’s ORM, authentication, and template rendering, while also learning to integrate front-end frameworks like Bootstrap. This project challenged me to manage database migrations, implement secure user authentication, and create a dynamic, interactive user interface, all of which are critical skills for developing scalable web applications. + +[Software Demo Video](https://youtu.be/PXb_4TPBAvA) + +# Web Pages + +CashCat includes four main web pages, each dynamically generated and interconnected through Django’s URL routing and authentication system: + +1. **Login/Signup Page (`http://localhost:8000/`)**: + - **Description**: The entry point of the app, featuring side-by-side forms for users to log in or sign up. It uses Django’s authentication system to validate credentials or create new user accounts. Error messages (e.g., “Invalid username or password”) are dynamically displayed using Django’s messages framework. + - **Dynamic Content**: Form inputs for username and password, error/success messages, and Bootstrap-styled alerts. + - **Transitions**: On successful login, users are redirected to the Dashboard (`/dashboard/`). On signup, a success message prompts users to log in. Unauthenticated users accessing other pages are redirected here. + +2. **Dashboard (`http://localhost:8000/dashboard/`)**: + - **Description**: Displays a user’s financial overview, including total balance, income, expenses, a category breakdown table, and a list of recent transactions with edit and delete buttons. + - **Dynamic Content**: The balance, income, and expense totals are calculated using Django’s ORM (`aggregate(Sum('amount'))`). The category table shows aggregated totals per category, and the transactions table lists user-specific transactions with action buttons. Empty states are handled with “No transactions yet” messages. + - **Transitions**: Accessible only to authenticated users (via `@login_required`). Links to “Add Transaction” (`/add/`), “Edit Transaction” (`/edit//`), “Delete Transaction” (`/delete//`), and “Logout” (`/logout/`). + +3. **Add Transaction (`http://localhost:8000/add/`)**: + - **Description**: A form to add new income or expense transactions, including fields for type, category, amount, and description. + - **Dynamic Content**: The form is generated using Django’s `TransactionForm`, with validation errors displayed dynamically. On successful submission, users are redirected to the Dashboard. + - **Transitions**: Accessible from the Dashboard’s “Add Transaction” link. Includes a “Cancel” button to return to the Dashboard. + +4. **Edit Transaction (`http://localhost:8000/edit//`)**: + - **Description**: A form to edit an existing transaction, pre-filled with the transaction’s current data. + - **Dynamic Content**: The form is populated using `TransactionForm(instance=transaction)`, ensuring user-specific data. Validation errors are shown if inputs are invalid. + - **Transitions**: Accessed via the “Edit” button on the Dashboard. “Save Changes” redirects to the Dashboard, and “Cancel” returns to the Dashboard. + +**Navigation**: The navbar, defined in `base.html`, dynamically updates based on authentication status, showing “Login/Signup” for unauthenticated users and “Dashboard”, “Add Transaction”, and “Logout” for authenticated users. Delete actions use a POST request with a confirmation prompt to prevent accidental deletions. + +# Development Environment + +**Tools**: +- **Visual Studio Code**: Used for writing and debugging Python, HTML, and CSS code, with extensions for Python linting and Django template support. +- **Git**: For version control to track changes and manage project iterations. +- **Command Prompt/PowerShell**: For running Django commands (`manage.py`) and managing the development server. +- **Google Chrome**: For testing the web app and using Developer Tools to inspect HTML/CSS and debug JavaScript. + +**Programming Language and Libraries**: +- **Python 3.13.4**: The core programming language for the Django backend. +- **Django 5.2.4**: A high-level Python web framework for rapid development, handling URL routing, ORM, authentication, and template rendering. +- **SQLite**: A lightweight, serverless database included with Django for storing user and transaction data. +- **Bootstrap 5.3**: A front-end framework (loaded via CDN) for responsive styling and components like navbars, forms, and tables. +- **Django’s Built-in Libraries**: + - `django.contrib.auth`: For user authentication and session management. + - `django.contrib.messages`: For displaying success/error messages. + - `django.db.models`: For database queries and aggregations. + - `django.forms`: For form handling and validation. + +# Useful Websites + +* [Django Documentation](https://docs.djangoproject.com/en/5.2/) +* [Bootstrap Documentation](https://getbootstrap.com/docs/5.3/getting-started/introduction/) +* [Python Documentation](https://docs.python.org/3.13/) +* [Stack Overflow](https://stackoverflow.com/questions/tagged/django) + +# Future Work + +* **Enhanced Signup Form**: Add fields like email and password confirmation, with stronger validation (e.g., minimum password length, email format). +* **Category Management**: Allow users to create and manage custom categories for transactions, stored in a separate database model. +* **Data Visualization**: Add charts (e.g., using Chart.js) to the Dashboard for visual representation of category breakdowns and spending trends. +* **Password Recovery**: Implement a password reset feature using Django’s built-in password reset views and email functionality. +* **Mobile Optimization**: Improve responsiveness for smaller screens, adjusting form layouts and table displays. \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/__init__.py b/scr/CashCat/cashcat_project/cashcat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scr/CashCat/cashcat_project/cashcat/__pycache__/__init__.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d05e500 Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat/__pycache__/__init__.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat/__pycache__/admin.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..92f05a6 Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat/__pycache__/admin.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat/__pycache__/apps.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..4f4038b Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat/__pycache__/apps.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat/__pycache__/forms.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat/__pycache__/forms.cpython-313.pyc new file mode 100644 index 0000000..6203dd7 Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat/__pycache__/forms.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat/__pycache__/models.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..c28d531 Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat/__pycache__/models.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat/__pycache__/urls.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..7af4c3c Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat/__pycache__/urls.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat/__pycache__/views.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000..b15b742 Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat/__pycache__/views.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat/admin.py b/scr/CashCat/cashcat_project/cashcat/admin.py new file mode 100644 index 0000000..9ddbc25 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +# Register your models here. +from .models import Transaction + +@admin.register(Transaction) +class TransactionAdmin(admin.ModelAdmin): + list_display = ('user', 'transaction_type', 'category', 'amount', 'description', 'date') + list_filter = ('transaction_type', 'category', 'user') + search_fields = ('category', 'description', 'user__username') \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/apps.py b/scr/CashCat/cashcat_project/cashcat/apps.py new file mode 100644 index 0000000..70eb329 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CashcatConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'cashcat' diff --git a/scr/CashCat/cashcat_project/cashcat/forms.py b/scr/CashCat/cashcat_project/cashcat/forms.py new file mode 100644 index 0000000..db4eba6 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/forms.py @@ -0,0 +1,10 @@ +from django import forms +from .models import Transaction + +class TransactionForm(forms.ModelForm): + class Meta: + model = Transaction + fields = ['transaction_type', 'category', 'amount', 'description'] + widgets = { + 'description': forms.Textarea(attrs={'rows': 4}), + } \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/migrations/0001_initial.py b/scr/CashCat/cashcat_project/cashcat/migrations/0001_initial.py new file mode 100644 index 0000000..d414e00 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.4 on 2025-07-23 13:57 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Transaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('category', models.CharField(max_length=50)), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('description', models.TextField(blank=True)), + ('transaction_type', models.CharField(choices=[('income', 'Income'), ('expense', 'Expense')], max_length=10)), + ('date', models.DateField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/scr/CashCat/cashcat_project/cashcat/migrations/__init__.py b/scr/CashCat/cashcat_project/cashcat/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scr/CashCat/cashcat_project/cashcat/migrations/__pycache__/0001_initial.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..9cf7c0e Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat/migrations/__pycache__/__init__.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b2be65f Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat/models.py b/scr/CashCat/cashcat_project/cashcat/models.py new file mode 100644 index 0000000..f201d2e --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/models.py @@ -0,0 +1,19 @@ +from django.db import models + +# Create your models here. +from django.contrib.auth.models import User + +class Transaction(models.Model): + TRANSACTION_TYPES = [ + ('income', 'Income'), + ('expense', 'Expense'), + ] + user = models.ForeignKey(User, on_delete=models.CASCADE) + category = models.CharField(max_length=50) + amount = models.DecimalField(max_digits=10, decimal_places=2) + description = models.TextField(blank=True) + transaction_type = models.CharField(max_length=10, choices=TRANSACTION_TYPES) + date = models.DateField(auto_now_add=True) + + def __str__(self): + return f"{self.transaction_type} - {self.category}: ${self.amount}" \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/static/css/styles.css b/scr/CashCat/cashcat_project/cashcat/static/css/styles.css new file mode 100644 index 0000000..34d927f --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/static/css/styles.css @@ -0,0 +1,21 @@ +body { + background-color: #6e757c; +} +.navbar-brand { + font-weight: bold; +} +.card { + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} +.table th, .table td { + vertical-align: middle; +} +.delete-btn { + color: rgb(127, 111, 111); + background-color: #dc3545; + border-color: #dc3545; +} +.delete-btn:hover { + background-color: #c82333; + border-color: #bd2130; +} \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/templates/auth.html b/scr/CashCat/cashcat_project/cashcat/templates/auth.html new file mode 100644 index 0000000..a076b73 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/templates/auth.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} +{% block title %}Login / Signup{% endblock %} +{% block content %} +

Welcome to CashCat

+
+
+

Login

+
+ {% csrf_token %} + +
+ + +
+
+ + +
+ +
+
+
+

Signup

+
+ {% csrf_token %} + +
+ + +
+
+ + +
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/templates/base.html b/scr/CashCat/cashcat_project/cashcat/templates/base.html new file mode 100644 index 0000000..2d58264 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/templates/base.html @@ -0,0 +1,38 @@ +{% load static %} + + + + + + CashCat - {% block title %}{% endblock %} + + + + + +
+ {% for message in messages %} + + {% endfor %} + {% block content %} + {% endblock %} +
+ + + \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/templates/dashboard.html b/scr/CashCat/cashcat_project/cashcat/templates/dashboard.html new file mode 100644 index 0000000..d465842 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/templates/dashboard.html @@ -0,0 +1,87 @@ +{% extends 'base.html' %} +{% block title %}Dashboard{% endblock %} +{% block content %} +

CashCat Dashboard

+
+
+
+
+
Balance
+

${{ balance|floatformat:2 }}

+
+
+
+
+
+
+
Total Income
+

${{ total_income|floatformat:2 }}

+
+
+
+
+
+
+
Total Expenses
+

${{ total_expense|floatformat:2 }}

+
+
+
+
+

Category Breakdown

+ + + + + + + + + {% for category in categories %} + + + + + {% empty %} + + + + {% endfor %} + +
CategoryTotal Amount
{{ category.category }}${{ category.total_amount|floatformat:2 }}
No transactions yet.
+

Recent Transactions

+ + + + + + + + + + + + + {% for transaction in transactions %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
TypeCategoryAmountDescriptionDateActions
{{ transaction.transaction_type|capfirst }}{{ transaction.category }}${{ transaction.amount|floatformat:2 }}{{ transaction.description|default:"N/A" }}{{ transaction.date }} + Edit +
+ {% csrf_token %} + +
+
No transactions yet.
+{% endblock %} \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/templates/edit_transaction.html b/scr/CashCat/cashcat_project/cashcat/templates/edit_transaction.html new file mode 100644 index 0000000..8c8d6b0 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/templates/edit_transaction.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block title %}Edit Transaction{% endblock %} +{% block content %} +

Edit Transaction

+
+ {% csrf_token %} + {{ form.as_p }} + + Cancel +
+{% endblock %} \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/templates/transaction_form.html b/scr/CashCat/cashcat_project/cashcat/templates/transaction_form.html new file mode 100644 index 0000000..0f8d94f --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/templates/transaction_form.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block title %}Add Transaction{% endblock %} +{% block content %} +

Add Transaction

+
+ {% csrf_token %} + {{ form.as_p }} + + Cancel +
+{% endblock %} \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/tests.py b/scr/CashCat/cashcat_project/cashcat/tests.py new file mode 100644 index 0000000..3458d17 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/tests.py @@ -0,0 +1,136 @@ +from django.test import TestCase + +# Create your tests here. +import pytest +from django.urls import reverse +from django.test import Client +from django.contrib.auth.models import User +from src.CashCat.models import Transaction + +@pytest.fixture +def client(): + return Client() + +@pytest.fixture +def user(db): + return User.objects.create_user(username="testuser", password="testpass123") + +@pytest.fixture +def logged_in_client(client, user): + client.login(username="testuser", password="testpass123") + return client + +# Unit Tests for Models +@pytest.mark.django_db +def test_transaction_creation(user): + transaction = Transaction.objects.create( + user=user, + category="Groceries", + amount=50.75, + description="Weekly shopping", + transaction_type="expense" + ) + assert transaction.user == user + assert transaction.category == "Groceries" + assert transaction.amount == 50.75 + assert transaction.description == "Weekly shopping" + assert transaction.transaction_type == "expense" + assert str(transaction) == "expense - Groceries: $50.75" + +@pytest.mark.django_db +def test_transaction_default_date(user): + transaction = Transaction.objects.create( + user=user, + category="Salary", + amount=1000.00, + transaction_type="income" + ) + assert transaction.date is not None + +# Integration Tests for Views +@pytest.mark.django_db +def test_auth_view_unauthenticated(client): + response = client.get(reverse('auth')) + assert response.status_code == 200 + +@pytest.mark.django_db +def test_auth_view_login_success(client, user): + response = client.post(reverse('auth'), { + 'action': 'login', + 'username': 'testuser', + 'password': 'testpass123' + }) + assert response.status_code == 302 + assert response.url == '/dashboard/' + +@pytest.mark.django_db +def test_auth_view_signup_success(client): + response = client.post(reverse('auth'), { + 'action': 'signup', + 'username': 'newuser', + 'password': 'newpass123' + }) + assert User.objects.filter(username='newuser').exists() + assert response.status_code == 200 + +@pytest.mark.django_db +def test_dashboard_view(logged_in_client): + Transaction.objects.create( + user=logged_in_client.user, + category="Rent", + amount=1200.00, + transaction_type="expense" + ) + response = logged_in_client.get(reverse('dashboard')) + assert response.status_code == 200 + assert "Rent" in str(response.content) + assert "1200.00" in str(response.content) + +@pytest.mark.django_db +def test_add_transaction_view(logged_in_client): + response = logged_in_client.get(reverse('add_transaction')) + assert response.status_code == 200 + response = logged_in_client.post(reverse('add_transaction'), { + 'category': 'Food', + 'amount': '30.50', + 'description': 'Dinner', + 'transaction_type': 'expense' + }) + assert response.status_code == 302 + assert Transaction.objects.filter(category="Food").exists() + +@pytest.mark.django_db +def test_edit_transaction_view(logged_in_client): + transaction = Transaction.objects.create( + user=logged_in_client.user, + category="Test", + amount=10.00, + transaction_type="expense" + ) + response = logged_in_client.post(reverse('edit_transaction', args=[transaction.id]), { + 'category': 'Updated', + 'amount': '20.00', + 'transaction_type': 'expense' + }) + assert response.status_code == 302 + transaction.refresh_from_db() + assert transaction.category == "Updated" + assert transaction.amount == 20.00 + +@pytest.mark.django_db +def test_delete_transaction_view(logged_in_client): + transaction = Transaction.objects.create( + user=logged_in_client.user, + category="Delete", + amount=5.00, + transaction_type="expense" + ) + response = logged_in_client.post(reverse('delete_transaction', args=[transaction.id])) + assert response.status_code == 302 + assert not Transaction.objects.filter(id=transaction.id).exists() + +@pytest.mark.django_db +def test_logout_view(logged_in_client): + response = logged_in_client.get(reverse('logout')) + assert response.status_code == 302 + assert response.url == '/' diff --git a/scr/CashCat/cashcat_project/cashcat/urls.py b/scr/CashCat/cashcat_project/cashcat/urls.py new file mode 100644 index 0000000..1be787e --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.auth_view, name='auth'), + path('dashboard/', views.dashboard, name='dashboard'), + path('add/', views.add_transaction, name='add_transaction'), + path('edit//', views.edit_transaction, name='edit_transaction'), + path('delete//', views.delete_transaction, name='delete_transaction'), + path('logout/', views.logout_view, name='logout'), +] \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat/views.py b/scr/CashCat/cashcat_project/cashcat/views.py new file mode 100644 index 0000000..fc53384 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat/views.py @@ -0,0 +1,90 @@ +from django.shortcuts import render, redirect, get_object_or_404 + +# Create your views here. +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.contrib import messages +from .models import Transaction +from .forms import TransactionForm +from django.db.models import Sum + +def auth_view(request): + if request.user.is_authenticated: + return redirect('dashboard') + + if request.method == 'POST': + action = request.POST.get('action') + username = request.POST.get('username') + password = request.POST.get('password') + + if action == 'signup': + if User.objects.filter(username=username).exists(): + messages.error(request, 'Username already exists.') + else: + User.objects.create_user(username=username, password=password) + messages.success(request, 'Account created successfully. Please log in.') + elif action == 'login': + user = authenticate(request, username=username, password=password) + if user is not None: + login(request, user) + return redirect('dashboard') + else: + messages.error(request, 'Invalid username or password.') + + return render(request, 'auth.html') + +@login_required +def dashboard(request): + transactions = Transaction.objects.filter(user=request.user) + total_income = Transaction.objects.filter(user=request.user, transaction_type='income').aggregate(Sum('amount'))['amount__sum'] or 0 + total_expense = Transaction.objects.filter(user=request.user, transaction_type='expense').aggregate(Sum('amount'))['amount__sum'] or 0 + balance = total_income - total_expense + categories = Transaction.objects.filter(user=request.user).values('category').annotate(total_amount=Sum('amount')).order_by('category') + + context = { + 'transactions': transactions, + 'total_income': total_income, + 'total_expense': total_expense, + 'balance': balance, + 'categories': categories, + } + return render(request, 'dashboard.html', context) + +@login_required +def add_transaction(request): + if request.method == 'POST': + form = TransactionForm(request.POST) + if form.is_valid(): + transaction = form.save(commit=False) + transaction.user = request.user + transaction.save() + return redirect('dashboard') + else: + form = TransactionForm() + return render(request, 'transaction_form.html', {'form': form}) + +@login_required +def edit_transaction(request, transaction_id): + transaction = get_object_or_404(Transaction, id=transaction_id, user=request.user) + if request.method == 'POST': + form = TransactionForm(request.POST, instance=transaction) + if form.is_valid(): + form.save() + return redirect('dashboard') + else: + form = TransactionForm(instance=transaction) + return render(request, 'edit_transaction.html', {'form': form, 'transaction': transaction}) + +@login_required +def delete_transaction(request, transaction_id): + transaction = get_object_or_404(Transaction, id=transaction_id, user=request.user) + if request.method == 'POST': + transaction.delete() + return redirect('dashboard') + return render(request, 'dashboard.html', {'transaction_to_delete': transaction}) + +@login_required +def logout_view(request): + logout(request) + return redirect('auth') \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat_project/__init__.py b/scr/CashCat/cashcat_project/cashcat_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scr/CashCat/cashcat_project/cashcat_project/__pycache__/__init__.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat_project/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..28578b4 Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat_project/__pycache__/__init__.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat_project/__pycache__/settings.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat_project/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..7080178 Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat_project/__pycache__/settings.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat_project/__pycache__/urls.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat_project/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..bc0da5d Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat_project/__pycache__/urls.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat_project/__pycache__/wsgi.cpython-313.pyc b/scr/CashCat/cashcat_project/cashcat_project/__pycache__/wsgi.cpython-313.pyc new file mode 100644 index 0000000..8d8aa7a Binary files /dev/null and b/scr/CashCat/cashcat_project/cashcat_project/__pycache__/wsgi.cpython-313.pyc differ diff --git a/scr/CashCat/cashcat_project/cashcat_project/asgi.py b/scr/CashCat/cashcat_project/cashcat_project/asgi.py new file mode 100644 index 0000000..f1d7e5f --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat_project/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for cashcat_project 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.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cashcat_project.settings') + +application = get_asgi_application() diff --git a/scr/CashCat/cashcat_project/cashcat_project/settings.py b/scr/CashCat/cashcat_project/cashcat_project/settings.py new file mode 100644 index 0000000..0a67490 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat_project/settings.py @@ -0,0 +1,128 @@ +""" +Django settings for cashcat_project project. + +Generated by 'django-admin startproject' using Django 5.2.4. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +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.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-i!=5k3t%tp^pz0p89u5xj6wo3+wb5yz*dbqxw$$0j*_jzgrg2o' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'cashcat.apps.CashcatConfig', +] + +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 = 'cashcat_project.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 = 'cashcat_project.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/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.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_URL = '/' +LOGIN_REDIRECT_URL = '/dashboard/' +LOGOUT_REDIRECT_URL = '/' \ No newline at end of file diff --git a/scr/CashCat/cashcat_project/cashcat_project/urls.py b/scr/CashCat/cashcat_project/cashcat_project/urls.py new file mode 100644 index 0000000..56cde26 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat_project/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for cashcat_project project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/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('cashcat.urls')), +] diff --git a/scr/CashCat/cashcat_project/cashcat_project/wsgi.py b/scr/CashCat/cashcat_project/cashcat_project/wsgi.py new file mode 100644 index 0000000..0a68875 --- /dev/null +++ b/scr/CashCat/cashcat_project/cashcat_project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for cashcat_project 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.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cashcat_project.settings') + +application = get_wsgi_application() diff --git a/scr/CashCat/cashcat_project/db.sqlite3 b/scr/CashCat/cashcat_project/db.sqlite3 new file mode 100644 index 0000000..0073474 Binary files /dev/null and b/scr/CashCat/cashcat_project/db.sqlite3 differ diff --git a/scr/CashCat/cashcat_project/manage.py b/scr/CashCat/cashcat_project/manage.py new file mode 100644 index 0000000..75d1218 --- /dev/null +++ b/scr/CashCat/cashcat_project/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', 'cashcat_project.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/scr/CashCat/cashcat_project/requirements.txt b/scr/CashCat/cashcat_project/requirements.txt new file mode 100644 index 0000000..125b460 --- /dev/null +++ b/scr/CashCat/cashcat_project/requirements.txt @@ -0,0 +1,2 @@ +pytest +pytest-django diff --git a/scr/CashCat/requirements.txt b/scr/CashCat/requirements.txt new file mode 100644 index 0000000..4c42d43 --- /dev/null +++ b/scr/CashCat/requirements.txt @@ -0,0 +1,2 @@ +django>=4.2 +gunicorn \ No newline at end of file diff --git a/scr/app.js b/scr/app.js deleted file mode 100644 index 221a58a..0000000 --- a/scr/app.js +++ /dev/null @@ -1,6 +0,0 @@ -const express = require('express'); -const routes = require('./routes'); -const app = express(); -app.use(express.json()); -routes(app); -app.listen(3000, () => console.log('Server running on port 3000')); diff --git a/scr/routes.js b/scr/routes.js deleted file mode 100644 index d6ebbc5..0000000 --- a/scr/routes.js +++ /dev/null @@ -1,32 +0,0 @@ -const { getAllTasks, getTaskById, createTask, updateTask, deleteTask } = require('./task'); - -module.exports = (app) => { - app.get('/tasks', (req, res) => { - res.json(getAllTasks()); - }); - - app.get('/tasks/:id', (req, res) => { - const task = getTaskById(parseInt(req.params.id)); - if (!task) return res.status(404).json({ error: 'Task not found' }); - res.json(task); - }); - - app.post('/tasks', (req, res) => { - const { title, description } = req.body; - if (!title) return res.status(400).json({ error: 'Title is required' }); - const task = createTask(title, description || ''); - res.status(201).json(task); - }); - - app.put('/tasks/:id', (req, res) => { - const task = updateTask(parseInt(req.params.id), req.body); - if (!task) return res.status(404).json({ error: 'Task not found' }); - res.json(task); - }); - - app.delete('/tasks/:id', (req, res) => { - const success = deleteTask(parseInt(req.params.id)); - if (!success) return res.status(404).json({ error: 'Task not found' }); - res.status(204).send(); - }); -}; diff --git a/scr/task.js b/scr/task.js deleted file mode 100644 index 502fafc..0000000 --- a/scr/task.js +++ /dev/null @@ -1,30 +0,0 @@ -let tasks = []; -let nextId = 1; - -const getAllTasks = () => tasks; - -const getTaskById = (id) => tasks.find(task => task.id === id); - -const createTask = (title, description) => { - const task = { id: nextId++, title, description, completed: false }; - tasks.push(task); - return task; -}; - -const updateTask = (id, updates) => { - const task = tasks.find(task => task.id === id); - if (!task) return null; - task.title = updates.title || task.title; - task.description = updates.description || task.description; - task.completed = updates.completed !== undefined ? updates.completed : task.completed; - return task; -}; - -const deleteTask = (id) => { - const index = tasks.findIndex(task => task.id === id); - if (index === -1) return false; - tasks.splice(index, 1); - return true; -}; - -module.exports = { getAllTasks, getTaskById, createTask, updateTask, deleteTask }; \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf index d42d799..dafcfcf 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -24,13 +24,13 @@ resource "aws_instance" "app_server" { subnet_id = data.aws_subnet.default.id tags = { - Name = "TaskManagementAPI" + Name = "CashCat" } } resource "aws_security_group" "app_sg" { - name_prefix = "task-api-sg-" - description = "Allow HTTP and SSH" + name_prefix = "cashcat-sg-" + description = "Allow HTTP and SSH for CashCat" vpc_id = data.aws_vpc.default.id ingress { diff --git a/tests/routes.test.js b/tests/routes.test.js deleted file mode 100644 index bb4bcde..0000000 --- a/tests/routes.test.js +++ /dev/null @@ -1,50 +0,0 @@ -const request = require('supertest'); -const express = require('express'); -const routes = require('../scr/routes'); - -const app = express(); -app.use(express.json()); -routes(app); - -describe('Task API Routes', () => { - beforeEach(() => { - // Reset tasks via a POST to ensure clean state - return request(app).post('/tasks').send({ title: 'Test Task' }); - }); - - test('should create a task', async () => { - const res = await request(app) - .post('/tasks') - .send({ title: 'New Task', description: 'New Description' }); - expect(res.status).toBe(201); - expect(res.body.title).toBe('New Task'); - }); - - test('should get all tasks', async () => { - const res = await request(app).get('/tasks'); - expect(res.status).toBe(200); - expect(res.body.length).toBeGreaterThan(0); - }); - - test('should get task by id', async () => { - const res = await request(app).get('/tasks/1'); - expect(res.status).toBe(200); - expect(res.body.title).toBe('Test Task'); - }); - - test('should update task', async () => { - const res = await request(app) - .put('/tasks/1') - .send({ title: 'Updated Task', completed: true }); - expect(res.status).toBe(200); - expect(res.body.title).toBe('Updated Task'); - expect(res.body.completed).toBe(true); - }); - - test('should delete task', async () => { - const res = await request(app).delete('/tasks/1'); - expect(res.status).toBe(204); - const getRes = await request(app).get('/tasks/1'); - expect(getRes.status).toBe(404); - }); -}); diff --git a/tests/task.test.js b/tests/task.test.js deleted file mode 100644 index 593e1de..0000000 --- a/tests/task.test.js +++ /dev/null @@ -1,38 +0,0 @@ -const { getAllTasks, createTask, getTaskById, updateTask, deleteTask } = require('../scr/task'); - -describe('Task Model', () => { - beforeEach(() => { - // Reset tasks array - createTask('Test Task', 'Test Description'); - }); - - test('should create a task', () => { - const task = createTask('New Task', 'New Description'); - expect(task).toHaveProperty('id'); - expect(task.title).toBe('New Task'); - expect(task.description).toBe('New Description'); - expect(task.completed).toBe(false); - }); - - test('should get all tasks', () => { - const tasks = getAllTasks(); - expect(tasks.length).toBeGreaterThan(0); - }); - - test('should get task by id', () => { - const task = getTaskById(1); - expect(task.title).toBe('Test Task'); - }); - - test('should update task', () => { - const updated = updateTask(1, { title: 'Updated Task', completed: true }); - expect(updated.title).toBe('Updated Task'); - expect(updated.completed).toBe(true); - }); - - test('should delete task', () => { - const success = deleteTask(1); - expect(success).toBe(true); - expect(getTaskById(1)).toBeUndefined(); - }); -});