├── .gitignore ├── README.md ├── buildspec.yml ├── deploy.sh ├── docker-compose.prod.yml ├── docker-compose.yml ├── ecs ├── ecs_client_taskdefinition.json └── ecs_users_taskdefinition.json ├── services ├── client │ ├── .dockerignore │ ├── .env │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── Dockerfile │ ├── Dockerfile.prod │ ├── README.md │ ├── conf │ │ └── conf.d │ │ │ └── default.conf │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── components │ │ │ ├── About.tsx │ │ │ ├── AddUser.tsx │ │ │ ├── AddUserModal.tsx │ │ │ ├── LoginForm.tsx │ │ │ ├── Message.tsx │ │ │ ├── NavBar.tsx │ │ │ ├── RegisterForm.tsx │ │ │ ├── UserStatus.tsx │ │ │ └── Users.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── tests │ │ │ ├── components │ │ │ │ ├── About.test.tsx │ │ │ │ ├── AddUser.test.tsx │ │ │ │ ├── AddUserModal.test.tsx │ │ │ │ ├── LoginForm.test.tsx │ │ │ │ ├── Message.test.tsx │ │ │ │ ├── NavBar.test.tsx │ │ │ │ ├── RegisterForm.test.tsx │ │ │ │ ├── UserStatus.test.tsx │ │ │ │ ├── Users.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ ├── About.test.tsx.snap │ │ │ │ │ ├── AddUser.test.tsx.snap │ │ │ │ │ ├── LoginForm.test.tsx.snap │ │ │ │ │ ├── Message.test.tsx.snap │ │ │ │ │ ├── NavBar.test.tsx.snap │ │ │ │ │ ├── RegisterForm.test.tsx.snap │ │ │ │ │ └── UserStatus.test.tsx.snap │ │ │ ├── main.test.ts │ │ │ └── test-utils.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.ts ├── nginx │ └── default.conf └── users │ ├── .coveragerc │ ├── .dockerignore │ ├── Dockerfile │ ├── Dockerfile.prod │ ├── entrypoint.prod.sh │ ├── entrypoint.sh │ ├── manage.py │ ├── requirements-dev.txt │ ├── requirements.txt │ ├── setup.cfg │ └── src │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── auth.py │ ├── ping.py │ └── users │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── crud.py │ │ ├── models.py │ │ └── views.py │ ├── config.py │ ├── db │ ├── Dockerfile │ └── create.sql │ └── tests │ ├── __init__.py │ ├── conftest.py │ ├── pytest.ini │ ├── test_admin.py │ ├── test_auth.py │ ├── test_config.py │ ├── test_ping.py │ ├── test_user_model.py │ ├── test_users.py │ └── test_users_unit.py └── terraform ├── 01_provider.tf ├── 02_network.tf ├── 03_securitygroups.tf ├── 04_loadbalancer.tf ├── 05_iam.tf ├── 06_logs.tf ├── 07_keypair.tf ├── 08_ecs.tf ├── 09_rds.tf ├── outputs.tf ├── templates ├── client_app.json.tpl └── users_app.json.tpl └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | env 3 | .coverage 4 | htmlcov 5 | coverage 6 | node_modules 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploying a Flask and React Microservice to AWS ECS 2 | 3 | https://testdriven.io/courses/aws-flask-react/ 4 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | AWS_REGION: "us-west-1" 6 | VITE_API_SERVICE_URL: "http://" 7 | 8 | phases: 9 | pre_build: 10 | commands: 11 | - echo logging in to ecr... 12 | - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com 13 | - | 14 | if expr "$CODEBUILD_WEBHOOK_TRIGGER" == "branch/master" >/dev/null && expr "$CODEBUILD_WEBHOOK_HEAD_REF" == "refs/heads/master" >/dev/null; then 15 | DOCKER_TAG=prod 16 | else 17 | DOCKER_TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION} 18 | fi 19 | - echo "Docker tag $DOCKER_TAG" 20 | - echo "Current branch trigger $CODEBUILD_WEBHOOK_TRIGGER" 21 | - echo "Current HEAD ref $CODEBUILD_WEBHOOK_HEAD_REF" 22 | - docker pull $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-users:$DOCKER_TAG || true 23 | - docker pull $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-client:builder || true 24 | - docker pull $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-client:$DOCKER_TAG || true 25 | build: 26 | commands: 27 | - echo building and testing dev images... 28 | - docker-compose up -d --build 29 | - docker-compose exec -T api python -m pytest "src/tests" -p no:warnings --cov="src" 30 | - docker-compose exec -T api flake8 src 31 | - docker-compose exec -T api black src --check 32 | - docker-compose exec -T api isort src --check-only 33 | - docker-compose exec -T client npm run lint 34 | - docker-compose exec -T client npm run prettier:check 35 | - docker-compose exec -T client npm run prettier:write 36 | - echo building prod images... 37 | - docker build --cache-from $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-users:$DOCKER_TAG -f services/users/Dockerfile.prod -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-users:$DOCKER_TAG ./services/users 38 | - docker build --target builder --cache-from $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-client:builder -f services/client/Dockerfile.prod -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-client:builder --build-arg NODE_ENV=production --build-arg VITE_API_SERVICE_URL=$VITE_API_SERVICE_URL ./services/client 39 | - docker build --cache-from $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-client:$DOCKER_TAG -f services/client/Dockerfile.prod -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-client:$DOCKER_TAG ./services/client 40 | post_build: 41 | commands: 42 | - echo "Using docker tag $DOCKER_TAG" 43 | - echo pushing prod images to ecr... 44 | - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-users:$DOCKER_TAG 45 | - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-client:builder 46 | - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/test-driven-client:$DOCKER_TAG 47 | - chmod +x ./deploy.sh 48 | - bash deploy.sh 49 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | JQ="jq --raw-output --exit-status" 4 | 5 | configure_aws_cli() { 6 | aws --version 7 | aws configure set default.region us-west-1 8 | aws configure set default.output json 9 | echo "AWS Configured!" 10 | } 11 | 12 | register_definition() { 13 | if revision=$(aws ecs register-task-definition --cli-input-json "$task_def" | $JQ '.taskDefinition.taskDefinitionArn'); then 14 | echo "Revision: $revision" 15 | else 16 | echo "Failed to register task definition" 17 | return 1 18 | fi 19 | } 20 | 21 | update_service() { 22 | if [[ $(aws ecs update-service --cluster $cluster --service $service --task-definition $revision | $JQ '.service.taskDefinition') != $revision ]]; then 23 | echo "Error updating service." 24 | return 1 25 | fi 26 | } 27 | 28 | deploy_cluster() { 29 | 30 | cluster="flask-react-cluster" 31 | 32 | # users 33 | service="flask-react-users-service" 34 | template="ecs_users_taskdefinition.json" 35 | task_template=$(cat "ecs/$template") 36 | task_def=$(printf "$task_template" $AWS_ACCOUNT_ID $AWS_RDS_URI $PRODUCTION_SECRET_KEY) 37 | echo "$task_def" 38 | register_definition 39 | update_service 40 | 41 | # client 42 | service="flask-react-client-service" 43 | template="ecs_client_taskdefinition.json" 44 | task_template=$(cat "ecs/$template") 45 | task_def=$(printf "$task_template" $AWS_ACCOUNT_ID) 46 | echo "$task_def" 47 | register_definition 48 | update_service 49 | 50 | } 51 | 52 | echo $CODEBUILD_WEBHOOK_BASE_REF 53 | echo $CODEBUILD_WEBHOOK_HEAD_REF 54 | echo $CODEBUILD_WEBHOOK_TRIGGER 55 | echo $CODEBUILD_WEBHOOK_EVENT 56 | 57 | if [ "$CODEBUILD_WEBHOOK_TRIGGER" == "branch/master" ] && \ 58 | [ "$CODEBUILD_WEBHOOK_HEAD_REF" == "refs/heads/master" ] 59 | then 60 | echo "Updating ECS." 61 | configure_aws_cli 62 | deploy_cluster 63 | fi 64 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | api: 4 | build: 5 | context: ./services/users 6 | dockerfile: Dockerfile.prod 7 | ports: 8 | - 5004:5000 9 | environment: 10 | - FLASK_ENV=production 11 | - APP_SETTINGS=src.config.ProductionConfig 12 | - DATABASE_URL=postgres://postgres:postgres@api-db:5432/api_prod 13 | - DATABASE_TEST_URL=postgres://postgres:postgres@api-db:5432/api_test 14 | - SECRET_KEY=my_precious 15 | depends_on: 16 | - api-db 17 | 18 | api-db: 19 | build: 20 | context: ./services/users/src/db 21 | dockerfile: Dockerfile 22 | expose: 23 | - 5432 24 | environment: 25 | - POSTGRES_USER=postgres 26 | - POSTGRES_PASSWORD=postgres 27 | 28 | client: 29 | build: 30 | context: ./services/client 31 | dockerfile: Dockerfile.prod 32 | args: 33 | - NODE_ENV=production 34 | - VITE_API_SERVICE_URL=${VITE_API_SERVICE_URL} 35 | ports: 36 | - 3007:80 37 | depends_on: 38 | - api 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | api: 4 | build: 5 | context: ./services/users # updated 6 | dockerfile: Dockerfile 7 | entrypoint: ['/usr/src/app/entrypoint.sh'] 8 | volumes: 9 | - './services/users:/usr/src/app' # updated 10 | ports: 11 | - 5004:5000 12 | environment: 13 | - FLASK_DEBUG=1 14 | - FLASK_ENV=development 15 | - APP_SETTINGS=src.config.DevelopmentConfig 16 | - DATABASE_URL=postgresql://postgres:postgres@api-db:5432/api_dev 17 | - DATABASE_TEST_URL=postgresql://postgres:postgres@api-db:5432/api_test 18 | depends_on: 19 | - api-db 20 | 21 | api-db: 22 | build: 23 | context: ./services/users/src/db # updated 24 | dockerfile: Dockerfile 25 | expose: 26 | - 5432 27 | environment: 28 | - POSTGRES_USER=postgres 29 | - POSTGRES_PASSWORD=postgres 30 | 31 | client: 32 | stdin_open: true 33 | build: 34 | context: ./services/client 35 | dockerfile: Dockerfile 36 | volumes: 37 | - './services/client:/usr/src/app' 38 | - '/usr/src/app/node_modules' 39 | ports: 40 | - 3007:5173 41 | environment: 42 | - NODE_ENV=development 43 | - VITE_API_SERVICE_URL=${VITE_API_SERVICE_URL} 44 | depends_on: 45 | - api 46 | -------------------------------------------------------------------------------- /ecs/ecs_client_taskdefinition.json: -------------------------------------------------------------------------------- 1 | { 2 | "containerDefinitions": [ 3 | { 4 | "name": "client", 5 | "image": "%s.dkr.ecr.us-west-1.amazonaws.com/test-driven-client:prod", 6 | "essential": true, 7 | "memoryReservation": 300, 8 | "portMappings": [ 9 | { 10 | "hostPort": 0, 11 | "protocol": "tcp", 12 | "containerPort": 80 13 | } 14 | ], 15 | "logConfiguration": { 16 | "logDriver": "awslogs", 17 | "options": { 18 | "awslogs-group": "flask-react-client-log", 19 | "awslogs-region": "us-west-1" 20 | } 21 | } 22 | } 23 | ], 24 | "family": "flask-react-client-td" 25 | } 26 | -------------------------------------------------------------------------------- /ecs/ecs_users_taskdefinition.json: -------------------------------------------------------------------------------- 1 | { 2 | "containerDefinitions": [ 3 | { 4 | "name": "users", 5 | "image": "%s.dkr.ecr.us-west-1.amazonaws.com/test-driven-users:prod", 6 | "essential": true, 7 | "memoryReservation": 300, 8 | "portMappings": [ 9 | { 10 | "hostPort": 0, 11 | "protocol": "tcp", 12 | "containerPort": 5000 13 | } 14 | ], 15 | "environment": [ 16 | { 17 | "name": "APP_SETTINGS", 18 | "value": "src.config.ProductionConfig" 19 | }, 20 | { 21 | "name": "DATABASE_TEST_URL", 22 | "value": "postgres://postgres:postgres@api-db:5432/api_test" 23 | }, 24 | { 25 | "name": "DATABASE_URL", 26 | "value": "%s" 27 | }, 28 | { 29 | "name": "SECRET_KEY", 30 | "value": "%s" 31 | } 32 | ], 33 | "logConfiguration": { 34 | "logDriver": "awslogs", 35 | "options": { 36 | "awslogs-group": "flask-react-users-log", 37 | "awslogs-region": "us-west-1" 38 | } 39 | } 40 | } 41 | ], 42 | "family": "flask-react-users-td" 43 | } 44 | -------------------------------------------------------------------------------- /services/client/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | Dockerfile 5 | Dockerfile.prod 6 | -------------------------------------------------------------------------------- /services/client/.env: -------------------------------------------------------------------------------- 1 | VITE_API_SERVICE_URL=http://localhost:5004 2 | -------------------------------------------------------------------------------- /services/client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | 'plugin:react/recommended' // Add React rules 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parser: '@typescript-eslint/parser', 12 | plugins: ['react-refresh', 'react'], // Add React plugin 13 | rules: { 14 | 'react-refresh/only-export-components': [ 15 | 'warn', 16 | { allowConstantExport: true }, 17 | ], 18 | 'react/jsx-no-bind': ['error', { 19 | 'allowArrowFunctions': true, 20 | 'allowBind': false, 21 | 'ignoreRefs': true 22 | }], 23 | }, 24 | settings: { 25 | react: { 26 | version: '18.0', 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /services/client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /services/client/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM node:20.16.0 3 | 4 | # set working directory 5 | WORKDIR /usr/src/app 6 | 7 | # add `/usr/src/app/node_modules/.bin` to $PATH 8 | ENV PATH /usr/src/app/node_modules/.bin:$PATH 9 | 10 | # install and cache app dependencies 11 | COPY package.json . 12 | COPY package-lock.json . 13 | RUN npm ci 14 | 15 | # start app 16 | CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] 17 | -------------------------------------------------------------------------------- /services/client/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | ########### 2 | # BUILDER # 3 | ########### 4 | 5 | # pull official base image 6 | FROM node:20.16.0 AS builder 7 | 8 | # set working directory 9 | WORKDIR /usr/src/app 10 | 11 | # add `/usr/src/app/node_modules/.bin` to $PATH 12 | ENV PATH /usr/src/app/node_modules/.bin:$PATH 13 | 14 | # install and cache app dependencies 15 | COPY package.json . 16 | COPY package-lock.json . 17 | RUN npm ci 18 | 19 | # set environment variables 20 | ARG VITE_API_SERVICE_URL 21 | ENV VITE_API_SERVICE_URL=$VITE_API_SERVICE_URL 22 | ARG NODE_ENV 23 | ENV NODE_ENV=$NODE_ENV 24 | 25 | # create build 26 | COPY . . 27 | RUN vite build 28 | 29 | 30 | ######### 31 | # FINAL # 32 | ######### 33 | 34 | # base image 35 | FROM nginx:stable-alpine 36 | 37 | # update nginx conf 38 | RUN rm -rf /etc/nginx/conf.d 39 | COPY conf /etc/nginx 40 | 41 | # copy static files 42 | COPY --from=builder /usr/src/app/dist /usr/share/nginx/html 43 | 44 | # expose port 45 | EXPOSE 80 46 | 47 | # run nginx 48 | CMD ["nginx", "-g", "daemon off;"] 49 | -------------------------------------------------------------------------------- /services/client/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /services/client/conf/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | location / { 4 | root /usr/share/nginx/html; 5 | index index.html index.htm; 6 | try_files $uri $uri/ /index.html; 7 | } 8 | error_page 500 502 503 504 /50x.html; 9 | location = /50x.html { 10 | root /usr/share/nginx/html; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /services/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /services/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "test": "vitest", 12 | "test:ui": "vitest --ui", 13 | "coverage": "vitest run --coverage", 14 | "prettier:check": "prettier --check 'src/**/*.{js,jsx,json,ts,tsx}'", 15 | "prettier:write": "prettier --write 'src/**/*.{js,jsx,json,ts,tsx}'" 16 | }, 17 | "dependencies": { 18 | "@chakra-ui/icons": "^2.1.1", 19 | "@chakra-ui/react": "^2.8.2", 20 | "@emotion/react": "^11.13.3", 21 | "@emotion/styled": "^11.13.0", 22 | "axios": "^1.7.4", 23 | "formik": "^2.4.6", 24 | "framer-motion": "^11.3.30", 25 | "react": "^18.3.1", 26 | "react-dom": "^18.3.1", 27 | "react-router-dom": "^6.26.1", 28 | "zod": "^3.23.8" 29 | }, 30 | "devDependencies": { 31 | "@testing-library/jest-dom": "^6.4.8", 32 | "@testing-library/react": "^16.0.0", 33 | "@types/react": "^18.3.3", 34 | "@types/react-dom": "^18.3.0", 35 | "@typescript-eslint/eslint-plugin": "^7.15.0", 36 | "@typescript-eslint/parser": "^7.15.0", 37 | "@vitejs/plugin-react": "^4.3.1", 38 | "@vitest/coverage-v8": "^2.1.5", 39 | "eslint": "^8.57.0", 40 | "eslint-plugin-react": "^7.35.0", 41 | "eslint-plugin-react-hooks": "^4.6.2", 42 | "eslint-plugin-react-refresh": "^0.4.7", 43 | "jsdom": "^24.1.1", 44 | "prettier": "^3.3.3", 45 | "typescript": "^5.2.2", 46 | "vite": "^5.3.4", 47 | "vitest": "^2.0.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /services/client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/client/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /services/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { ChakraProvider, useDisclosure } from "@chakra-ui/react"; 3 | import { Route, Routes } from "react-router-dom"; 4 | import axios from "axios"; 5 | import Users from "./components/Users"; 6 | import About from "./components/About"; 7 | import NavBar from "./components/NavBar"; 8 | import LoginForm from "./components/LoginForm"; 9 | import RegisterForm from "./components/RegisterForm"; 10 | import UserStatus from "./components/UserStatus"; 11 | import Message from "./components/Message"; 12 | import AddUserModal from "./components/AddUserModal"; 13 | 14 | interface User { 15 | created_date: string; 16 | email: string; 17 | id: number; 18 | username: string; 19 | } 20 | 21 | const App = () => { 22 | const [users, setUsers] = useState([]); 23 | const [title] = useState("TestDriven.io"); 24 | const [accessToken, setAccessToken] = useState(null); 25 | const [messageType, setMessageType] = useState< 26 | "info" | "warning" | "success" | "error" | null 27 | >(null); 28 | const [messageText, setMessageText] = useState(null); 29 | const { isOpen, onOpen, onClose } = useDisclosure(); 30 | 31 | useEffect(() => { 32 | const checkAuth = async () => { 33 | await validRefresh(); 34 | }; 35 | checkAuth(); 36 | // eslint-disable-next-line react-hooks/exhaustive-deps 37 | }, []); 38 | 39 | const isAuthenticated = () => { 40 | return !!accessToken; 41 | }; 42 | 43 | const fetchUsers = async () => { 44 | try { 45 | const response = await axios.get( 46 | `${import.meta.env.VITE_API_SERVICE_URL}/users`, 47 | ); 48 | if (response.status === 200) { 49 | setUsers(response.data); 50 | } 51 | } catch (error) { 52 | console.error("Error fetching users:", error); 53 | } 54 | }; 55 | 56 | const handleRegisterFormSubmit = async (data: { 57 | username: string; 58 | email: string; 59 | password: string; 60 | }) => { 61 | try { 62 | const url = `${import.meta.env.VITE_API_SERVICE_URL}/auth/register`; 63 | const response = await axios.post(url, data); 64 | console.log(response.data); 65 | createMessage("success", "Registration successful! You can now log in."); // Display success message 66 | } catch (err) { 67 | console.log(err); 68 | createMessage( 69 | "error", 70 | "Registration failed. The user might already exist.", 71 | ); // Display error message 72 | } 73 | }; 74 | 75 | const handleLoginFormSubmit = async (data: { 76 | email: string; 77 | password: string; 78 | }) => { 79 | try { 80 | const url = `${import.meta.env.VITE_API_SERVICE_URL}/auth/login`; 81 | const response = await axios.post(url, data); 82 | console.log(response.data); 83 | setAccessToken(response.data.access_token); 84 | window.localStorage.setItem("refreshToken", response.data.refresh_token); 85 | await fetchUsers(); 86 | createMessage("success", "Login successful!"); // Display success message 87 | } catch (err) { 88 | console.log(err); 89 | createMessage("error", "Login failed. Please check your credentials."); // Display error message 90 | } 91 | }; 92 | 93 | const validRefresh = async () => { 94 | const token = window.localStorage.getItem("refreshToken"); 95 | if (token) { 96 | try { 97 | const response = await axios.post( 98 | `${import.meta.env.VITE_API_SERVICE_URL}/auth/refresh`, 99 | { 100 | refresh_token: token, 101 | }, 102 | ); 103 | setAccessToken(response.data.access_token); 104 | await fetchUsers(); 105 | window.localStorage.setItem( 106 | "refreshToken", 107 | response.data.refresh_token, 108 | ); 109 | return true; 110 | } catch (err) { 111 | console.log(err); 112 | return false; 113 | } 114 | } 115 | return false; 116 | }; 117 | 118 | const logoutUser = () => { 119 | setAccessToken(null); 120 | window.localStorage.removeItem("refreshToken"); 121 | createMessage("info", "You have been logged out."); // Display success message 122 | }; 123 | 124 | const clearMessage = () => { 125 | setMessageType(null); 126 | setMessageText(null); 127 | }; 128 | 129 | const createMessage = ( 130 | type: "info" | "warning" | "success" | "error", 131 | text: string, 132 | ) => { 133 | setMessageType(type); 134 | setMessageText(text); 135 | setTimeout(() => { 136 | clearMessage(); 137 | }, 3000); 138 | }; 139 | 140 | // Add a test message when the component mounts 141 | useEffect(() => { 142 | createMessage("info", "Welcome to the application!"); 143 | // eslint-disable-next-line react-hooks/exhaustive-deps 144 | }, []); 145 | 146 | const addUser = async (userData: { 147 | username: string; 148 | email: string; 149 | password: string; 150 | }) => { 151 | try { 152 | const response = await axios.post( 153 | `${import.meta.env.VITE_API_SERVICE_URL}/users`, 154 | { 155 | username: userData.username, 156 | email: userData.email, 157 | password: userData.password, 158 | created_date: new Date().toISOString(), 159 | }, 160 | ); 161 | 162 | // Update the users state with the new user 163 | setUsers((prevUsers) => [...prevUsers, response.data]); 164 | createMessage("success", "User added successfully."); 165 | onClose(); // Close the modal 166 | await fetchUsers(); // Fetch the updated list of users 167 | } catch (err) { 168 | console.error(err); 169 | createMessage( 170 | "error", 171 | "Failed to add user. The user might already exist.", 172 | ); 173 | } 174 | }; 175 | 176 | const removeUser = async (userId: number) => { 177 | try { 178 | await axios.delete( 179 | `${import.meta.env.VITE_API_SERVICE_URL}/users/${userId}`, 180 | ); 181 | await fetchUsers(); // Fetch the updated list of users 182 | createMessage("success", "User removed successfully."); 183 | } catch (err) { 184 | console.error(err); 185 | createMessage("error", "Failed to remove user. Please try again."); 186 | } 187 | }; 188 | 189 | return ( 190 | 191 | 196 | {messageType && messageText && ( 197 | 202 | )} 203 | 204 | 208 | 214 | 219 | 220 | } 221 | /> 222 | } /> 223 | 230 | } 231 | /> 232 | 239 | } 240 | /> 241 | 248 | } 249 | /> 250 | 251 | 252 | ); 253 | }; 254 | 255 | export default App; 256 | -------------------------------------------------------------------------------- /services/client/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/client/src/components/About.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Heading, Text, Divider, VStack } from "@chakra-ui/react"; 3 | 4 | const About = () => ( 5 | 6 | 7 | 8 | About 9 | 10 | 11 | 12 | Add something relevant here. 13 | 14 | 15 | 16 | ); 17 | 18 | export default About; 19 | -------------------------------------------------------------------------------- /services/client/src/components/AddUser.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import axios from "axios"; 3 | import { Formik, Form, Field } from "formik"; 4 | import { z } from "zod"; 5 | import { 6 | Box, 7 | Button, 8 | FormControl, 9 | FormLabel, 10 | Input, 11 | Heading, 12 | } from "@chakra-ui/react"; 13 | import { FormErrorMessage } from "@chakra-ui/react"; 14 | 15 | interface UserObject { 16 | username: string; 17 | email: string; 18 | password?: string; 19 | created_date?: string; 20 | } 21 | 22 | interface AddUserProps { 23 | addUserToList: (user: UserObject) => void; 24 | } 25 | 26 | type FieldProps = { 27 | field: { 28 | name: string; 29 | value: string; 30 | onChange: (event: React.ChangeEvent) => void; 31 | onBlur: (event: React.FocusEvent) => void; 32 | }; 33 | }; 34 | 35 | const validationSchema = z.object({ 36 | username: z 37 | .string() 38 | .min(4, "Username must be at least 4 characters long") 39 | .regex( 40 | /^[a-zA-Z0-9_]+$/, 41 | "Username can only contain letters, numbers, and underscores", 42 | ), 43 | email: z.string().email("Enter a valid email"), 44 | password: z.string().min(6, "Password must be at least 6 characters long"), 45 | }); 46 | 47 | type FormValues = z.infer; 48 | 49 | const AddUser: React.FC = ({ addUserToList }) => { 50 | const handleSubmit = async ( 51 | values: FormValues, 52 | { 53 | setSubmitting, 54 | resetForm, 55 | }: { 56 | setSubmitting: (isSubmitting: boolean) => void; 57 | resetForm: () => void; 58 | }, 59 | ) => { 60 | try { 61 | const response = await axios.post( 62 | `${import.meta.env.VITE_API_SERVICE_URL}/users`, 63 | values, 64 | ); 65 | 66 | if (response.status === 201) { 67 | console.log(response.data.message); 68 | 69 | const newUser = { 70 | username: values.username, 71 | email: values.email, 72 | created_date: new Date().toISOString(), 73 | }; 74 | 75 | addUserToList(newUser); 76 | resetForm(); 77 | } 78 | } catch (error) { 79 | console.error("There was an error registering the user:", error); 80 | } finally { 81 | setSubmitting(false); 82 | } 83 | }; 84 | 85 | return ( 86 | 87 | 88 | 89 | Register a User 90 | 91 | 92 | 93 | { 96 | try { 97 | validationSchema.parse(values); 98 | return {}; 99 | } catch (error) { 100 | return (error as z.ZodError).formErrors.fieldErrors; 101 | } 102 | }} 103 | onSubmit={handleSubmit} 104 | > 105 | {({ errors, touched, isSubmitting }) => ( 106 |
107 | 115 | 116 | {({ field }: FieldProps) => ( 117 | 122 | 123 | Username 124 | 125 | 130 | {errors.username && touched.username && ( 131 | {errors.username} 132 | )} 133 | 134 | )} 135 | 136 | 137 | 138 | {({ field }: FieldProps) => ( 139 | 144 | 145 | Email 146 | 147 | 153 | {errors.email && touched.email && ( 154 | {errors.email} 155 | )} 156 | 157 | )} 158 | 159 | 160 | 161 | {({ field }: FieldProps) => ( 162 | 167 | 168 | Password 169 | 170 | 176 | {errors.password && touched.password && ( 177 | {errors.password} 178 | )} 179 | 180 | )} 181 | 182 | 183 | 193 | 194 |
195 | )} 196 |
197 |
198 | ); 199 | }; 200 | 201 | export default AddUser; 202 | -------------------------------------------------------------------------------- /services/client/src/components/AddUserModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Modal, 4 | ModalOverlay, 5 | ModalContent, 6 | ModalHeader, 7 | ModalFooter, 8 | ModalBody, 9 | ModalCloseButton, 10 | Button, 11 | FormControl, 12 | FormLabel, 13 | Input, 14 | } from "@chakra-ui/react"; 15 | 16 | interface AddUserModalProps { 17 | isOpen: boolean; 18 | onClose: () => void; 19 | addUser: (userData: { 20 | username: string; 21 | email: string; 22 | password: string; 23 | }) => void; 24 | } 25 | 26 | const AddUserModal: React.FC = ({ 27 | isOpen, 28 | onClose, 29 | addUser, 30 | }) => { 31 | const [username, setUsername] = useState(""); 32 | const [email, setEmail] = useState(""); 33 | const [password, setPassword] = useState(""); 34 | 35 | const handleSubmit = (e: React.FormEvent) => { 36 | e.preventDefault(); 37 | addUser({ username, email, password }); 38 | setUsername(""); 39 | setEmail(""); 40 | setPassword(""); 41 | }; 42 | 43 | return ( 44 | 45 | 46 | 47 | Add User 48 | 49 | 50 |
51 | 52 | Username 53 | setUsername(e.target.value)} 56 | required 57 | /> 58 | 59 | 60 | Email 61 | setEmail(e.target.value)} 65 | required 66 | /> 67 | 68 | 69 | Password 70 | setPassword(e.target.value)} 74 | required 75 | /> 76 | 77 |
78 |
79 | 80 | 81 | 84 | 87 | 88 |
89 |
90 | ); 91 | }; 92 | 93 | export default AddUserModal; 94 | -------------------------------------------------------------------------------- /services/client/src/components/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Formik, Form, Field } from "formik"; 3 | import { z } from "zod"; 4 | import { 5 | Box, 6 | Button, 7 | FormControl, 8 | FormLabel, 9 | Input, 10 | FormErrorMessage, 11 | VStack, 12 | Heading, 13 | } from "@chakra-ui/react"; 14 | import { Navigate } from "react-router-dom"; 15 | 16 | const validationSchema = z.object({ 17 | email: z.string().email("Enter a valid email"), 18 | password: z.string().min(1, "Password is required"), 19 | }); 20 | 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | type FormValues = z.infer; 23 | 24 | interface LoginFormProps { 25 | onSubmit: (values: { email: string; password: string }) => Promise; 26 | isAuthenticated: () => boolean; 27 | } 28 | 29 | const LoginForm: React.FC = ({ onSubmit, isAuthenticated }) => { 30 | if (isAuthenticated()) { 31 | return ; 32 | } 33 | return ( 34 | 35 | 36 | Log In 37 | 38 | { 44 | onSubmit(values); 45 | resetForm(); 46 | setSubmitting(false); 47 | }} 48 | validate={(values) => { 49 | try { 50 | validationSchema.parse(values); 51 | return {}; 52 | } catch (error) { 53 | return (error as z.ZodError).formErrors.fieldErrors; 54 | } 55 | }} 56 | > 57 | {({ errors, touched, isSubmitting }) => ( 58 |
59 | 60 | 61 | {({ field }) => ( 62 | 63 | Email 64 | 70 | {errors.email} 71 | 72 | )} 73 | 74 | 75 | {({ field }) => ( 76 | 79 | Password 80 | 86 | {errors.password} 87 | 88 | )} 89 | 90 | 100 | 101 |
102 | )} 103 |
104 |
105 | ); 106 | }; 107 | 108 | export default LoginForm; 109 | -------------------------------------------------------------------------------- /services/client/src/components/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Alert, 4 | AlertIcon, 5 | AlertTitle, 6 | CloseButton, 7 | Box, 8 | Container, 9 | } from "@chakra-ui/react"; 10 | 11 | interface MessageProps { 12 | messageType: "info" | "warning" | "success" | "error"; 13 | messageText: string; 14 | onClose?: () => void; 15 | } 16 | 17 | const Message: React.FC = ({ 18 | messageType, 19 | messageText, 20 | onClose, 21 | }) => { 22 | return ( 23 | 24 | 32 | 33 | 34 | {messageText} 35 | 36 | {onClose && ( 37 | 44 | )} 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default Message; 51 | -------------------------------------------------------------------------------- /services/client/src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Flex, 5 | Heading, 6 | Link, 7 | Spacer, 8 | IconButton, 9 | Stack, 10 | Collapse, 11 | useDisclosure, 12 | } from "@chakra-ui/react"; 13 | import { HamburgerIcon, CloseIcon } from "@chakra-ui/icons"; 14 | import { Link as RouterLink } from "react-router-dom"; 15 | 16 | interface NavBarProps { 17 | title: string; 18 | logoutUser: () => void; 19 | isAuthenticated: () => boolean; 20 | } 21 | 22 | const NavBar: React.FC = ({ 23 | title, 24 | logoutUser, 25 | isAuthenticated, 26 | }) => { 27 | const { isOpen, onToggle } = useDisclosure(); 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 40 | {title} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | : } 52 | variant="outline" 53 | aria-label="Toggle Navigation" 54 | color="white" 55 | /> 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | const NavLinks: React.FC<{ 68 | logoutUser: () => void; 69 | isAuthenticated: () => boolean; 70 | }> = ({ logoutUser, isAuthenticated }) => ( 71 | <> 72 | 73 | About 74 | 75 | {isAuthenticated() ? ( 76 | <> 77 | 78 | User Status 79 | 80 | 87 | Log Out 88 | 89 | 90 | ) : ( 91 | <> 92 | 93 | Register 94 | 95 | 96 | Log In 97 | 98 | 99 | )} 100 | 101 | ); 102 | 103 | export default NavBar; 104 | -------------------------------------------------------------------------------- /services/client/src/components/RegisterForm.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Formik, Form, Field } from "formik"; 3 | import { z } from "zod"; 4 | import { 5 | Box, 6 | Button, 7 | FormControl, 8 | FormLabel, 9 | Input, 10 | FormErrorMessage, 11 | VStack, 12 | Heading, 13 | } from "@chakra-ui/react"; 14 | import { Navigate } from "react-router-dom"; 15 | 16 | interface RegisterFormProps { 17 | onSubmit: (values: { 18 | username: string; 19 | email: string; 20 | password: string; 21 | }) => Promise; 22 | isAuthenticated: () => boolean; 23 | } 24 | 25 | const validationSchema = z.object({ 26 | username: z 27 | .string() 28 | .min(6, "Username must be at least 6 characters long") 29 | .regex( 30 | /^[a-zA-Z0-9_]+$/, 31 | "Username can only contain letters, numbers, and underscores", 32 | ), 33 | email: z 34 | .string() 35 | .email("Enter a valid email") 36 | .min(6, "Email must be at least 6 characters long"), 37 | password: z.string().min(11, "Password must be at least 11 characters long"), 38 | }); 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 41 | type FormValues = z.infer; 42 | 43 | const RegisterForm: React.FC = ({ 44 | onSubmit, 45 | isAuthenticated, 46 | }) => { 47 | if (isAuthenticated()) { 48 | return ; 49 | } 50 | 51 | return ( 52 | 53 | 54 | Register 55 | 56 | { 63 | try { 64 | await onSubmit(values); 65 | resetForm(); 66 | } catch (error) { 67 | console.error("Registration failed:", error); 68 | } finally { 69 | setSubmitting(false); 70 | } 71 | }} 72 | validate={(values) => { 73 | try { 74 | validationSchema.parse(values); 75 | return {}; 76 | } catch (error) { 77 | return (error as z.ZodError).formErrors.fieldErrors; 78 | } 79 | }} 80 | > 81 | {({ errors, touched, isSubmitting }) => ( 82 |
83 | 84 | 85 | {({ field }) => ( 86 | 89 | Username 90 | 95 | 96 | {errors.username} 97 | 98 | 99 | )} 100 | 101 | 102 | {({ field }) => ( 103 | 104 | Email 105 | 111 | 112 | {errors.email} 113 | 114 | 115 | )} 116 | 117 | 118 | {({ field }) => ( 119 | 122 | Password 123 | 129 | 130 | {errors.password} 131 | 132 | 133 | )} 134 | 135 | 145 | 146 |
147 | )} 148 |
149 |
150 | ); 151 | }; 152 | 153 | export default RegisterForm; 154 | -------------------------------------------------------------------------------- /services/client/src/components/UserStatus.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | import { Box } from "@chakra-ui/react"; 4 | import { Navigate } from "react-router-dom"; 5 | 6 | interface UserStatusProps { 7 | accessToken: string; 8 | isAuthenticated: () => boolean; 9 | } 10 | 11 | const UserStatus: React.FC = ({ 12 | accessToken, 13 | isAuthenticated, 14 | }) => { 15 | const [email, setEmail] = useState(""); 16 | const [username, setUsername] = useState(""); 17 | 18 | useEffect(() => { 19 | const getUserStatus = async () => { 20 | try { 21 | const options = { 22 | url: `${import.meta.env.VITE_API_SERVICE_URL}/auth/status`, 23 | method: "get", 24 | headers: { 25 | "Content-Type": "application/json", 26 | Authorization: `Bearer ${accessToken}`, 27 | }, 28 | }; 29 | const res = await axios(options); 30 | setEmail(res.data.email); 31 | setUsername(res.data.username); 32 | } catch (error) { 33 | console.log(error); 34 | } 35 | }; 36 | getUserStatus(); 37 | }, [accessToken]); 38 | 39 | if (!isAuthenticated()) { 40 | return ; 41 | } 42 | 43 | return ( 44 | 45 |
46 |

{email}

47 |

{username}

48 |
49 |
50 | ); 51 | }; 52 | 53 | export default UserStatus; 54 | -------------------------------------------------------------------------------- /services/client/src/components/Users.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Heading, 4 | Box, 5 | Divider, 6 | Table, 7 | Thead, 8 | Tbody, 9 | Tr, 10 | Th, 11 | Td, 12 | Text, 13 | Button, 14 | } from "@chakra-ui/react"; 15 | 16 | interface User { 17 | created_date: string; 18 | email: string; 19 | id: number; 20 | username: string; 21 | } 22 | 23 | interface UsersProps { 24 | users: User[]; 25 | onAddUser: () => void; 26 | removeUser: (userId: number) => void; 27 | isAuthenticated: boolean; 28 | } 29 | 30 | const Users: React.FC = ({ 31 | users, 32 | onAddUser, 33 | removeUser, 34 | isAuthenticated, 35 | }) => { 36 | return ( 37 | 38 | 46 | Users 47 | 48 | {isAuthenticated && ( 49 | 52 | )} 53 | 54 | 55 | {users.length > 0 ? ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {isAuthenticated && } 64 | 65 | 66 | 67 | {users.map((user) => ( 68 | 69 | 70 | 71 | 72 | 73 | {isAuthenticated && ( 74 | 83 | )} 84 | 85 | ))} 86 | 87 |
IDUsernameEmailCreated DateActions
{user.id}{user.username}{user.email}{new Date(user.created_date).toLocaleString()} 75 | 82 |
88 | ) : ( 89 | 90 | There are no registered users. 91 | 92 | )} 93 |
94 | ); 95 | }; 96 | 97 | export default Users; 98 | -------------------------------------------------------------------------------- /services/client/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/flask-react-aws/4eefa6fc42208c3006c4513300c752977bfad78a/services/client/src/index.css -------------------------------------------------------------------------------- /services/client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ChakraProvider } from "@chakra-ui/react"; 3 | import ReactDOM from "react-dom/client"; 4 | import { BrowserRouter as Router } from "react-router-dom"; 5 | import App from "./App.tsx"; 6 | import "./index.css"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | 14 | 15 | , 16 | ); 17 | -------------------------------------------------------------------------------- /services/client/src/tests/components/About.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, cleanup } from "../test-utils"; 3 | import { it, expect, afterEach } from "vitest"; 4 | import "@testing-library/jest-dom/vitest"; 5 | 6 | import About from "../../components/About"; 7 | 8 | afterEach(cleanup); 9 | 10 | it("renders properly", () => { 11 | const { getByText } = render(); 12 | expect(getByText("Add something relevant here.")).toHaveClass("content"); 13 | }); 14 | 15 | it("renders", () => { 16 | const { asFragment } = render(); 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | -------------------------------------------------------------------------------- /services/client/src/tests/components/AddUser.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "../test-utils"; 3 | import { it, expect, describe, vi } from "vitest"; 4 | import "@testing-library/jest-dom/vitest"; 5 | import AddUser from "../../components/AddUser"; 6 | 7 | describe("AddUser", () => { 8 | const mockAddUserToList = vi.fn(); 9 | 10 | it("renders with default props", () => { 11 | render(); 12 | 13 | const usernameInput = screen.getByLabelText(/username/i); 14 | expect(usernameInput).toBeInTheDocument(); 15 | expect(usernameInput).toHaveValue(""); 16 | 17 | const emailInput = screen.getByLabelText(/email/i); 18 | expect(emailInput).toBeInTheDocument(); 19 | expect(emailInput).toHaveValue(""); 20 | 21 | const passwordInput = screen.getByLabelText(/password/i); 22 | expect(passwordInput).toBeInTheDocument(); 23 | expect(passwordInput).toHaveValue(""); 24 | 25 | const submitButton = screen.getByRole("button", { name: "Submit" }); 26 | expect(submitButton).toBeInTheDocument(); 27 | }); 28 | 29 | it("renders", () => { 30 | const { asFragment } = render( 31 | , 32 | ); 33 | expect(asFragment()).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /services/client/src/tests/components/AddUserModal.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen, fireEvent, cleanup } from "../test-utils"; 3 | import { describe, it, expect, vi, afterEach } from "vitest"; 4 | import { ChakraProvider } from "@chakra-ui/react"; 5 | import AddUserModal from "../../components/AddUserModal"; 6 | 7 | const renderWithChakra = (ui: React.ReactElement) => { 8 | return render({ui}); 9 | }; 10 | 11 | describe("AddUserModal", () => { 12 | afterEach(() => { 13 | cleanup(); 14 | vi.restoreAllMocks(); 15 | }); 16 | 17 | const mockAddUser = vi.fn(); 18 | const mockOnClose = vi.fn(); 19 | 20 | it("renders correctly when open", () => { 21 | renderWithChakra( 22 | , 27 | ); 28 | 29 | expect(screen.getByText("Add User")).toBeDefined(); 30 | expect(screen.getByLabelText("Username")).toBeDefined(); 31 | expect(screen.getByLabelText("Email")).toBeDefined(); 32 | expect(screen.getByLabelText("Password")).toBeDefined(); 33 | expect(screen.getByRole("button", { name: "Add" })).toBeDefined(); 34 | expect(screen.getByRole("button", { name: "Cancel" })).toBeDefined(); 35 | }); 36 | 37 | it("does not render when closed", () => { 38 | renderWithChakra( 39 | , 44 | ); 45 | 46 | expect(screen.queryByText("Add User")).toBeNull(); 47 | }); 48 | 49 | it("calls addUser with correct data when form is submitted", () => { 50 | renderWithChakra( 51 | , 56 | ); 57 | 58 | fireEvent.change(screen.getByLabelText("Username"), { 59 | target: { value: "testuser" }, 60 | }); 61 | fireEvent.change(screen.getByLabelText("Email"), { 62 | target: { value: "test@example.com" }, 63 | }); 64 | fireEvent.change(screen.getByLabelText("Password"), { 65 | target: { value: "password123" }, 66 | }); 67 | fireEvent.click(screen.getByRole("button", { name: "Add" })); 68 | 69 | expect(mockAddUser).toHaveBeenCalledWith({ 70 | username: "testuser", 71 | email: "test@example.com", 72 | password: "password123", 73 | }); 74 | }); 75 | 76 | it("calls onClose when Cancel button is clicked", () => { 77 | renderWithChakra( 78 | , 83 | ); 84 | 85 | fireEvent.click(screen.getByRole("button", { name: "Cancel" })); 86 | 87 | expect(mockOnClose).toHaveBeenCalled(); 88 | }); 89 | 90 | it("resets form fields after submission", () => { 91 | renderWithChakra( 92 | , 97 | ); 98 | 99 | const usernameInput = screen.getByLabelText("Username") as HTMLInputElement; 100 | const emailInput = screen.getByLabelText("Email") as HTMLInputElement; 101 | const passwordInput = screen.getByLabelText("Password") as HTMLInputElement; 102 | 103 | fireEvent.change(usernameInput, { target: { value: "testuser" } }); 104 | fireEvent.change(emailInput, { target: { value: "test@example.com" } }); 105 | fireEvent.change(passwordInput, { target: { value: "password123" } }); 106 | fireEvent.click(screen.getByRole("button", { name: "Add" })); 107 | 108 | expect(usernameInput.value).toBe(""); 109 | expect(emailInput.value).toBe(""); 110 | expect(passwordInput.value).toBe(""); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /services/client/src/tests/components/LoginForm.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "../test-utils"; 3 | import { describe, it, expect, vi } from "vitest"; 4 | import { expect as expectVitest } from "vitest"; 5 | import { BrowserRouter as Router } from "react-router-dom"; 6 | import LoginForm from "../../components/LoginForm"; 7 | 8 | const mockProps = { 9 | onSubmit: vi.fn(), 10 | isAuthenticated: vi.fn().mockReturnValue(false), 11 | }; 12 | 13 | const renderWithRouter = (ui: React.ReactElement, { route = "/" } = {}) => { 14 | window.history.pushState({}, "Test page", route); 15 | return render(ui, { wrapper: Router }); 16 | }; 17 | 18 | describe("LoginForm", () => { 19 | it("renders properly", () => { 20 | renderWithRouter(); 21 | const heading = screen.getByRole("heading", { name: "Log In" }); 22 | expect(heading.tagName.toLowerCase()).toBe("h1"); 23 | }); 24 | 25 | it("renders with default props", () => { 26 | renderWithRouter(); 27 | 28 | const emailInput = screen.getByLabelText("Email") as HTMLInputElement; 29 | expect(emailInput.type).toBe("email"); 30 | expect(emailInput.value).toBe(""); 31 | 32 | const passwordInput = screen.getByLabelText("Password") as HTMLInputElement; 33 | expect(passwordInput.type).toBe("password"); 34 | expect(passwordInput.value).toBe(""); 35 | 36 | const submitButtons = screen.getAllByRole("button", { name: "Log In" }); 37 | expect(submitButtons[0].textContent).toBe("Log In"); 38 | }); 39 | 40 | it("renders", () => { 41 | const { asFragment } = renderWithRouter(); 42 | expect(asFragment()).toMatchSnapshot(); 43 | }); 44 | 45 | it("renders login form when not authenticated", () => { 46 | const { container } = renderWithRouter(); 47 | const heading = container.querySelector("h1"); 48 | expectVitest(heading?.textContent).toBe("Log In"); 49 | }); 50 | 51 | it("redirects when authenticated", () => { 52 | const authenticatedProps = { 53 | ...mockProps, 54 | isAuthenticated: vi.fn().mockReturnValue(true), 55 | }; 56 | renderWithRouter(); 57 | expect(window.location.pathname).toBe("/"); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /services/client/src/tests/components/Message.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen, cleanup } from "../test-utils"; 3 | import { describe, it, expect, afterEach, vi } from "vitest"; 4 | import { ChakraProvider } from "@chakra-ui/react"; 5 | import Message from "../../components/Message"; 6 | 7 | const renderWithChakra = (component: React.ReactElement) => { 8 | return render({component}); 9 | }; 10 | 11 | describe("Message Component", () => { 12 | afterEach(() => { 13 | cleanup(); 14 | vi.restoreAllMocks(); 15 | }); 16 | 17 | it("renders a success message", () => { 18 | const props = { 19 | messageType: "success" as const, 20 | messageText: "Operation successful!", 21 | }; 22 | renderWithChakra(); 23 | 24 | const alert = screen.getByRole("alert"); 25 | expect(alert).toBeDefined(); 26 | expect(alert.getAttribute("data-status")).toBe("success"); 27 | expect(screen.getByText("Operation successful!")).toBeDefined(); 28 | }); 29 | 30 | it("renders an error message", () => { 31 | const props = { 32 | messageType: "error" as const, 33 | messageText: "An error occurred.", 34 | }; 35 | renderWithChakra(); 36 | 37 | const alert = screen.getByRole("alert"); 38 | expect(alert).toBeDefined(); 39 | expect(alert.getAttribute("data-status")).toBe("error"); 40 | expect(screen.getByText("An error occurred.")).toBeDefined(); 41 | }); 42 | 43 | it("renders an info message", () => { 44 | const props = { 45 | messageType: "info" as const, 46 | messageText: "Here is some information.", 47 | }; 48 | renderWithChakra(); 49 | 50 | const alert = screen.getByRole("alert"); 51 | expect(alert).toBeDefined(); 52 | expect(alert.getAttribute("data-status")).toBe("info"); 53 | expect(screen.getByText("Here is some information.")).toBeDefined(); 54 | }); 55 | 56 | it("renders a close button when onClose prop is provided", () => { 57 | const onCloseMock = vi.fn(); 58 | const props = { 59 | messageType: "info" as const, 60 | messageText: "Closable message", 61 | onClose: onCloseMock, 62 | }; 63 | renderWithChakra(); 64 | 65 | const closeButton = screen.getByRole("button"); 66 | expect(closeButton).toBeDefined(); 67 | }); 68 | 69 | it("does not render a close button when onClose prop is not provided", () => { 70 | const props = { 71 | messageType: "info" as const, 72 | messageText: "Non-closable message", 73 | }; 74 | renderWithChakra(); 75 | 76 | const closeButton = screen.queryByRole("button"); 77 | expect(closeButton).toBeNull(); 78 | }); 79 | }); 80 | 81 | describe("Message Component Snapshots", () => { 82 | it("matches snapshot for success message", () => { 83 | const props = { 84 | messageType: "success" as const, 85 | messageText: "Operation successful!", 86 | }; 87 | const { asFragment } = renderWithChakra(); 88 | expect(asFragment()).toMatchSnapshot(); 89 | }); 90 | 91 | it("matches snapshot for error message", () => { 92 | const props = { 93 | messageType: "error" as const, 94 | messageText: "An error occurred.", 95 | }; 96 | const { asFragment } = renderWithChakra(); 97 | expect(asFragment()).toMatchSnapshot(); 98 | }); 99 | 100 | it("matches snapshot for info message", () => { 101 | const props = { 102 | messageType: "info" as const, 103 | messageText: "Here is some information.", 104 | }; 105 | const { asFragment } = renderWithChakra(); 106 | expect(asFragment()).toMatchSnapshot(); 107 | }); 108 | 109 | it("matches snapshot for message with close button", () => { 110 | const props = { 111 | messageType: "info" as const, 112 | messageText: "Closable message", 113 | onClose: () => {}, 114 | }; 115 | const { asFragment } = renderWithChakra(); 116 | expect(asFragment()).toMatchSnapshot(); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /services/client/src/tests/components/NavBar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "../test-utils"; 3 | import { it, expect } from "vitest"; 4 | import "@testing-library/jest-dom/vitest"; 5 | 6 | import NavBar from "../../components/NavBar"; 7 | 8 | const mockProps = { 9 | title: "Hello, World!", 10 | logoutUser: () => {}, 11 | isAuthenticated: () => false, 12 | }; 13 | 14 | it("NavBar renders without crashing", () => { 15 | render(); 16 | 17 | const titleElement = screen.getByText("Hello, World!"); 18 | expect(titleElement).toBeInTheDocument(); 19 | expect(titleElement.closest("h1")).toHaveClass("navbar-item"); 20 | expect(titleElement.closest("h1")).toHaveClass("nav-title"); 21 | }); 22 | 23 | it("NavBar renders properly", () => { 24 | const { asFragment } = render(); 25 | expect(asFragment()).toMatchSnapshot(); 26 | }); 27 | -------------------------------------------------------------------------------- /services/client/src/tests/components/RegisterForm.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | render, 4 | screen, 5 | cleanup, 6 | fireEvent, 7 | waitFor, 8 | act, 9 | } from "../test-utils"; 10 | import { describe, it, expect, vi, afterEach } from "vitest"; 11 | import { BrowserRouter as Router } from "react-router-dom"; 12 | import RegisterForm from "../../components/RegisterForm"; 13 | import { expect as expectVitest } from "vitest"; 14 | 15 | const mockProps = { 16 | onSubmit: vi.fn(), 17 | isAuthenticated: vi.fn().mockReturnValue(false), 18 | }; 19 | 20 | const renderWithRouter = (ui: React.ReactElement, { route = "/" } = {}) => { 21 | window.history.pushState({}, "Test page", route); 22 | return render(ui, { wrapper: Router }); 23 | }; 24 | 25 | describe("RegisterForm", () => { 26 | afterEach(() => { 27 | cleanup(); 28 | }); 29 | 30 | it("renders properly", () => { 31 | renderWithRouter(); 32 | const heading = screen.getByRole("heading", { name: "Register" }); 33 | expect(heading.tagName.toLowerCase()).toBe("h1"); 34 | }); 35 | 36 | it("renders with default props", () => { 37 | renderWithRouter(); 38 | 39 | const usernameInput = screen.getByLabelText("Username") as HTMLInputElement; 40 | expect(usernameInput.value).toBe(""); 41 | 42 | const emailInput = screen.getByLabelText("Email") as HTMLInputElement; 43 | expect(emailInput.type).toBe("email"); 44 | expect(emailInput.value).toBe(""); 45 | 46 | const passwordInput = screen.getByLabelText("Password") as HTMLInputElement; 47 | expect(passwordInput.type).toBe("password"); 48 | expect(passwordInput.value).toBe(""); 49 | 50 | const submitButtons = screen.getAllByRole("button", { name: "Register" }); 51 | expect(submitButtons[0].textContent).toBe("Register"); 52 | }); 53 | 54 | it("renders", () => { 55 | const { asFragment } = renderWithRouter(); 56 | expect(asFragment()).toMatchSnapshot(); 57 | }); 58 | 59 | it("renders register form when not authenticated", () => { 60 | const { container } = renderWithRouter(); 61 | const heading = container.querySelector("h1"); 62 | expectVitest(heading?.textContent).toBe("Register"); 63 | }); 64 | 65 | it("redirects when authenticated", () => { 66 | const authenticatedProps = { 67 | ...mockProps, 68 | isAuthenticated: vi.fn().mockReturnValue(true), 69 | }; 70 | renderWithRouter(); 71 | expect(window.location.pathname).toBe("/"); 72 | }); 73 | }); 74 | 75 | describe("handles form validation correctly", () => { 76 | afterEach(() => { 77 | cleanup(); 78 | }); 79 | const mockProps = { 80 | onSubmit: vi.fn(), 81 | isAuthenticated: vi.fn().mockReturnValue(false), 82 | }; 83 | 84 | it("when fields are empty", async () => { 85 | renderWithRouter(); 86 | 87 | const submitButton = screen.getByRole("button", { name: "Register" }); 88 | fireEvent.click(submitButton); 89 | 90 | await waitFor(() => { 91 | expect(screen.getByTestId("errors-username").textContent).to.include( 92 | "Username must be at least 6 characters long", 93 | ); 94 | expect(screen.getByTestId("errors-email").textContent).to.include( 95 | "Enter a valid email", 96 | ); 97 | expect(screen.getByTestId("errors-password").textContent).to.include( 98 | "Password must be at least 11 characters long", 99 | ); 100 | }); 101 | 102 | expect(mockProps.onSubmit).not.toHaveBeenCalled(); 103 | }); 104 | 105 | it("when email field is not valid", async () => { 106 | const { getByLabelText, container, findByTestId } = renderWithRouter( 107 | , 108 | ); 109 | 110 | const form = container.querySelector("form"); 111 | if (!form) throw new Error("Form not found"); 112 | const emailInput = getByLabelText("Email"); 113 | 114 | expect(mockProps.onSubmit).toHaveBeenCalledTimes(0); 115 | 116 | await act(async () => { 117 | fireEvent.change(emailInput, { target: { value: "invalid" } }); 118 | fireEvent.blur(emailInput); 119 | }); 120 | 121 | expect((await findByTestId("errors-email")).innerHTML).toBe( 122 | "Enter a valid email", 123 | ); 124 | 125 | await act(async () => { 126 | fireEvent.submit(form); 127 | }); 128 | 129 | await waitFor(() => { 130 | expect(mockProps.onSubmit).toHaveBeenCalledTimes(0); 131 | }); 132 | }); 133 | 134 | it("when fields are not the proper length", async () => { 135 | const { getByLabelText, container, findByTestId } = renderWithRouter( 136 | , 137 | ); 138 | 139 | const form = container.querySelector("form"); 140 | if (!form) throw new Error("Form not found"); 141 | const usernameInput = getByLabelText("Username"); 142 | const emailInput = getByLabelText("Email"); 143 | const passwordInput = getByLabelText("Password"); 144 | 145 | expect(mockProps.onSubmit).toHaveBeenCalledTimes(0); 146 | 147 | await act(async () => { 148 | fireEvent.change(usernameInput, { target: { value: "null" } }); 149 | fireEvent.change(emailInput, { target: { value: "t@t.c" } }); 150 | fireEvent.change(passwordInput, { target: { value: "invalid" } }); 151 | fireEvent.blur(usernameInput); 152 | fireEvent.blur(emailInput); 153 | fireEvent.blur(passwordInput); 154 | }); 155 | 156 | expect((await findByTestId("errors-username")).innerHTML).to.include( 157 | "Username must be at least 6 characters long", 158 | ); 159 | expect((await findByTestId("errors-email")).innerHTML).to.include( 160 | "Email must be at least 6 characters long", 161 | ); 162 | expect((await findByTestId("errors-password")).innerHTML).to.include( 163 | "Password must be at least 11 characters long", 164 | ); 165 | 166 | await act(async () => { 167 | fireEvent.submit(form); 168 | }); 169 | 170 | await waitFor(() => { 171 | expect(mockProps.onSubmit).toHaveBeenCalledTimes(0); 172 | }); 173 | }); 174 | 175 | it("when fields are valid", async () => { 176 | const { getByLabelText, container } = renderWithRouter( 177 | , 178 | ); 179 | 180 | const form = container.querySelector("form"); 181 | if (!form) throw new Error("Form not found"); 182 | const usernameInput = getByLabelText("Username"); 183 | const emailInput = getByLabelText("Email"); 184 | const passwordInput = getByLabelText("Password"); 185 | 186 | expect(mockProps.onSubmit).toHaveBeenCalledTimes(0); 187 | 188 | await act(async () => { 189 | fireEvent.change(usernameInput, { target: { value: "proper" } }); 190 | fireEvent.change(emailInput, { target: { value: "t@t.com" } }); 191 | fireEvent.change(passwordInput, { target: { value: "properlength" } }); 192 | fireEvent.blur(usernameInput); 193 | fireEvent.blur(emailInput); 194 | fireEvent.blur(passwordInput); 195 | 196 | fireEvent.submit(form); 197 | }); 198 | 199 | await waitFor(() => { 200 | expect(mockProps.onSubmit).toHaveBeenCalledTimes(1); 201 | }); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /services/client/src/tests/components/UserStatus.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen, cleanup, fireEvent } from "../test-utils"; 3 | import { describe, it, expect, vi, afterEach } from "vitest"; 4 | import "@testing-library/jest-dom/vitest"; 5 | 6 | import NavBar from "../../components/NavBar"; 7 | 8 | describe("NavBar", () => { 9 | afterEach(() => { 10 | cleanup(); 11 | vi.restoreAllMocks(); 12 | }); 13 | 14 | const mockLogoutUser = vi.fn(); 15 | 16 | const expandMenu = () => { 17 | const menuButton = screen.getByLabelText("Toggle Navigation"); 18 | fireEvent.click(menuButton); 19 | }; 20 | 21 | it("NavBar renders without crashing", () => { 22 | render( 23 | false} 27 | />, 28 | ); 29 | 30 | const titleElement = screen.getByText("Hello, World!"); 31 | expect(titleElement).toBeInTheDocument(); 32 | expect(titleElement.closest("h1")).toHaveClass("navbar-item"); 33 | expect(titleElement.closest("h1")).toHaveClass("nav-title"); 34 | }); 35 | 36 | it("NavBar contains correct navigation links when user is logged out", () => { 37 | render( 38 | false} 42 | />, 43 | ); 44 | 45 | expandMenu(); 46 | 47 | // Links that should be visible when logged out 48 | expect(screen.getAllByText("About")[0]).toBeInTheDocument(); 49 | expect(screen.getAllByText("Register")[0]).toBeInTheDocument(); 50 | expect(screen.getAllByText("Log In")[0]).toBeInTheDocument(); 51 | 52 | // Links that should be hidden when logged out 53 | expect(screen.queryByText("User Status")).not.toBeInTheDocument(); 54 | expect(screen.queryByText("Log Out")).not.toBeInTheDocument(); 55 | }); 56 | 57 | it("NavBar contains correct navigation links when user is logged in", () => { 58 | render( 59 | true} 63 | />, 64 | ); 65 | 66 | expandMenu(); 67 | 68 | // Links that should be visible when logged in 69 | expect(screen.getAllByText("About")[0]).toBeInTheDocument(); 70 | expect(screen.getAllByText("User Status")[0]).toBeInTheDocument(); 71 | 72 | // Check for Log Out link 73 | const logOutLinks = screen.getAllByText("Log Out"); 74 | expect(logOutLinks.length).toBeGreaterThan(0); 75 | expect(logOutLinks[0]).toBeInTheDocument(); 76 | 77 | // Links that should be hidden when logged in 78 | expect(screen.queryByText("Register")).not.toBeInTheDocument(); 79 | expect(screen.queryByText("Log In")).not.toBeInTheDocument(); 80 | }); 81 | 82 | it("NavBar renders properly", () => { 83 | const { asFragment } = render( 84 | false} 88 | />, 89 | ); 90 | expect(asFragment()).toMatchSnapshot(); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /services/client/src/tests/components/Users.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { it, expect, describe, vi, afterEach } from "vitest"; 3 | import { render, screen, fireEvent, cleanup } from "../test-utils"; 4 | import Users from "../../components/Users"; 5 | import "@testing-library/jest-dom/vitest"; 6 | 7 | describe("Users", () => { 8 | afterEach(() => { 9 | cleanup(); 10 | vi.restoreAllMocks(); 11 | }); 12 | const mockOnAddUser = vi.fn(); 13 | const mockRemoveUser = vi.fn(); 14 | 15 | it("Should render no registered users when there are no users passed to the component", () => { 16 | render( 17 | , 23 | ); // Pass isAuthenticated={false} 24 | const message = screen.getByText(/no registered users/i); 25 | expect(message).toBeTruthy(); 26 | }); 27 | 28 | it("Should render user details when users are passed to the component", () => { 29 | const mockUsers = [ 30 | { 31 | id: 1, 32 | username: "john_doe", 33 | email: "john@example.com", 34 | created_date: "2024-08-19", 35 | }, 36 | { 37 | id: 2, 38 | username: "jane_doe", 39 | email: "jane@example.com", 40 | created_date: "2024-08-18", 41 | }, 42 | ]; 43 | render( 44 | , 50 | ); // Pass isAuthenticated={true} 51 | 52 | // Assert that the user details are correctly rendered 53 | const userOne = screen.getByText("john_doe"); 54 | const userTwo = screen.getByText("jane_doe"); 55 | 56 | expect(userOne).toBeInTheDocument(); 57 | expect(userTwo).toBeInTheDocument(); 58 | 59 | const emailOne = screen.getByText("john@example.com"); 60 | const emailTwo = screen.getByText("jane@example.com"); 61 | 62 | expect(emailOne).toBeInTheDocument(); 63 | expect(emailTwo).toBeInTheDocument(); 64 | 65 | // Check for Delete buttons 66 | const deleteButtons = screen.getAllByText("Delete"); 67 | expect(deleteButtons).toHaveLength(2); 68 | }); 69 | 70 | it("Should call onAddUser when Add User button is clicked", () => { 71 | render( 72 | , 78 | ); // Pass isAuthenticated={true} 79 | const addButton = screen.getByText("Add User"); 80 | fireEvent.click(addButton); 81 | expect(mockOnAddUser).toHaveBeenCalled(); 82 | }); 83 | 84 | it("Should call removeUser when Delete button is clicked", () => { 85 | const mockUsers = [ 86 | { 87 | id: 1, 88 | username: "john_doe", 89 | email: "john@example.com", 90 | created_date: "2024-08-19", 91 | }, 92 | ]; 93 | render( 94 | , 100 | ); // Pass isAuthenticated={true} 101 | const deleteButton = screen.getByText("Delete"); 102 | fireEvent.click(deleteButton); 103 | expect(mockRemoveUser).toHaveBeenCalledWith(1); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /services/client/src/tests/components/__snapshots__/About.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`renders 1`] = ` 4 | 5 |
8 |
11 |

14 | About 15 |

16 |
20 |

23 | Add something relevant here. 24 |

25 |
26 |
27 |
32 | `; 33 | -------------------------------------------------------------------------------- /services/client/src/tests/components/__snapshots__/AddUser.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`AddUser > renders 1`] = ` 4 | 5 |
8 |
11 |

14 | Register a User 15 |

16 |
17 |
20 |
23 |
27 | 41 | 50 |
51 |
55 | 69 | 79 |
80 |
84 | 98 | 108 |
109 | 115 |
116 |
117 |
118 |
123 | `; 124 | -------------------------------------------------------------------------------- /services/client/src/tests/components/__snapshots__/LoginForm.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`LoginForm > renders 1`] = ` 4 | 5 |
8 |

11 | Log In 12 |

13 |
16 |
19 |
23 | 30 | 38 |
39 |
43 | 50 | 58 |
59 | 65 |
66 |
67 |
68 |
69 | `; 70 | -------------------------------------------------------------------------------- /services/client/src/tests/components/__snapshots__/Message.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Message Component Snapshots > matches snapshot for error message 1`] = ` 4 | 5 |
8 | 39 |
40 |
49 | `; 50 | 51 | exports[`Message Component Snapshots > matches snapshot for info message 1`] = ` 52 | 53 |
56 | 87 |
88 |
97 | `; 98 | 99 | exports[`Message Component Snapshots > matches snapshot for message with close button 1`] = ` 100 | 101 |
104 | 152 |
153 |
162 | `; 163 | 164 | exports[`Message Component Snapshots > matches snapshot for success message 1`] = ` 165 | 166 |
169 | 200 |
201 |
210 | `; 211 | -------------------------------------------------------------------------------- /services/client/src/tests/components/__snapshots__/NavBar.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`NavBar renders properly 1`] = ` 4 | 5 |
8 |
11 |
14 |

17 | 21 | Hello, World! 22 | 23 |

24 |
25 |
28 | 50 | 67 |
68 | 95 |
96 |