diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c06255..0f8abb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,109 +10,146 @@ on: - main jobs: - build: + build-jars: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + service: + - auth-service + - task-service + - api-gateway + - service-registry steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + + - name: Build ${{ matrix.service }} + run: mvn -f ${{ matrix.service }}/pom.xml clean package -DskipTests + + - name: Upload JAR + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.service }}-jar + path: ${{ matrix.service }}/target/*.jar + if-no-files-found: error + + integration: + runs-on: ubuntu-latest + needs: build-jars - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - - name: Cache Maven dependencies - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - - - name: Build auth-service - run: cd auth-service && mvn clean install - - - name: Build task-service - run: cd task-service && mvn clean install - - - name: Build api-gateway - run: cd api-gateway && mvn clean install - - - name: Build service-registry - run: cd service-registry && mvn clean install - - - name: Build Docker images - run: | - docker build -t task-manager-auth ./auth-service - docker build -t task-manager-task ./task-service - docker build -t task-manager-gateway ./api-gateway - docker build -t task-manager-registry ./service-registry - - - name: Build services (compose) - run: docker compose build - - - name: Start services - run: docker compose up -d - - - name: Wait for system readiness - run: | - for i in {1..40}; do - echo "Checking system readiness..." - - auth_status=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST http://localhost:8090/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"test","password":"test"}' \ - || echo "000") - - task_status=$(curl -s -o /dev/null -w "%{http_code}" \ - http://localhost:8090/api/tasks/dashboard || echo "000") - - echo "Auth: $auth_status | Task: $task_status" - - if [[ "$auth_status" =~ ^(200|400|401)$ ]] && [[ "$task_status" =~ ^(200|403)$ ]]; then - echo "System is READY ✅" - exit 0 - fi - - sleep 5 - done - - echo "System NOT ready ❌" - docker compose logs - exit 1 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 18 - - - name: Install Newman - run: npm install -g newman - - - name: Run Postman tests - run: newman run postman_collection.json -e postman_environment.json - - - name: Stop services - run: docker compose down -v - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Push Docker images - run: | - docker tag task-manager-auth ${{ secrets.DOCKER_USERNAME }}/task-manager-auth - docker push ${{ secrets.DOCKER_USERNAME }}/task-manager-auth - - docker tag task-manager-task ${{ secrets.DOCKER_USERNAME }}/task-manager-task - docker push ${{ secrets.DOCKER_USERNAME }}/task-manager-task - - docker tag task-manager-gateway ${{ secrets.DOCKER_USERNAME }}/task-manager-gateway - docker push ${{ secrets.DOCKER_USERNAME }}/task-manager-gateway - - docker tag task-manager-registry ${{ secrets.DOCKER_USERNAME }}/task-manager-registry - docker push ${{ secrets.DOCKER_USERNAME }}/task-manager-registry \ No newline at end of file + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + + - name: Download auth-service jar + uses: actions/download-artifact@v4 + with: + name: auth-service-jar + path: auth-service/target + + - name: Download task-service jar + uses: actions/download-artifact@v4 + with: + name: task-service-jar + path: task-service/target + + - name: Download api-gateway jar + uses: actions/download-artifact@v4 + with: + name: api-gateway-jar + path: api-gateway/target + + - name: Download service-registry jar + uses: actions/download-artifact@v4 + with: + name: service-registry-jar + path: service-registry/target + + - name: Build Docker images + run: docker compose build + + - name: Start services + run: docker compose up -d + + - name: Wait before readiness checks + run: sleep 40 + + - name: Wait for Gateway + run: | + for i in $(seq 1 50); do + status=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST http://localhost:8090/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"test","password":"test"}' \ + || echo "000") + + echo "Gateway status: $status" + + if [ "$status" = "200" ] || [ "$status" = "400" ] || [ "$status" = "401" ]; then + echo "Gateway ready" + exit 0 + fi + + sleep 5 + done + + echo "Gateway NOT ready" + docker compose logs --no-color + exit 1 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install Newman + run: npm install -g newman + + - name: Run Postman tests + run: | + newman run postman_collection.json \ + -e postman_environment.json \ + --env-var "baseUrl=http://localhost:8090" + + - name: Stop services + run: docker compose down -v + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Push images + if: github.ref == 'refs/heads/main' + run: | + docker push yash9912/task-manager-auth:latest + docker push yash9912/task-manager-task:latest + docker push yash9912/task-manager-gateway:latest + docker push yash9912/task-manager-registry:latest \ No newline at end of file diff --git a/api-gateway/docker-application.properties b/api-gateway/docker-application.properties deleted file mode 100644 index 2199cec..0000000 --- a/api-gateway/docker-application.properties +++ /dev/null @@ -1,12 +0,0 @@ -server.port=8080 -spring.application.name=api-gateway - -eureka.client.service-url.defaultZone=http://service-registry:8761/eureka - -spring.cloud.gateway.routes[0].id=auth -spring.cloud.gateway.routes[0].uri=lb://auth-service -spring.cloud.gateway.routes[0].predicates[0]=Path=/api/auth/** - -spring.cloud.gateway.routes[1].id=task -spring.cloud.gateway.routes[1].uri=lb://task-service -spring.cloud.gateway.routes[1].predicates[0]=Path=/api/tasks/** diff --git a/api-gateway/src/main/resources/application-docker.properties b/api-gateway/src/main/resources/application-docker.properties index 245fe44..0ff94ee 100644 --- a/api-gateway/src/main/resources/application-docker.properties +++ b/api-gateway/src/main/resources/application-docker.properties @@ -2,8 +2,10 @@ server.port=8080 spring.application.name=api-gateway eureka.client.serviceUrl.defaultZone=http://service-registry:8761/eureka spring.cloud.gateway.routes[0].id=auth -spring.cloud.gateway.routes[0].uri=lb://auth-service +spring.cloud.gateway.routes[0].uri=lb://AUTH-SERVICE spring.cloud.gateway.routes[0].predicates[0]=Path=/api/auth/** spring.cloud.gateway.routes[1].id=task -spring.cloud.gateway.routes[1].uri=lb://task-service +spring.cloud.gateway.routes[1].uri=lb://TASK-SERVICE spring.cloud.gateway.routes[1].predicates[0]=Path=/api/tasks/** +spring.cloud.gateway.discovery.locator.enabled=true +spring.cloud.loadbalancer.retry.enabled=true diff --git a/docker-compose.yml b/docker-compose.yml index 94e54fb..614fbed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,32 +1,26 @@ -version: "3.9" - services: service-registry: build: ./service-registry - container_name: service-registry + image: yash9912/task-manager-registry ports: - "8761:8761" networks: - microservices-net environment: - SPRING_PROFILES_ACTIVE=docker + redis: image: redis:7 - container_name: redis ports: - "6379:6379" command: ["redis-server", "--bind", "0.0.0.0", "--protected-mode", "no"] networks: - microservices-net - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 5 + auth-service: build: ./auth-service - container_name: auth-service + image: yash9912/task-manager-auth ports: - "8085:8081" depends_on: @@ -39,15 +33,12 @@ services: task-service: build: ./task-service - container_name: task-service + image: yash9912/task-manager-task ports: - "8082:8082" depends_on: - service-registry : - condition: service_started - redis : - condition: service_healthy - restart: always + - service-registry + - redis environment: - SPRING_PROFILES_ACTIVE=docker networks: @@ -55,13 +46,16 @@ services: api-gateway: build: ./api-gateway - container_name: api-gateway + image: yash9912/task-manager-gateway ports: - "8090:8080" depends_on: - - service-registry - - auth-service - - task-service + service-registry : + condition: service_started + auth-service : + condition: service_started + task-service : + condition: service_started environment: - SPRING_PROFILES_ACTIVE=docker networks: @@ -70,4 +64,3 @@ services: networks: microservices-net: driver: bridge - diff --git a/postman/collections/Task Manager E2E/create-task.request.yaml b/postman/collections/Task Manager E2E/create-task.request.yaml new file mode 100644 index 0000000..fbed5b5 --- /dev/null +++ b/postman/collections/Task Manager E2E/create-task.request.yaml @@ -0,0 +1,18 @@ +$kind: http-request +name: create-task +url: "{{baseUrl}}/api/tasks" +method: POST +body: + type: json + content: "{\r + + \ \"title\": \"Task {{username}}\",\r + + \ \"description\": \"Generated dynamically\"\r + + }" +auth: + type: bearer + credentials: + token: auth_token +order: 4503596869659604 diff --git a/postman/collections/Task Manager E2E/dashboard.request.yaml b/postman/collections/Task Manager E2E/dashboard.request.yaml new file mode 100644 index 0000000..00f353f --- /dev/null +++ b/postman/collections/Task Manager E2E/dashboard.request.yaml @@ -0,0 +1,5 @@ +$kind: http-request +name: dashboard +url: "" +method: GET +order: 4503596896278070 diff --git a/postman/collections/Task Manager E2E/login.request.yaml b/postman/collections/Task Manager E2E/login.request.yaml new file mode 100644 index 0000000..40ca044 --- /dev/null +++ b/postman/collections/Task Manager E2E/login.request.yaml @@ -0,0 +1,32 @@ +$kind: http-request +name: login +url: "{{baseUrl}}/api/auth/login" +method: POST +body: + type: json + content: "{\r + + \ \"username\": \"{{username}}\",\r + + \ \"password\": \"{{password}}\"\r + + }" +scripts: + - type: beforeRequest + code: "const json = pm.response.json();\r + + \r + + pm.environment.set(\"auth_token\", json.token);\r + + \r + + pm.test(\"Login success\", function () \r + + {\r + + \ pm.expect(pm.response.code).to.eql(200);\r + + });" + language: text/javascript +order: 4503582524205482 diff --git a/postman/collections/Task Manager E2E/register.request.yaml b/postman/collections/Task Manager E2E/register.request.yaml new file mode 100644 index 0000000..27f0e61 --- /dev/null +++ b/postman/collections/Task Manager E2E/register.request.yaml @@ -0,0 +1,24 @@ +$kind: http-request +name: register +url: "{{baseUrl}}/api/auth/register" +method: POST +body: + type: json + content: "{\r + + \ \"username\": \"{{username}}\",\r + + \ \"password\": \"{{password}}\"\r + + }" +scripts: + - type: beforeRequest + code: "pm.test(\"Register success\", function () \r + + {\r + + \ pm.expect(pm.response.code).to.be.oneOf([200, 400]);\r + + });" + language: text/javascript +order: 4503535441727413 diff --git a/postman_collection.json b/postman_collection.json index 10122e9..86b4416 100644 --- a/postman_collection.json +++ b/postman_collection.json @@ -1,211 +1,149 @@ { "info": { - "name": "Task Manager E2E", + "name": "Task Manager E2E (Dynamic)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, - "item": [ + "event": [ { - "name": "register-yash", - "request": { - "method": "POST", - "header": [{ "key": "Content-Type", "value": "application/json" }], - "body": { - "mode": "raw", - "raw": "{ \"username\": \"yash\", \"password\": \"123\", \"role\": \"USER\" }" - }, - "url": "{{baseUrl}}/api/auth/register" + "listen": "prerequest", + "script": { + "exec": [ + "if (!pm.environment.get('username')) {", + " const unique = Date.now();", + "", + " const username = 'user_' + unique;", + " const password = 'pass_' + unique;", + "", + " const statuses = ['TODO','IN_PROGRESS','DONE'];", + " const priorities = ['LOW','MEDIUM','HIGH'];", + "", + " const status = statuses[Math.floor(Math.random() * statuses.length)];", + " const priority = priorities[Math.floor(Math.random() * priorities.length)];", + "", + " const today = new Date();", + " const startDate = today.toISOString().split('T')[0];", + "", + " const due = new Date();", + " due.setDate(today.getDate() + 3);", + " const dueDate = due.toISOString().split('T')[0];", + "", + " pm.environment.set('username', username);", + " pm.environment.set('password', password);", + " pm.environment.set('status', status);", + " pm.environment.set('priority', priority);", + " pm.environment.set('startDate', startDate);", + " pm.environment.set('dueDate', dueDate);", + "}" + ] } - }, + } + ], - { - "name": "register-rahul", - "request": { - "method": "POST", - "header": [{ "key": "Content-Type", "value": "application/json" }], - "body": { - "mode": "raw", - "raw": "{ \"username\": \"rahul\", \"password\": \"123\", \"role\": \"USER\" }" - }, - "url": "{{baseUrl}}/api/auth/register" - } - }, + "item": [ { - "name": "register-admin", + "name": "register", "request": { "method": "POST", "header": [{ "key": "Content-Type", "value": "application/json" }], "body": { "mode": "raw", - "raw": "{ \"username\": \"admin\", \"password\": \"123\", \"role\": \"ADMIN\" }" + "raw": "{ \"username\": \"{{username}}\", \"password\": \"{{password}}\", \"role\": \"USER\" }" }, "url": "{{baseUrl}}/api/auth/register" - } - }, - - { - "name": "login-yash", + }, "event": [{ "listen": "test", "script": { "exec": [ - "if (pm.response.code === 200) {", - " const json = pm.response.json();", - " pm.environment.set('token_yash', json.accessToken);", - "}" + "pm.test('Register success', function () {", + " pm.expect(pm.response.code).to.be.oneOf([200,201,400]);", + "});" ] } - }], - "request": { - "method": "POST", - "header": [{ "key": "Content-Type", "value": "application/json" }], - "body": { - "mode": "raw", - "raw": "{ \"username\": \"yash\", \"password\": \"123\" }" - }, - "url": "{{baseUrl}}/api/auth/login" - } + }] }, { - "name": "login-rahul", - "event": [{ - "listen": "test", - "script": { - "exec": [ - "if (pm.response.code === 200) {", - " const json = pm.response.json();", - " pm.environment.set('token_rahul', json.accessToken);", - "}" - ] - } - }], + "name": "login", "request": { "method": "POST", "header": [{ "key": "Content-Type", "value": "application/json" }], "body": { "mode": "raw", - "raw": "{ \"username\": \"rahul\", \"password\": \"123\" }" + "raw": "{ \"username\": \"{{username}}\", \"password\": \"{{password}}\" }" }, "url": "{{baseUrl}}/api/auth/login" - } - }, - - { - "name": "login-admin", + }, "event": [{ "listen": "test", "script": { "exec": [ + "const json = pm.response.json();", + "", + "pm.test('Login success', function () {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", "if (pm.response.code === 200) {", - " const json = pm.response.json();", - " pm.environment.set('token_admin', json.accessToken);", + " pm.environment.set('auth_token', json.accessToken || json.token);", "}" ] } - }], - "request": { - "method": "POST", - "header": [{ "key": "Content-Type", "value": "application/json" }], - "body": { - "mode": "raw", - "raw": "{ \"username\": \"admin\", \"password\": \"123\" }" - }, - "url": "{{baseUrl}}/api/auth/login" - } - }, - - { - "name": "create-task-yash", - "request": { - "method": "POST", - "header": [ - { "key": "Content-Type", "value": "application/json" }, - { "key": "Authorization", "value": "Bearer {{token_yash}}" } - ], - "body": { - "mode": "raw", - "raw": "{ \"title\": \"Yash Task\", \"description\": \"Only for yash\", \"status\": \"TODO\", \"priority\": \"HIGH\" }" - }, - "url": "{{baseUrl}}/api/tasks" - } + }] }, { - "name": "create-task-rahul", + "name": "create-task", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" }, - { "key": "Authorization", "value": "Bearer {{token_rahul}}" } + { "key": "Authorization", "value": "Bearer {{auth_token}}" } ], "body": { "mode": "raw", - "raw": "{ \"title\": \"Rahul Task\", \"description\": \"Only for rahul\", \"status\": \"TODO\", \"priority\": \"LOW\" }" + "raw": "{\n \"title\": \"Task {{username}}\",\n \"description\": \"Generated dynamically\",\n \"status\": \"{{status}}\",\n \"priority\": \"{{priority}}\",\n \"startDate\": \"{{startDate}}\",\n \"dueDate\": \"{{dueDate}}\",\n \"assignedUsername\": \"{{username}}\"\n}" }, "url": "{{baseUrl}}/api/tasks" - } - }, - - { - "name": "dashboard-yash", + }, "event": [{ "listen": "test", "script": { "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", - "pm.test('Has totalTasks', function () { pm.expect(pm.response.json()).to.have.property('totalTasks'); });" + "pm.test('Task created', function () {", + " pm.expect(pm.response.code).to.be.oneOf([200,201]);", + "});" ] } - }], - "request": { - "method": "GET", - "header": [ - { "key": "Authorization", "value": "Bearer {{token_yash}}" } - ], - "url": "{{baseUrl}}/api/tasks/dashboard" - } + }] }, { - "name": "dashboard-rahul", - "event": [{ - "listen": "test", - "script": { - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", - "pm.test('Has totalTasks', function () { pm.expect(pm.response.json()).to.have.property('totalTasks'); });" - ] - } - }], + "name": "dashboard", "request": { "method": "GET", "header": [ - { "key": "Authorization", "value": "Bearer {{token_rahul}}" } + { "key": "Authorization", "value": "Bearer {{auth_token}}" } ], "url": "{{baseUrl}}/api/tasks/dashboard" - } - }, - - { - "name": "dashboard-admin", + }, "event": [{ "listen": "test", "script": { "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", - "pm.test('Has totalTasks', function () { pm.expect(pm.response.json()).to.have.property('totalTasks'); });" + "pm.test('Dashboard OK', function () {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test('Has totalTasks', function () {", + " const json = pm.response.json();", + " pm.expect(json).to.have.property('totalTasks');", + "});" ] } - }], - "request": { - "method": "GET", - "header": [ - { "key": "Authorization", "value": "Bearer {{token_admin}}" } - ], - "url": "{{baseUrl}}/api/tasks/dashboard" - } + }] } ] diff --git a/postman_environment.json b/postman_environment.json index 25aa58c..baeaced 100644 --- a/postman_environment.json +++ b/postman_environment.json @@ -3,23 +3,43 @@ "values": [ { "key": "baseUrl", - "value": "http://13.232.34.24:8090", + "value": "http://localhost:8090", "enabled": true }, { - "key": "token_yash", + "key": "username", "value": "", "enabled": true }, { - "key": "token_rahul", + "key": "password", "value": "", "enabled": true }, { - "key": "token_admin", + "key": "auth_token", + "value": "", + "enabled": true + }, + { + "key": "status", + "value": "", + "enabled": true + }, + { + "key": "priority", + "value": "", + "enabled": true + }, + { + "key": "startDate", + "value": "", + "enabled": true + }, + { + "key": "dueDate", "value": "", "enabled": true } ] -} +} \ No newline at end of file