feat: initial AutoPilot monorepo
- master/ : Spring Boot cloud coordinator (Gap 1-4 features) - worker/ : Selenium execution engine - ui/ : React + Vite dashboard (CI/CD integration page) - docker-compose.yml : production deployment - docker-compose.local.yml : local dev (DB only) - .env.example : environment variable template - README.md : full deployment and local setup guide
This commit is contained in:
commit
a467069c9c
|
|
@ -0,0 +1,23 @@
|
||||||
|
# AutoPilot - Production Environment Variables
|
||||||
|
# Copy this file to .env and fill in the real values before deploying
|
||||||
|
|
||||||
|
# ─── Database (PostgreSQL) ───────────────────────────────────────────────────
|
||||||
|
DB_URL=jdbc:postgresql://postgres:5432/localagent_cloud
|
||||||
|
DB_USER=localagent
|
||||||
|
DB_PASS=changeme_strong_password_here
|
||||||
|
|
||||||
|
# ─── Security ────────────────────────────────────────────────────────────────
|
||||||
|
# Must be at least 256 bits (32+ characters). Generate one with:
|
||||||
|
# openssl rand -base64 64
|
||||||
|
JWT_SECRET=replace_this_with_a_very_long_random_secret_string_minimum_256_bits
|
||||||
|
|
||||||
|
# ─── AWS S3 (for screenshot storage) ─────────────────────────────────────────
|
||||||
|
AWS_REGION=ap-south-1
|
||||||
|
AWS_ACCESS_KEY_ID=your_aws_access_key
|
||||||
|
AWS_SECRET_ACCESS_KEY=your_aws_secret_key
|
||||||
|
S3_BUCKET_NAME=autopilot-screenshots
|
||||||
|
|
||||||
|
# ─── Worker (AutoPilot Engine) ───────────────────────────────────────────────
|
||||||
|
# URL where the master backend is reachable from the worker container
|
||||||
|
LOCALAGENT_CLOUD_URL=http://master:9090
|
||||||
|
LOCALAGENT_POLLING_ENABLED=true
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# AutoPilot — ignore secrets and build artifacts
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# Java build artifacts
|
||||||
|
**/target/
|
||||||
|
**/*.class
|
||||||
|
**/*.jar
|
||||||
|
!**/mvnw
|
||||||
|
!**/mvnw.cmd
|
||||||
|
|
||||||
|
# Node / Frontend
|
||||||
|
**/node_modules/
|
||||||
|
**/dist/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
*.log.*
|
||||||
|
|
||||||
|
# Data / runtime
|
||||||
|
**/data/
|
||||||
|
**/screenshots/
|
||||||
|
**/DriverFiles/
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
# AutoPilot — Distributed Web Test Automation Platform
|
||||||
|
|
||||||
|
AutoPilot is a full-stack, enterprise-grade distributed web browser automation platform. It consists of three components deployed together as a monorepo.
|
||||||
|
|
||||||
|
```
|
||||||
|
autopilot/
|
||||||
|
├── master/ # Cloud Coordinator — Spring Boot REST API + Scheduler + DB
|
||||||
|
├── worker/ # Execution Engine — Selenium WebDriver agent (polls master)
|
||||||
|
├── ui/ # Dashboard — React + Vite frontend (served via Nginx)
|
||||||
|
├── docker-compose.yml # Production: full stack with PostgreSQL
|
||||||
|
├── docker-compose.local.yml # Local dev: DB only (run services manually)
|
||||||
|
└── .env.example # Template for environment variables
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Production Deployment (AWS / Any Linux Server)
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Docker & Docker Compose installed
|
||||||
|
- At least 2 CPU cores and 2GB RAM
|
||||||
|
|
||||||
|
### 1. Clone the repository
|
||||||
|
```bash
|
||||||
|
git clone https://repo.qruize.com/VithobaaSrinivasakumar/Autopilot.git
|
||||||
|
cd Autopilot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure environment variables
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env # Fill in DB_PASS, JWT_SECRET, AWS keys, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy the full stack
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts:
|
||||||
|
- **PostgreSQL** (database, internal only)
|
||||||
|
- **AutoPilot Master** on port `9090`
|
||||||
|
- **AutoPilot UI** on port `80` (Nginx)
|
||||||
|
|
||||||
|
### 4. Verify deployment
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
curl http://localhost:9090/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Updating to a new version
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Local Development (No Docker for Services)
|
||||||
|
|
||||||
|
### 1. Start only the database
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.local.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start the Master (Terminal 1)
|
||||||
|
```bash
|
||||||
|
cd master
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
# Runs on http://localhost:9090
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start the UI (Terminal 2)
|
||||||
|
```bash
|
||||||
|
cd ui
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
# Runs on http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Start the Worker (Terminal 3)
|
||||||
|
```bash
|
||||||
|
cd worker
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
# Polls http://localhost:9090 for jobs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 CI/CD Integration
|
||||||
|
|
||||||
|
AutoPilot can be triggered directly from your DevOps pipeline using an API key.
|
||||||
|
|
||||||
|
### Trigger a test suite
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer ap_live_your_api_key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"suiteName": "Smoke Tests", "browser": "chrome"}' \
|
||||||
|
https://your-domain.com/api/v1/suites/trigger-by-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Poll for results
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer ap_live_your_api_key" \
|
||||||
|
https://your-domain.com/api/v1/schedulers/{schedulerId}/execution-status
|
||||||
|
# exitCode: 0 = all passed, 1 = failures, 2 = still running
|
||||||
|
```
|
||||||
|
|
||||||
|
For full GitHub Actions, GitLab CI, and Jenkins snippets, see the **CI/CD Integration** page inside the dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Environment Variables Reference
|
||||||
|
|
||||||
|
| Variable | Description | Required |
|
||||||
|
|---|---|---|
|
||||||
|
| `DB_URL` | JDBC connection string to PostgreSQL | ✅ |
|
||||||
|
| `DB_USER` | Database username | ✅ |
|
||||||
|
| `DB_PASS` | Database password | ✅ |
|
||||||
|
| `JWT_SECRET` | Minimum 256-bit secret for signing tokens | ✅ |
|
||||||
|
| `AWS_REGION` | AWS region for S3 screenshot storage | Optional |
|
||||||
|
| `AWS_ACCESS_KEY_ID` | AWS access key | Optional |
|
||||||
|
| `AWS_SECRET_ACCESS_KEY` | AWS secret key | Optional |
|
||||||
|
| `S3_BUCKET_NAME` | S3 bucket for storing screenshots | Optional |
|
||||||
|
| `LOCALAGENT_CLOUD_URL` | URL the Worker uses to reach the Master | Worker only |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 API Reference
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `POST` | `/api/v1/suites/{id}/trigger` | Trigger suite by ID |
|
||||||
|
| `POST` | `/api/v1/suites/trigger-by-name` | Trigger suite by name |
|
||||||
|
| `GET` | `/api/v1/schedulers/{id}/execution-status` | Poll execution result |
|
||||||
|
| `GET` | `/api/v1/executions/{id}/status` | Get execution detail |
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
# ─── LOCAL DEVELOPMENT OVERRIDE ───────────────────────────────────────────────
|
||||||
|
# Run with: docker-compose -f docker-compose.yml -f docker-compose.local.yml up
|
||||||
|
# Or just: docker-compose -f docker-compose.local.yml up
|
||||||
|
#
|
||||||
|
# This starts ONLY the database. Master, Worker, and UI are run directly
|
||||||
|
# from your terminal for fast hot-reloading during development.
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: autopilot-postgres-local
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: localagent_cloud
|
||||||
|
POSTGRES_USER: localagent
|
||||||
|
POSTGRES_PASSWORD: localagent
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_local_data:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_local_data:
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
# ─── PostgreSQL Database ──────────────────────────────────────────────────
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: autopilot-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: localagent_cloud
|
||||||
|
POSTGRES_USER: ${DB_USER}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASS}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d localagent_cloud"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# ─── AutoPilot Master (Cloud Coordinator Backend) ─────────────────────────
|
||||||
|
master:
|
||||||
|
build:
|
||||||
|
context: ./master
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: autopilot/master:latest
|
||||||
|
container_name: autopilot-master
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
environment:
|
||||||
|
- SPRING_PROFILES_ACTIVE=prod
|
||||||
|
- DB_URL=${DB_URL}
|
||||||
|
- DB_USER=${DB_USER}
|
||||||
|
- DB_PASS=${DB_PASS}
|
||||||
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- AWS_REGION=${AWS_REGION}
|
||||||
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||||
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||||
|
- S3_BUCKET_NAME=${S3_BUCKET_NAME}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ─── AutoPilot UI (React Frontend via Nginx) ───────────────────────────────
|
||||||
|
ui:
|
||||||
|
build:
|
||||||
|
context: ./ui
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: autopilot/ui:latest
|
||||||
|
container_name: autopilot-ui
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
depends_on:
|
||||||
|
- master
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ─── AutoPilot Worker (Execution Engine) ──────────────────────────────────
|
||||||
|
# NOTE: The worker needs access to a real browser (Chrome/Firefox).
|
||||||
|
# For headless browser automation, run the worker on a separate machine
|
||||||
|
# or a server with Xvfb installed. Uncomment if running on a capable host.
|
||||||
|
#
|
||||||
|
# worker:
|
||||||
|
# build:
|
||||||
|
# context: ./worker
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# image: autopilot/worker:latest
|
||||||
|
# container_name: autopilot-worker
|
||||||
|
# environment:
|
||||||
|
# - LOCALAGENT_CLOUD_URL=${LOCALAGENT_CLOUD_URL}
|
||||||
|
# - LOCALAGENT_POLLING_ENABLED=${LOCALAGENT_POLLING_ENABLED}
|
||||||
|
# depends_on:
|
||||||
|
# - master
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Build outputs
|
||||||
|
/target/
|
||||||
|
**/target/
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Maven wrapper downloads
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
# Local runtime data
|
||||||
|
/data/
|
||||||
|
**/data/
|
||||||
|
localagent-cloud/data/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
*.log.*
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Java
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
|
||||||
|
# Test reports
|
||||||
|
**/surefire-reports/
|
||||||
|
|
||||||
|
# Screenshots and generated artifacts
|
||||||
|
screenshots/
|
||||||
|
**/screenshots/
|
||||||
|
|
||||||
|
# Vendor / dependency folders if they are generated locally
|
||||||
|
vendor/
|
||||||
|
vendor_slim/
|
||||||
|
|
||||||
|
# Local agent runtime files
|
||||||
|
localagent/driver_log.txt
|
||||||
|
localagent/*.log
|
||||||
|
localagent/**/*.log
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
wrapperVersion=3.3.4
|
||||||
|
distributionType=only-script
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Stage 1: Build Backend
|
||||||
|
FROM maven:3.9.6-eclipse-temurin-21-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pom.xml .
|
||||||
|
RUN mvn dependency:go-offline -B
|
||||||
|
COPY src ./src
|
||||||
|
RUN mvn clean package -DskipTests -B
|
||||||
|
|
||||||
|
# Stage 2: Run (minimal JRE image)
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/target/autopilot-master-0.0.1-SNAPSHOT.jar app.jar
|
||||||
|
EXPOSE 9090
|
||||||
|
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
# AutoPilot Platform
|
||||||
|
|
||||||
|
AutoPilot is a high-performance web browser automation platform designed for distributed execution orchestration. It consists of three core components:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
localagent/
|
||||||
|
├── autopilot-ui/ # React/Vite Frontend (Web Dashboard)
|
||||||
|
├── autopilot-master/ # Cloud Coordinator Broker service (Spring Boot)
|
||||||
|
├── autopilot-worker/ # AutoPropel LocalAgent application runner (Spring Boot)
|
||||||
|
├── service-packaging/ # Windows Service wrappers and registration scripts
|
||||||
|
├── docker-compose.yml # Local development database infrastructure
|
||||||
|
└── pom.xml # Workspace parent POM for the Java projects
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Prerequisites
|
||||||
|
- **Java**: JDK 21 or JDK 25 installed and configured on your `PATH`.
|
||||||
|
- **Node.js**: v18+ (for running the React UI).
|
||||||
|
- **Browsers**: Google Chrome and/or Mozilla Firefox installed in default system paths.
|
||||||
|
- **Docker**: For running the local PostgreSQL database.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Local Development Setup
|
||||||
|
|
||||||
|
To run the entire distributed platform on your local machine, follow these steps in order:
|
||||||
|
|
||||||
|
### 1. Start the Local Database
|
||||||
|
We use Docker to spin up a local PostgreSQL database for development, so you don't need to connect to AWS.
|
||||||
|
From the root of the project:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
*(This starts PostgreSQL on port `5432` with credentials `user: localagent`, `password: localagent`)*
|
||||||
|
|
||||||
|
### 2. Start the Master (Cloud Coordinator)
|
||||||
|
The Master broker manages the database, serves the API, and schedules jobs.
|
||||||
|
Open a new terminal at the root of the project:
|
||||||
|
```bash
|
||||||
|
.\mvnw.cmd clean spring-boot:run -pl autopilot-master -Dspring-boot.run.profiles=default -DJWT_SECRET=supersecretjwtkeythatismorethan256bits
|
||||||
|
```
|
||||||
|
*(The Master will start on port `9090`)*
|
||||||
|
|
||||||
|
### 3. Start the UI (React Frontend)
|
||||||
|
The frontend dashboard allows you to visually interact with the platform.
|
||||||
|
Open a new terminal at the root of the project:
|
||||||
|
```bash
|
||||||
|
cd autopilot-ui
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
*(The UI will start on port `5173` and proxy API requests to the Master on `9090`)*
|
||||||
|
|
||||||
|
### 4. Start the Worker (Execution Engine)
|
||||||
|
The Worker actually drives the browser and runs the tests. It will automatically connect to your local Master to fetch jobs.
|
||||||
|
Open a new terminal at the root of the project:
|
||||||
|
```bash
|
||||||
|
.\mvnw.cmd clean spring-boot:run -pl autopilot-worker/localagent-java
|
||||||
|
```
|
||||||
|
*(The Worker will start on port `8080` and begin polling `http://localhost:9090` for jobs)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ☁️ Cloud / Production Deployment
|
||||||
|
When deploying to AWS or another cloud provider:
|
||||||
|
1. Deploy `autopilot-master` to an EC2 instance or ECS container and point it to a managed PostgreSQL RDS database using `DB_URL`, `DB_USER`, and `DB_PASS`.
|
||||||
|
2. Build and deploy `autopilot-ui` (e.g. via S3 + CloudFront or an Nginx container).
|
||||||
|
3. Distribute the `autopilot-worker` to the testing nodes. Set the `LOCALAGENT_CLOUD_URL` environment variable on the workers to point to your public Master API.
|
||||||
|
|
||||||
|
Example for Worker:
|
||||||
|
```bash
|
||||||
|
set LOCALAGENT_CLOUD_URL=https://api.yourdomain.com
|
||||||
|
set LOCALAGENT_AGENT_ID=agent_windows_01
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: autopilot/backend:latest
|
||||||
|
container_name: autopilot-backend
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- SPRING_PROFILES_ACTIVE=prod
|
||||||
|
# If you have a local postgres DB running, uncomment and configure:
|
||||||
|
# - SPRING_DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/autopilot
|
||||||
|
# - SPRING_DATASOURCE_USERNAME=postgres
|
||||||
|
# - SPRING_DATASOURCE_PASSWORD=postgres
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
let content = fs.readFileSync('frontend/src/App.jsx', 'utf8');
|
||||||
|
|
||||||
|
// Replace fetch('/api/...) but not /api/auth
|
||||||
|
content = content.replace(/\bfetch\((['"`])\/api\/(?!auth)/g, 'api($1/api/');
|
||||||
|
|
||||||
|
// Replace fetch(url
|
||||||
|
content = content.replace(/\bfetch\(url/g, 'api(url');
|
||||||
|
|
||||||
|
fs.writeFileSync('frontend/src/App.jsx', content);
|
||||||
|
console.log('App.jsx updated');
|
||||||
|
|
@ -0,0 +1,295 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
#
|
||||||
|
# Optional ENV vars
|
||||||
|
# -----------------
|
||||||
|
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||||
|
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -euf
|
||||||
|
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||||
|
|
||||||
|
# OS specific support.
|
||||||
|
native_path() { printf %s\\n "$1"; }
|
||||||
|
case "$(uname)" in
|
||||||
|
CYGWIN* | MINGW*)
|
||||||
|
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||||
|
native_path() { cygpath --path --windows "$1"; }
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# set JAVACMD and JAVACCMD
|
||||||
|
set_java_home() {
|
||||||
|
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||||
|
if [ -n "${JAVA_HOME-}" ]; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||||
|
|
||||||
|
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||||
|
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||||
|
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v java
|
||||||
|
)" || :
|
||||||
|
JAVACCMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v javac
|
||||||
|
)" || :
|
||||||
|
|
||||||
|
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||||
|
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# hash string like Java String::hashCode
|
||||||
|
hash_string() {
|
||||||
|
str="${1:-}" h=0
|
||||||
|
while [ -n "$str" ]; do
|
||||||
|
char="${str%"${str#?}"}"
|
||||||
|
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||||
|
str="${str#?}"
|
||||||
|
done
|
||||||
|
printf %x\\n $h
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose() { :; }
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf %s\\n "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
trim() {
|
||||||
|
# MWRAPPER-139:
|
||||||
|
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||||
|
# Needed for removing poorly interpreted newline sequences when running in more
|
||||||
|
# exotic environments such as mingw bash on Windows.
|
||||||
|
printf "%s" "${1}" | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptDir="$(dirname "$0")"
|
||||||
|
scriptName="$(basename "$0")"
|
||||||
|
|
||||||
|
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
while IFS="=" read -r key value; do
|
||||||
|
case "${key-}" in
|
||||||
|
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||||
|
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||||
|
esac
|
||||||
|
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
|
||||||
|
case "${distributionUrl##*/}" in
|
||||||
|
maven-mvnd-*bin.*)
|
||||||
|
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||||
|
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||||
|
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||||
|
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||||
|
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||||
|
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||||
|
*)
|
||||||
|
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||||
|
distributionPlatform=linux-amd64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||||
|
;;
|
||||||
|
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||||
|
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||||
|
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||||
|
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||||
|
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||||
|
|
||||||
|
exec_maven() {
|
||||||
|
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||||
|
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -d "$MAVEN_HOME" ]; then
|
||||||
|
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
exec_maven "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${distributionUrl-}" in
|
||||||
|
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||||
|
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||||
|
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||||
|
trap clean HUP INT TERM EXIT
|
||||||
|
else
|
||||||
|
die "cannot create temp dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
verbose "Downloading from: $distributionUrl"
|
||||||
|
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
# select .zip or .tar.gz
|
||||||
|
if ! command -v unzip >/dev/null; then
|
||||||
|
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# verbose opt
|
||||||
|
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||||
|
|
||||||
|
# normalize http auth
|
||||||
|
case "${MVNW_PASSWORD:+has-password}" in
|
||||||
|
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||||
|
verbose "Found wget ... using wget"
|
||||||
|
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||||
|
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||||
|
verbose "Found curl ... using curl"
|
||||||
|
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||||
|
elif set_java_home; then
|
||||||
|
verbose "Falling back to use Java to download"
|
||||||
|
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||||
|
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
cat >"$javaSource" <<-END
|
||||||
|
public class Downloader extends java.net.Authenticator
|
||||||
|
{
|
||||||
|
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||||
|
{
|
||||||
|
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||||
|
}
|
||||||
|
public static void main( String[] args ) throws Exception
|
||||||
|
{
|
||||||
|
setDefault( new Downloader() );
|
||||||
|
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END
|
||||||
|
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||||
|
verbose " - Compiling Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||||
|
verbose " - Running Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
if [ -n "${distributionSha256Sum-}" ]; then
|
||||||
|
distributionSha256Result=false
|
||||||
|
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||||
|
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||||
|
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
elif command -v sha256sum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
elif command -v shasum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||||
|
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ $distributionSha256Result = false ]; then
|
||||||
|
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||||
|
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
if command -v unzip >/dev/null; then
|
||||||
|
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||||
|
else
|
||||||
|
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
actualDistributionDir=""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||||
|
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$distributionUrlNameMain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
# enable globbing to iterate over items
|
||||||
|
set +f
|
||||||
|
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$(basename "$dir")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
set -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||||
|
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||||
|
die "Could not find Maven distribution directory in extracted archive"
|
||||||
|
fi
|
||||||
|
|
||||||
|
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||||
|
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||||
|
|
||||||
|
clean || :
|
||||||
|
exec_maven "$@"
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
<# : batch portion
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
@REM or more contributor license agreements. See the NOTICE file
|
||||||
|
@REM distributed with this work for additional information
|
||||||
|
@REM regarding copyright ownership. The ASF licenses this file
|
||||||
|
@REM to you under the Apache License, Version 2.0 (the
|
||||||
|
@REM "License"); you may not use this file except in compliance
|
||||||
|
@REM with the License. You may obtain a copy of the License at
|
||||||
|
@REM
|
||||||
|
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@REM
|
||||||
|
@REM Unless required by applicable law or agreed to in writing,
|
||||||
|
@REM software distributed under the License is distributed on an
|
||||||
|
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
@REM KIND, either express or implied. See the License for the
|
||||||
|
@REM specific language governing permissions and limitations
|
||||||
|
@REM under the License.
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
@REM
|
||||||
|
@REM Optional ENV vars
|
||||||
|
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||||
|
@SET __MVNW_CMD__=
|
||||||
|
@SET __MVNW_ERROR__=
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||||
|
@SET PSModulePath=
|
||||||
|
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||||
|
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||||
|
)
|
||||||
|
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=
|
||||||
|
@SET __MVNW_ARG0_NAME__=
|
||||||
|
@SET MVNW_USERNAME=
|
||||||
|
@SET MVNW_PASSWORD=
|
||||||
|
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||||
|
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||||
|
@GOTO :EOF
|
||||||
|
: end batch / begin powershell #>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if ($env:MVNW_VERBOSE -eq "true") {
|
||||||
|
$VerbosePreference = "Continue"
|
||||||
|
}
|
||||||
|
|
||||||
|
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||||
|
if (!$distributionUrl) {
|
||||||
|
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||||
|
"maven-mvnd-*" {
|
||||||
|
$USE_MVND = $true
|
||||||
|
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||||
|
$MVN_CMD = "mvnd.cmd"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
$USE_MVND = $false
|
||||||
|
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
if ($env:MVNW_REPOURL) {
|
||||||
|
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||||
|
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||||
|
}
|
||||||
|
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||||
|
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||||
|
|
||||||
|
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||||
|
if ($env:MAVEN_USER_HOME) {
|
||||||
|
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||||
|
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_WRAPPER_DISTS = $null
|
||||||
|
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||||
|
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||||
|
} else {
|
||||||
|
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||||
|
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||||
|
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||||
|
|
||||||
|
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||||
|
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
exit $?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||||
|
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||||
|
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||||
|
trap {
|
||||||
|
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
Write-Verbose "Downloading from: $distributionUrl"
|
||||||
|
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
$webclient = New-Object System.Net.WebClient
|
||||||
|
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||||
|
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||||
|
}
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||||
|
if ($distributionSha256Sum) {
|
||||||
|
if ($USE_MVND) {
|
||||||
|
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||||
|
}
|
||||||
|
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||||
|
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||||
|
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
$actualDistributionDir = ""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||||
|
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||||
|
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||||
|
$actualDistributionDir = $distributionUrlNameMain
|
||||||
|
}
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||||
|
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||||
|
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||||
|
$actualDistributionDir = $_.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||||
|
try {
|
||||||
|
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||||
|
} catch {
|
||||||
|
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||||
|
Write-Error "fail to move MAVEN_HOME"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.4.0</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.autopilot</groupId>
|
||||||
|
<artifactId>autopilot-master</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>autopilot-master</name>
|
||||||
|
<description>Cloud Coordinator for AutoPilot Local Agent</description>
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-database-postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.persistence</groupId>
|
||||||
|
<artifactId>jakarta.persistence-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- AWS SDK for S3 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>software.amazon.awssdk</groupId>
|
||||||
|
<artifactId>s3</artifactId>
|
||||||
|
<version>2.25.11</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- Spring Security -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- JWT -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.autopilot.localagent_cloud;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
|
public class LocalagentCloudApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(LocalagentCloudApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.autopilot.localagent_cloud;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class MvpUiViewController {
|
||||||
|
|
||||||
|
@GetMapping({
|
||||||
|
"/autopilot",
|
||||||
|
"/autopilot/",
|
||||||
|
"/autopilot/login",
|
||||||
|
"/autopilot/register",
|
||||||
|
"/autopilot/dashboard",
|
||||||
|
"/autopilot/scheduler",
|
||||||
|
"/autopilot/scheduler/**",
|
||||||
|
"/autopilot/groups",
|
||||||
|
"/autopilot/groups/**",
|
||||||
|
"/autopilot/test-cases",
|
||||||
|
"/autopilot/test-cases/**",
|
||||||
|
"/autopilot/test-case-groups",
|
||||||
|
"/autopilot/test-case-groups/**",
|
||||||
|
"/autopilot/test-suites",
|
||||||
|
"/autopilot/test-suites/**",
|
||||||
|
"/autopilot/stub/**"
|
||||||
|
})
|
||||||
|
public String dashboardRedirect() {
|
||||||
|
return "forward:/autopilot/index.html";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.autopilot.localagent_cloud.aiengine;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.service.ExecutionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intelligent Decision Engine Layer
|
||||||
|
* Implements rule-based validation and AI risk models.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/ai-engine")
|
||||||
|
public class DecisionEngineController {
|
||||||
|
|
||||||
|
private final ExecutionService executionService;
|
||||||
|
|
||||||
|
public DecisionEngineController(ExecutionService executionService) {
|
||||||
|
this.executionService = executionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@GetMapping("/predict-release-risk")
|
||||||
|
public ResponseEntity<Map<String, Object>> predictRisk(@RequestParam Long executionId) {
|
||||||
|
ResponseEntity<Map<String, Object>> res = executionService.getById(executionId);
|
||||||
|
if (res.getStatusCode() != HttpStatus.OK || res.getBody() == null) {
|
||||||
|
return ResponseEntity.status(res.getStatusCode()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> executionData = res.getBody();
|
||||||
|
Map<String, Object> execution = (Map<String, Object>) executionData.get("execution");
|
||||||
|
|
||||||
|
int passed = execution.get("passedCount") != null ? ((Number) execution.get("passedCount")).intValue() : 0;
|
||||||
|
int failed = execution.get("failedCount") != null ? ((Number) execution.get("failedCount")).intValue() : 0;
|
||||||
|
|
||||||
|
String decision;
|
||||||
|
double qualityScore;
|
||||||
|
String[] riskFactors;
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
decision = "REJECTED_HIGH_RISK";
|
||||||
|
qualityScore = 0.0;
|
||||||
|
riskFactors = new String[]{failed + " tests failed during execution"};
|
||||||
|
} else if (passed > 0 && failed == 0) {
|
||||||
|
decision = "APPROVED";
|
||||||
|
qualityScore = 1.0;
|
||||||
|
riskFactors = new String[]{"All tests passed successfully"};
|
||||||
|
} else {
|
||||||
|
decision = "CONDITIONAL_DEPLOYMENT";
|
||||||
|
qualityScore = 0.5;
|
||||||
|
riskFactors = new String[]{"Insufficient test data. " + passed + " passed, " + failed + " failed"};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"executionId", executionId,
|
||||||
|
"qualityScore", qualityScore,
|
||||||
|
"decision", decision,
|
||||||
|
"riskFactors", riskFactors
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.autopilot.localagent_cloud.analytics;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.service.AnalyticsService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Normalization & Quality Feature Engineering
|
||||||
|
* Handles aggregated metrics, pass/fail rates, and coverage data.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/analytics")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class AnalyticsController {
|
||||||
|
|
||||||
|
private final AnalyticsService analyticsService;
|
||||||
|
|
||||||
|
public AnalyticsController(AnalyticsService analyticsService) {
|
||||||
|
this.analyticsService = analyticsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/dashboard/summary")
|
||||||
|
public ResponseEntity<Map<String, Object>> getQualitySummary() {
|
||||||
|
// TODO: Aggregate test data from DB and calculate pass/fail and coverage rates
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"totalTests", 0,
|
||||||
|
"passRate", 0.0,
|
||||||
|
"flakyTestsDetected", 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/analytics/flaky-suites
|
||||||
|
* Returns the top N flakiest test suites for the authenticated org.
|
||||||
|
*/
|
||||||
|
@GetMapping("/flaky-suites")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getFlakySuites(
|
||||||
|
@RequestParam(defaultValue = "5") int limit,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
List<Map<String, Object>> data = analyticsService.getFlakySuites(orgId, limit);
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/analytics/fleet-health
|
||||||
|
* Returns live fleet status: agent statuses, counts, and queue depth.
|
||||||
|
*/
|
||||||
|
@GetMapping("/fleet-health")
|
||||||
|
public ResponseEntity<Map<String, Object>> getFleetHealth(HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
Map<String, Object> data = analyticsService.getFleetHealth(orgId);
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/analytics/suite-performance
|
||||||
|
* Returns ranked list of test suites by success rate, avg duration, and run count.
|
||||||
|
*/
|
||||||
|
@GetMapping("/suite-performance")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getSuitePerformance(
|
||||||
|
@RequestParam(defaultValue = "10") int limit,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
List<Map<String, Object>> data = analyticsService.getSuitePerformance(orgId, limit);
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/analytics/time-saved-roi
|
||||||
|
* Returns ROI metrics: hours saved, dollar value saved, runs automated,
|
||||||
|
* vs estimated manual testing time. Includes 14-day daily breakdown.
|
||||||
|
*/
|
||||||
|
@GetMapping("/time-saved-roi")
|
||||||
|
public ResponseEntity<Map<String, Object>> getTimeSavedRoi(HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
Map<String, Object> data = analyticsService.getTimeSavedRoi(orgId);
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.autopilot.localagent_cloud.auth;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.AppUser;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AppUserRepository;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AppUserDetailsService implements UserDetailsService {
|
||||||
|
private final AppUserRepository userRepository;
|
||||||
|
public AppUserDetailsService(AppUserRepository userRepository) { this.userRepository = userRepository; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
|
||||||
|
AppUser user = userRepository.findByEmail(email)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
|
||||||
|
return new org.springframework.security.core.userdetails.User(
|
||||||
|
user.getEmail(),
|
||||||
|
user.getPasswordHash(),
|
||||||
|
List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().toUpperCase()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
package com.autopilot.localagent_cloud.auth;
|
||||||
|
import com.autopilot.localagent_cloud.model.*;
|
||||||
|
import com.autopilot.localagent_cloud.repository.*;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class AuthController {
|
||||||
|
private final AppUserRepository userRepository;
|
||||||
|
private final OrganisationRepository orgRepository;
|
||||||
|
private final AgentTokenRepository agentTokenRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
|
public AuthController(AppUserRepository userRepository,
|
||||||
|
OrganisationRepository orgRepository,
|
||||||
|
AgentTokenRepository agentTokenRepository,
|
||||||
|
PasswordEncoder passwordEncoder,
|
||||||
|
JwtUtil jwtUtil) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.orgRepository = orgRepository;
|
||||||
|
this.agentTokenRepository = agentTokenRepository;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.jwtUtil = jwtUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/auth/register — creates an org + user, returns JWT */
|
||||||
|
@PostMapping("/register")
|
||||||
|
public ResponseEntity<Map<String, Object>> register(@RequestBody Map<String, String> body) {
|
||||||
|
String email = body.get("email");
|
||||||
|
String password = body.get("password");
|
||||||
|
String fullName = body.getOrDefault("fullName", "");
|
||||||
|
String orgName = body.getOrDefault("orgName", fullName + "'s Organisation");
|
||||||
|
|
||||||
|
if (email == null || password == null || email.isBlank() || password.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Email and password are required"));
|
||||||
|
}
|
||||||
|
if (userRepository.existsByEmail(email)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("error", "Email already registered"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create organisation
|
||||||
|
Organisation org = new Organisation();
|
||||||
|
org.setName(orgName);
|
||||||
|
org.setPlan("trial");
|
||||||
|
|
||||||
|
// Generate subdomain
|
||||||
|
String baseSlug = orgName.toLowerCase().replaceAll("[^a-z0-9]+", "-");
|
||||||
|
if (baseSlug.startsWith("-")) baseSlug = baseSlug.substring(1);
|
||||||
|
if (baseSlug.endsWith("-")) baseSlug = baseSlug.substring(0, baseSlug.length() - 1);
|
||||||
|
if (baseSlug.isBlank()) baseSlug = "org";
|
||||||
|
|
||||||
|
String slug = baseSlug;
|
||||||
|
int counter = 1;
|
||||||
|
while (orgRepository.findBySubdomain(slug) != null) {
|
||||||
|
slug = baseSlug + "-" + counter;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
org.setSubdomain(slug);
|
||||||
|
|
||||||
|
// Generate unique public ID like VIT-49281
|
||||||
|
String namePrefix = orgName.replaceAll("[^a-zA-Z0-9]", "").toUpperCase();
|
||||||
|
namePrefix = namePrefix.length() >= 3 ? namePrefix.substring(0, 3) : namePrefix;
|
||||||
|
String publicId;
|
||||||
|
do {
|
||||||
|
publicId = namePrefix + "-" + String.format("%05d", (int)(Math.random() * 99999));
|
||||||
|
} while (orgRepository.findByPublicId(publicId) != null);
|
||||||
|
org.setPublicId(publicId);
|
||||||
|
org = orgRepository.save(org);
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
AppUser user = new AppUser();
|
||||||
|
user.setOrgId(org.getId());
|
||||||
|
user.setEmail(email);
|
||||||
|
user.setPasswordHash(passwordEncoder.encode(password));
|
||||||
|
user.setFullName(fullName);
|
||||||
|
user.setRole("admin");
|
||||||
|
user = userRepository.save(user);
|
||||||
|
|
||||||
|
// Auto-generate first agent token
|
||||||
|
AgentToken token = new AgentToken();
|
||||||
|
token.setOrgId(org.getId());
|
||||||
|
token.setToken("agt_" + UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
token.setLabel("Default Agent");
|
||||||
|
agentTokenRepository.save(token);
|
||||||
|
|
||||||
|
String jwt = jwtUtil.generateToken(user.getEmail(), org.getId(), user.getRole());
|
||||||
|
Map<String, Object> registerResp = new HashMap<>();
|
||||||
|
registerResp.put("token", jwt);
|
||||||
|
registerResp.put("email", user.getEmail());
|
||||||
|
registerResp.put("fullName", user.getFullName() != null ? user.getFullName() : "");
|
||||||
|
registerResp.put("orgId", org.getId());
|
||||||
|
registerResp.put("orgPublicId", org.getPublicId() != null ? org.getPublicId() : "");
|
||||||
|
registerResp.put("orgName", org.getName());
|
||||||
|
registerResp.put("subdomain", org.getSubdomain());
|
||||||
|
registerResp.put("plan", org.getPlan());
|
||||||
|
registerResp.put("role", user.getRole());
|
||||||
|
registerResp.put("agentToken", token.getToken());
|
||||||
|
registerResp.put("requiresPasswordChange", user.isRequiresPasswordChange());
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(registerResp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/auth/login — verify credentials, return JWT */
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> body) {
|
||||||
|
String email = body.get("email");
|
||||||
|
String password = body.get("password");
|
||||||
|
if (email == null || password == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Email and password required"));
|
||||||
|
}
|
||||||
|
AppUser user = userRepository.findByEmail(email).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", "No such user. Please create an account."));
|
||||||
|
}
|
||||||
|
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", "Invalid credentials"));
|
||||||
|
}
|
||||||
|
Organisation org = orgRepository.findById(user.getOrgId()).orElseThrow();
|
||||||
|
String agentToken = agentTokenRepository.findByOrgId(org.getId())
|
||||||
|
.stream().findFirst().map(AgentToken::getToken).orElse("");
|
||||||
|
|
||||||
|
String jwt = jwtUtil.generateToken(user.getEmail(), org.getId(), user.getRole());
|
||||||
|
Map<String, Object> loginResp = new HashMap<>();
|
||||||
|
loginResp.put("token", jwt);
|
||||||
|
loginResp.put("email", user.getEmail());
|
||||||
|
loginResp.put("fullName", user.getFullName() != null ? user.getFullName() : "");
|
||||||
|
loginResp.put("orgId", org.getId());
|
||||||
|
loginResp.put("orgPublicId", org.getPublicId() != null ? org.getPublicId() : "");
|
||||||
|
loginResp.put("orgName", org.getName());
|
||||||
|
loginResp.put("subdomain", org.getSubdomain());
|
||||||
|
loginResp.put("plan", org.getPlan());
|
||||||
|
loginResp.put("role", user.getRole());
|
||||||
|
loginResp.put("agentToken", agentToken);
|
||||||
|
loginResp.put("requiresPasswordChange", user.isRequiresPasswordChange());
|
||||||
|
return ResponseEntity.ok(loginResp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/auth/me — get current user info from JWT */
|
||||||
|
@GetMapping("/me")
|
||||||
|
public ResponseEntity<Map<String, Object>> me(jakarta.servlet.http.HttpServletRequest req) {
|
||||||
|
Long orgId = (Long) req.getAttribute("orgId");
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
Organisation org = orgRepository.findById(orgId).orElse(null);
|
||||||
|
if (org == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
String agentToken = agentTokenRepository.findByOrgId(orgId)
|
||||||
|
.stream().findFirst().map(AgentToken::getToken).orElse("");
|
||||||
|
AppUser currentUser = userRepository.findByEmail((String) req.getAttribute("email")).orElse(null);
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"orgId", orgId,
|
||||||
|
"orgPublicId", org.getPublicId() != null ? org.getPublicId() : "",
|
||||||
|
"orgName", org.getName(),
|
||||||
|
"subdomain", org.getSubdomain(),
|
||||||
|
"plan", org.getPlan(),
|
||||||
|
"role", currentUser != null ? currentUser.getRole() : "user",
|
||||||
|
"agentToken", agentToken,
|
||||||
|
"requiresPasswordChange", currentUser != null && currentUser.isRequiresPasswordChange()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/auth/agent-tokens — generate a new agent token for the org */
|
||||||
|
@PostMapping("/agent-tokens")
|
||||||
|
public ResponseEntity<Map<String, Object>> createAgentToken(
|
||||||
|
@RequestBody(required = false) Map<String, String> body,
|
||||||
|
jakarta.servlet.http.HttpServletRequest req) {
|
||||||
|
Long orgId = (Long) req.getAttribute("orgId");
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
AgentToken token = new AgentToken();
|
||||||
|
token.setOrgId(orgId);
|
||||||
|
token.setToken("agt_" + UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
token.setLabel(body != null ? body.getOrDefault("label", "Agent") : "Agent");
|
||||||
|
agentTokenRepository.save(token);
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(Map.of(
|
||||||
|
"token", token.getToken(),
|
||||||
|
"label", token.getLabel(),
|
||||||
|
"id", token.getId()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/** GET /api/auth/users — list all users in the organization */
|
||||||
|
@GetMapping("/users")
|
||||||
|
public ResponseEntity<java.util.List<AppUser>> getOrgUsers(jakarta.servlet.http.HttpServletRequest req) {
|
||||||
|
Long orgId = (Long) req.getAttribute("orgId");
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
return ResponseEntity.ok(userRepository.findByOrgId(orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/auth/users/invite — invite/create a user in the org */
|
||||||
|
@PostMapping("/users/invite")
|
||||||
|
public ResponseEntity<Map<String, Object>> inviteUser(
|
||||||
|
@RequestBody Map<String, String> body,
|
||||||
|
jakarta.servlet.http.HttpServletRequest req) {
|
||||||
|
Long orgId = (Long) req.getAttribute("orgId");
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
// Only admins can invite users
|
||||||
|
String callerEmail = (String) req.getAttribute("email");
|
||||||
|
AppUser caller = callerEmail != null ? userRepository.findByEmail(callerEmail).orElse(null) : null;
|
||||||
|
if (caller == null || !"admin".equals(caller.getRole())) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("error", "Only administrators can invite users"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String email = body.get("email");
|
||||||
|
String role = body.getOrDefault("role", "user");
|
||||||
|
String fullName = body.getOrDefault("fullName", "");
|
||||||
|
|
||||||
|
if (email == null || email.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Email is required"));
|
||||||
|
}
|
||||||
|
if (userRepository.existsByEmail(email)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("error", "Email already registered"));
|
||||||
|
}
|
||||||
|
|
||||||
|
AppUser user = new AppUser();
|
||||||
|
user.setOrgId(orgId);
|
||||||
|
user.setEmail(email);
|
||||||
|
user.setFullName(fullName);
|
||||||
|
user.setRole(role);
|
||||||
|
|
||||||
|
// SECURITY FIX: Generate a stronger temporary password (16-char alphanumeric)
|
||||||
|
// Industry standard: NEVER return plaintext passwords in API responses.
|
||||||
|
// The password must be shared out-of-band (e.g., email, secure channel).
|
||||||
|
// We return it ONCE here as it cannot be recovered later — the admin must copy it immediately.
|
||||||
|
String randomPassword = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
|
||||||
|
user.setPasswordHash(passwordEncoder.encode(randomPassword));
|
||||||
|
user.setRequiresPasswordChange(true);
|
||||||
|
user = userRepository.save(user);
|
||||||
|
|
||||||
|
// NOTE: In production with email configured, send this to the user's email instead.
|
||||||
|
// The field is named 'initialPassword' not 'temporaryPassword' to make it clear this
|
||||||
|
// is a one-time display — it will not appear again after this response.
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(Map.of(
|
||||||
|
"id", user.getId(),
|
||||||
|
"email", user.getEmail(),
|
||||||
|
"role", user.getRole(),
|
||||||
|
"fullName", user.getFullName() != null ? user.getFullName() : "",
|
||||||
|
"initialPassword", randomPassword,
|
||||||
|
"notice", "Share this password with the user via a secure channel. It will not be shown again."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/auth/agent-tokens — list all agent tokens in the org */
|
||||||
|
@GetMapping("/agent-tokens")
|
||||||
|
public ResponseEntity<java.util.List<AgentToken>> getOrgAgentTokens(jakarta.servlet.http.HttpServletRequest req) {
|
||||||
|
Long orgId = (Long) req.getAttribute("orgId");
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
return ResponseEntity.ok(agentTokenRepository.findByOrgId(orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/agent-tokens/{id}")
|
||||||
|
public ResponseEntity<Void> deleteAgentToken(
|
||||||
|
@PathVariable Long id,
|
||||||
|
jakarta.servlet.http.HttpServletRequest req) {
|
||||||
|
Long orgId = (Long) req.getAttribute("orgId");
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
return agentTokenRepository.findById(id).map(token -> {
|
||||||
|
if (token.getOrgId().equals(orgId)) {
|
||||||
|
agentTokenRepository.delete(token);
|
||||||
|
return ResponseEntity.ok().<Void>build();
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).<Void>build();
|
||||||
|
}
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/change-password")
|
||||||
|
public ResponseEntity<Map<String, Object>> changePassword(
|
||||||
|
@RequestBody Map<String, String> body,
|
||||||
|
jakarta.servlet.http.HttpServletRequest req) {
|
||||||
|
String email = (String) req.getAttribute("email");
|
||||||
|
if (email == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
String newPassword = body.get("newPassword");
|
||||||
|
if (newPassword == null || newPassword.length() < 8) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Password must be at least 8 characters"));
|
||||||
|
}
|
||||||
|
|
||||||
|
AppUser user = userRepository.findByEmail(email).orElse(null);
|
||||||
|
if (user == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
user.setPasswordHash(passwordEncoder.encode(newPassword));
|
||||||
|
user.setRequiresPasswordChange(false);
|
||||||
|
userRepository.save(user);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Password updated successfully"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.autopilot.localagent_cloud.auth;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.repository.AppUserRepository;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtAuthFilter extends OncePerRequestFilter {
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
private final AppUserRepository userRepository;
|
||||||
|
private final com.autopilot.localagent_cloud.repository.ApiKeyRepository apiKeyRepository;
|
||||||
|
|
||||||
|
public JwtAuthFilter(JwtUtil jwtUtil, AppUserRepository userRepository, com.autopilot.localagent_cloud.repository.ApiKeyRepository apiKeyRepository) {
|
||||||
|
this.jwtUtil = jwtUtil;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.apiKeyRepository = apiKeyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
String header = req.getHeader("Authorization");
|
||||||
|
if (header != null && header.startsWith("Bearer ")) {
|
||||||
|
String token = header.substring(7);
|
||||||
|
if (jwtUtil.isValid(token)) {
|
||||||
|
String email = jwtUtil.extractEmail(token);
|
||||||
|
// Verify user actually exists in DB! (so if DB is wiped, token is invalid)
|
||||||
|
if (userRepository.existsByEmail(email)) {
|
||||||
|
Long orgId = jwtUtil.extractOrgId(token);
|
||||||
|
var claims = jwtUtil.extractClaims(token);
|
||||||
|
String role = claims.get("role", String.class);
|
||||||
|
// Store orgId and email in request attribute for controllers to read
|
||||||
|
req.setAttribute("orgId", orgId);
|
||||||
|
req.setAttribute("email", email);
|
||||||
|
var auth = new UsernamePasswordAuthenticationToken(
|
||||||
|
email, null,
|
||||||
|
List.of(new SimpleGrantedAuthority("ROLE_" + (role != null ? role.toUpperCase() : "USER")))
|
||||||
|
);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||||
|
}
|
||||||
|
} else if (token.startsWith("ap_live_")) {
|
||||||
|
// It's an API Key
|
||||||
|
apiKeyRepository.findByToken(token).ifPresent(key -> {
|
||||||
|
// Throttle updates: only save if lastUsedAt is null or older than 5 minutes
|
||||||
|
java.time.LocalDateTime now = java.time.LocalDateTime.now();
|
||||||
|
if (key.getLastUsedAt() == null || key.getLastUsedAt().isBefore(now.minusMinutes(5))) {
|
||||||
|
key.setLastUsedAt(now);
|
||||||
|
apiKeyRepository.save(key);
|
||||||
|
}
|
||||||
|
req.setAttribute("orgId", key.getOrgId());
|
||||||
|
var auth = new UsernamePasswordAuthenticationToken(
|
||||||
|
"api-key-" + key.getId(), null,
|
||||||
|
List.of(new SimpleGrantedAuthority("ROLE_API"))
|
||||||
|
);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chain.doFilter(req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.autopilot.localagent_cloud.auth;
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtUtil {
|
||||||
|
private final SecretKey key;
|
||||||
|
private final long expirationMs;
|
||||||
|
|
||||||
|
public JwtUtil(@Value("${autopilot.jwt.secret}") String secret,
|
||||||
|
@Value("${autopilot.jwt.expiration-ms}") long expirationMs) {
|
||||||
|
this.key = Keys.hmacShaKeyFor(secret.getBytes());
|
||||||
|
this.expirationMs = expirationMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(String email, Long orgId, String role) {
|
||||||
|
return Jwts.builder()
|
||||||
|
.subject(email)
|
||||||
|
.claim("orgId", orgId)
|
||||||
|
.claim("role", role)
|
||||||
|
.issuedAt(new Date())
|
||||||
|
.expiration(new Date(System.currentTimeMillis() + expirationMs))
|
||||||
|
.signWith(key)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Claims extractClaims(String token) {
|
||||||
|
return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String extractEmail(String token) { return extractClaims(token).getSubject(); }
|
||||||
|
public Long extractOrgId(String token) { return extractClaims(token).get("orgId", Long.class); }
|
||||||
|
|
||||||
|
public boolean isValid(String token) {
|
||||||
|
try { extractClaims(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
package com.autopilot.localagent_cloud.auth;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.AgentToken;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AgentTokenRepository;
|
||||||
|
import com.autopilot.localagent_cloud.model.AppUser;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AppUserRepository;
|
||||||
|
import com.autopilot.localagent_cloud.model.DevicePairing;
|
||||||
|
import com.autopilot.localagent_cloud.repository.DevicePairingRepository;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class PairingController {
|
||||||
|
|
||||||
|
private final DevicePairingRepository pairingRepository;
|
||||||
|
private final AgentTokenRepository tokenRepository;
|
||||||
|
private final AppUserRepository userRepository;
|
||||||
|
|
||||||
|
public PairingController(DevicePairingRepository pairingRepository, AgentTokenRepository tokenRepository, AppUserRepository userRepository) {
|
||||||
|
this.pairingRepository = pairingRepository;
|
||||||
|
this.tokenRepository = tokenRepository;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent calls this to start pairing.
|
||||||
|
*/
|
||||||
|
@PostMapping("/agents/pairing/start")
|
||||||
|
public ResponseEntity<Map<String, Object>> startPairing() {
|
||||||
|
String code = String.format("%06d", new Random().nextInt(999999));
|
||||||
|
|
||||||
|
// Ensure uniqueness
|
||||||
|
while (pairingRepository.findByPairingCode(code).isPresent()) {
|
||||||
|
code = String.format("%06d", new Random().nextInt(999999));
|
||||||
|
}
|
||||||
|
|
||||||
|
DevicePairing pairing = new DevicePairing();
|
||||||
|
pairing.setPairingCode(code);
|
||||||
|
pairing = pairingRepository.save(pairing);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("pairingId", pairing.getId());
|
||||||
|
response.put("code", pairing.getPairingCode());
|
||||||
|
response.put("expiresAt", pairing.getExpiresAt());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent polls this to see if user has verified the code.
|
||||||
|
*/
|
||||||
|
@GetMapping("/agents/pairing/{id}/status")
|
||||||
|
public ResponseEntity<Map<String, Object>> checkStatus(@PathVariable Long id) {
|
||||||
|
Optional<DevicePairing> opt = pairingRepository.findById(id);
|
||||||
|
if (opt.isEmpty()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
DevicePairing pairing = opt.get();
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
|
||||||
|
if (LocalDateTime.now().isAfter(pairing.getExpiresAt()) && "PENDING".equals(pairing.getStatus())) {
|
||||||
|
pairing.setStatus("EXPIRED");
|
||||||
|
pairingRepository.save(pairing);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.put("status", pairing.getStatus());
|
||||||
|
if ("PAIRED".equals(pairing.getStatus())) {
|
||||||
|
response.put("token", pairing.getAgentToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User (via Web Dashboard) verifies the 6 digit code.
|
||||||
|
*/
|
||||||
|
@PostMapping("/pairing/verify")
|
||||||
|
public ResponseEntity<Map<String, String>> verifyCode(@RequestBody Map<String, String> body) {
|
||||||
|
String code = body.get("code");
|
||||||
|
if (code == null || code.trim().isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Code is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
String email = auth.getName();
|
||||||
|
Optional<AppUser> userOpt = userRepository.findByEmail(email);
|
||||||
|
|
||||||
|
if (userOpt.isEmpty()) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Long orgId = userOpt.get().getOrgId();
|
||||||
|
|
||||||
|
Optional<DevicePairing> pairingOpt = pairingRepository.findByPairingCode(code);
|
||||||
|
if (pairingOpt.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid pairing code"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DevicePairing pairing = pairingOpt.get();
|
||||||
|
|
||||||
|
if (!"PENDING".equals(pairing.getStatus())) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Code is already paired or expired"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LocalDateTime.now().isAfter(pairing.getExpiresAt())) {
|
||||||
|
pairing.setStatus("EXPIRED");
|
||||||
|
pairingRepository.save(pairing);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Code has expired"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new Agent Token for the organization
|
||||||
|
AgentToken newToken = new AgentToken();
|
||||||
|
newToken.setOrgId(orgId);
|
||||||
|
newToken.setToken("agt_" + UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
newToken.setLabel("Agent paired via device code");
|
||||||
|
tokenRepository.save(newToken);
|
||||||
|
|
||||||
|
// Mark pairing as complete
|
||||||
|
pairing.setStatus("PAIRED");
|
||||||
|
pairing.setOrgId(orgId);
|
||||||
|
pairing.setAgentToken(newToken.getToken());
|
||||||
|
pairingRepository.save(pairing);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Agent successfully paired"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.autopilot.localagent_cloud.auth;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
private final JwtAuthFilter jwtAuthFilter;
|
||||||
|
public SecurityConfig(JwtAuthFilter jwtAuthFilter) { this.jwtAuthFilter = jwtAuthFilter; }
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.exceptionHandling(ex -> ex.authenticationEntryPoint((req, res, e) -> res.sendError(jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")))
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
// Public endpoints
|
||||||
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
|
// Agent polling uses token-based auth (not JWT), so permit it
|
||||||
|
.requestMatchers("/api/agents/*/jobs/next").permitAll()
|
||||||
|
.requestMatchers("/api/agents/register").permitAll()
|
||||||
|
.requestMatchers("/api/agents/*/heartbeat").permitAll()
|
||||||
|
.requestMatchers("/api/agents/pairing/**").permitAll()
|
||||||
|
.requestMatchers("/api/executions/*/results").permitAll()
|
||||||
|
// Legacy agent endpoints
|
||||||
|
.requestMatchers("/agents/**").permitAll()
|
||||||
|
.requestMatchers("/executions/**").permitAll()
|
||||||
|
// Static frontend and assets
|
||||||
|
.requestMatchers("/autopilot/**", "/", "/index.html").permitAll()
|
||||||
|
.requestMatchers("/assets/**", "/*.js", "/*.css", "/*.json", "/*.png", "/*.svg", "/*.ico").permitAll()
|
||||||
|
.requestMatchers("/*.ps1", "/*.sh", "/agent/**").permitAll()
|
||||||
|
// Everything else requires JWT
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||||
|
return config.getAuthenticationManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Agent;
|
||||||
|
import com.autopilot.localagent_cloud.model.AgentGroupMapping;
|
||||||
|
import com.autopilot.localagent_cloud.service.AgentService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/agents")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class AgentController {
|
||||||
|
|
||||||
|
private final AgentService agentService;
|
||||||
|
|
||||||
|
public AgentController(AgentService agentService) {
|
||||||
|
this.agentService = agentService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Agent>> getAgents(HttpServletRequest req) {
|
||||||
|
return agentService.getAll(orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public ResponseEntity<Map<String, Object>> registerAgent(@RequestBody Map<String, Object> body) {
|
||||||
|
return agentService.register(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/heartbeat")
|
||||||
|
public ResponseEntity<Void> agentHeartbeat(@PathVariable("id") String id) {
|
||||||
|
return agentService.heartbeat(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/jobs/next")
|
||||||
|
public ResponseEntity<Map<String, Object>> getNextJob(@PathVariable("id") String agentId) {
|
||||||
|
return agentService.getNextJob(agentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.ApiKey;
|
||||||
|
import com.autopilot.localagent_cloud.model.AuditLog;
|
||||||
|
import com.autopilot.localagent_cloud.repository.ApiKeyRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AuditLogRepository;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/apikeys")
|
||||||
|
public class ApiKeyController {
|
||||||
|
|
||||||
|
private final ApiKeyRepository apiKeyRepository;
|
||||||
|
private final AuditLogRepository auditLogRepository;
|
||||||
|
|
||||||
|
public ApiKeyController(ApiKeyRepository apiKeyRepository, AuditLogRepository auditLogRepository) {
|
||||||
|
this.apiKeyRepository = apiKeyRepository;
|
||||||
|
this.auditLogRepository = auditLogRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserEmail(HttpServletRequest req) {
|
||||||
|
Object e = req.getAttribute("email");
|
||||||
|
return e != null ? e.toString() : "API_KEY";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<ApiKey>> getKeys(HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
return ResponseEntity.ok(apiKeyRepository.findByOrgId(orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<?> createKey(@RequestBody Map<String, String> body, HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
String name = body.get("name");
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Key name cannot be blank."));
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiKey key = new ApiKey();
|
||||||
|
key.setOrgId(orgId);
|
||||||
|
key.setName(name);
|
||||||
|
key.setToken("ap_live_" + UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
|
||||||
|
ApiKey saved = apiKeyRepository.save(key);
|
||||||
|
auditLogRepository.save(new AuditLog(orgId, getUserEmail(req), "CREATE", "API_KEY", saved.getId().toString(), "Generated API Key: " + saved.getName()));
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteKey(@PathVariable Long id, HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
return apiKeyRepository.findById(id).map(key -> {
|
||||||
|
if (!key.getOrgId().equals(orgId)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).<Void>build();
|
||||||
|
}
|
||||||
|
apiKeyRepository.delete(key);
|
||||||
|
auditLogRepository.save(new AuditLog(orgId, getUserEmail(req), "DELETE", "API_KEY", key.getId().toString(), "Revoked API Key: " + key.getName()));
|
||||||
|
return ResponseEntity.noContent().<Void>build();
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called internally (e.g., by future API middleware) to record when an API key was last used.
|
||||||
|
* Fix: api_keys.last_used_at was never being updated anywhere.
|
||||||
|
*/
|
||||||
|
public void recordKeyUsage(String token) {
|
||||||
|
apiKeyRepository.findByToken(token).ifPresent(key -> {
|
||||||
|
key.setLastUsedAt(LocalDateTime.now());
|
||||||
|
apiKeyRepository.save(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.AuditLog;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AuditLogRepository;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/audit-logs")
|
||||||
|
public class AuditLogController {
|
||||||
|
|
||||||
|
private final AuditLogRepository auditLogRepository;
|
||||||
|
|
||||||
|
public AuditLogController(AuditLogRepository auditLogRepository) {
|
||||||
|
this.auditLogRepository = auditLogRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<AuditLog>> getAuditLogs(HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
List<AuditLog> logs = auditLogRepository.findByOrgIdOrderByCreatedAtDesc(orgId);
|
||||||
|
return ResponseEntity.ok(logs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Dataset;
|
||||||
|
import com.autopilot.localagent_cloud.repository.DatasetRepository;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/datasets")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class DatasetController {
|
||||||
|
|
||||||
|
private final DatasetRepository datasetRepository;
|
||||||
|
|
||||||
|
public DatasetController(DatasetRepository datasetRepository) {
|
||||||
|
this.datasetRepository = datasetRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getOrgId(HttpServletRequest request) {
|
||||||
|
Object o = request.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/datasets — list all datasets for org
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Dataset>> getAll(HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
return ResponseEntity.ok(datasetRepository.findByOrgId(orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/datasets/{id} — get single dataset by id
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Dataset> getById(@PathVariable Long id, HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
return datasetRepository.findById(id)
|
||||||
|
.filter(d -> orgId.equals(d.getOrgId()))
|
||||||
|
.map(ResponseEntity::ok)
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/datasets — create dataset from JSON body
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Dataset> create(@RequestBody Dataset dataset, HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
dataset.setOrgId(orgId);
|
||||||
|
return ResponseEntity.ok(datasetRepository.save(dataset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/datasets/upload — create dataset from CSV file upload
|
||||||
|
@PostMapping("/upload")
|
||||||
|
public ResponseEntity<?> uploadCsv(@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam("name") String name,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
|
||||||
|
List<String[]> allRows = new ArrayList<>();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if (!line.trim().isEmpty()) {
|
||||||
|
allRows.add(parseCsvLine(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allRows.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(Map.of("error", "CSV file is empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] headerRow = allRows.get(0);
|
||||||
|
List<String[]> dataRows = allRows.subList(1, allRows.size());
|
||||||
|
|
||||||
|
String headersJson = toJsonArray(headerRow);
|
||||||
|
String rowsJson = toJsonArrayOfArrays(dataRows);
|
||||||
|
|
||||||
|
Dataset dataset = new Dataset();
|
||||||
|
dataset.setOrgId(orgId);
|
||||||
|
dataset.setName(name);
|
||||||
|
dataset.setHeaders(headersJson);
|
||||||
|
dataset.setRows(rowsJson);
|
||||||
|
dataset.setRowCount(dataRows.size());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(datasetRepository.save(dataset));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Map.of("error", "Failed to parse CSV: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/datasets/{id} — update dataset
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Dataset> update(@PathVariable Long id,
|
||||||
|
@RequestBody Dataset body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
return datasetRepository.findById(id)
|
||||||
|
.filter(d -> orgId.equals(d.getOrgId()))
|
||||||
|
.map(d -> {
|
||||||
|
if (body.getName() != null) d.setName(body.getName());
|
||||||
|
if (body.getDescription() != null) d.setDescription(body.getDescription());
|
||||||
|
if (body.getHeaders() != null) d.setHeaders(body.getHeaders());
|
||||||
|
if (body.getRows() != null) d.setRows(body.getRows());
|
||||||
|
if (body.getRowCount() != null) d.setRowCount(body.getRowCount());
|
||||||
|
return ResponseEntity.ok(datasetRepository.save(d));
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/datasets/{id} — delete dataset
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id, HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
return datasetRepository.findById(id)
|
||||||
|
.filter(d -> orgId.equals(d.getOrgId()))
|
||||||
|
.map(d -> { datasetRepository.delete(d); return ResponseEntity.ok().<Void>build(); })
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CSV Parsing Helpers ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a single CSV line, handling quoted fields that may contain commas.
|
||||||
|
*/
|
||||||
|
private String[] parseCsvLine(String line) {
|
||||||
|
List<String> fields = new ArrayList<>();
|
||||||
|
StringBuilder current = new StringBuilder();
|
||||||
|
boolean inQuotes = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < line.length(); i++) {
|
||||||
|
char c = line.charAt(i);
|
||||||
|
if (inQuotes) {
|
||||||
|
if (c == '"') {
|
||||||
|
// Check for escaped quote ""
|
||||||
|
if (i + 1 < line.length() && line.charAt(i + 1) == '"') {
|
||||||
|
current.append('"');
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
inQuotes = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current.append(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (c == '"') {
|
||||||
|
inQuotes = true;
|
||||||
|
} else if (c == ',') {
|
||||||
|
fields.add(current.toString().trim());
|
||||||
|
current = new StringBuilder();
|
||||||
|
} else {
|
||||||
|
current.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields.add(current.toString().trim());
|
||||||
|
return fields.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a String array to a JSON array string, e.g. ["a","b","c"]
|
||||||
|
*/
|
||||||
|
private String toJsonArray(String[] items) {
|
||||||
|
StringBuilder sb = new StringBuilder("[");
|
||||||
|
for (int i = 0; i < items.length; i++) {
|
||||||
|
if (i > 0) sb.append(",");
|
||||||
|
sb.append("\"").append(escapeJson(items[i])).append("\"");
|
||||||
|
}
|
||||||
|
sb.append("]");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a list of String arrays to a JSON array of arrays string.
|
||||||
|
*/
|
||||||
|
private String toJsonArrayOfArrays(List<String[]> rows) {
|
||||||
|
StringBuilder sb = new StringBuilder("[");
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
if (i > 0) sb.append(",");
|
||||||
|
sb.append(toJsonArray(rows.get(i)));
|
||||||
|
}
|
||||||
|
sb.append("]");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes special characters for JSON string values.
|
||||||
|
*/
|
||||||
|
private String escapeJson(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
return value
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\t", "\\t");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.AuditLog;
|
||||||
|
import com.autopilot.localagent_cloud.model.Environment;
|
||||||
|
import com.autopilot.localagent_cloud.model.Variable;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AuditLogRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.EnvironmentRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.VariableRepository;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Secret masking sentinel — a clean ASCII constant, never a corrupted UTF-8 literal */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/environments")
|
||||||
|
public class EnvironmentController {
|
||||||
|
|
||||||
|
// Industry-standard sentinel string for masked secrets (never stored in DB)
|
||||||
|
private static final String SECRET_MASK = "••••••••";
|
||||||
|
|
||||||
|
private final EnvironmentRepository environmentRepository;
|
||||||
|
private final VariableRepository variableRepository;
|
||||||
|
private final AuditLogRepository auditLogRepository;
|
||||||
|
|
||||||
|
public EnvironmentController(EnvironmentRepository environmentRepository,
|
||||||
|
VariableRepository variableRepository,
|
||||||
|
AuditLogRepository auditLogRepository) {
|
||||||
|
this.environmentRepository = environmentRepository;
|
||||||
|
this.variableRepository = variableRepository;
|
||||||
|
this.auditLogRepository = auditLogRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserEmail(HttpServletRequest req) {
|
||||||
|
Object e = req.getAttribute("email");
|
||||||
|
return e != null ? e.toString() : "SYSTEM";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getOrgId(HttpServletRequest request) {
|
||||||
|
// Use the orgId already extracted and validated by JwtAuthFilter — consistent with all other controllers
|
||||||
|
Object o = request.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Environment>> getAll(HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
return ResponseEntity.ok(environmentRepository.findByOrgId(orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Environment> create(@RequestBody Environment env, HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
env.setOrgId(orgId);
|
||||||
|
Environment saved = environmentRepository.save(env);
|
||||||
|
auditLogRepository.save(new AuditLog(orgId, getUserEmail(request), "CREATE", "ENVIRONMENT", saved.getId().toString(), "Created Environment: " + saved.getName()));
|
||||||
|
return ResponseEntity.ok(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Environment> update(@PathVariable Long id,
|
||||||
|
@RequestBody Environment body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
return environmentRepository.findById(id)
|
||||||
|
.filter(e -> orgId.equals(e.getOrgId())) // Tenant isolation check
|
||||||
|
.map(e -> {
|
||||||
|
e.setName(body.getName());
|
||||||
|
e.setDescription(body.getDescription());
|
||||||
|
Environment saved = environmentRepository.save(e);
|
||||||
|
auditLogRepository.save(new AuditLog(orgId, getUserEmail(request), "UPDATE", "ENVIRONMENT", saved.getId().toString(), "Updated Environment: " + saved.getName()));
|
||||||
|
return ResponseEntity.ok(saved);
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id, HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
// Bug fix: verify this environment belongs to the calling org before deleting
|
||||||
|
Environment env = environmentRepository.findById(id).orElse(null);
|
||||||
|
if (env == null) return ResponseEntity.notFound().build();
|
||||||
|
if (!orgId.equals(env.getOrgId())) return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
|
||||||
|
// Bug fix: pass correct orgId (was passing null — could delete other org's variables!)
|
||||||
|
List<Variable> envVars = variableRepository.findByOrgIdAndScopeAndScopeId(orgId, "ENVIRONMENT", id);
|
||||||
|
variableRepository.deleteAll(envVars);
|
||||||
|
environmentRepository.deleteById(id);
|
||||||
|
auditLogRepository.save(new AuditLog(orgId, getUserEmail(request), "DELETE", "ENVIRONMENT", id.toString(), "Deleted Environment: " + env.getName()));
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/variables")
|
||||||
|
public ResponseEntity<List<Variable>> getVariables(@PathVariable Long id, HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
List<Variable> vars = variableRepository.findByOrgIdAndScopeAndScopeId(orgId, "ENVIRONMENT", id);
|
||||||
|
// Mask secret values before returning to client
|
||||||
|
vars.forEach(v -> { if (Boolean.TRUE.equals(v.getIsSecret())) v.setValue(SECRET_MASK); });
|
||||||
|
return ResponseEntity.ok(vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/variables")
|
||||||
|
public ResponseEntity<Variable> addVariable(@PathVariable Long id,
|
||||||
|
@RequestBody Variable variable,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
variable.setOrgId(orgId);
|
||||||
|
variable.setScope("ENVIRONMENT");
|
||||||
|
variable.setScopeId(id);
|
||||||
|
return ResponseEntity.ok(variableRepository.save(variable));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{envId}/variables/{varId}")
|
||||||
|
public ResponseEntity<Void> deleteVariable(@PathVariable Long envId,
|
||||||
|
@PathVariable Long varId,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
// Verify the variable belongs to this org before deleting
|
||||||
|
return variableRepository.findById(varId)
|
||||||
|
.filter(v -> orgId.equals(v.getOrgId()))
|
||||||
|
.map(v -> { variableRepository.delete(v); return ResponseEntity.ok().<Void>build(); })
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Execution;
|
||||||
|
import com.autopilot.localagent_cloud.service.AgentService;
|
||||||
|
import com.autopilot.localagent_cloud.service.ExecutionService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/executions")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class ExecutionController {
|
||||||
|
|
||||||
|
private final ExecutionService executionService;
|
||||||
|
private final AgentService agentService;
|
||||||
|
|
||||||
|
public ExecutionController(ExecutionService executionService, AgentService agentService) {
|
||||||
|
this.executionService = executionService;
|
||||||
|
this.agentService = agentService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Execution>> getExecutions(HttpServletRequest req) {
|
||||||
|
return executionService.getAll(orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getExecutionDetail(@PathVariable("id") Long id) {
|
||||||
|
return executionService.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/results")
|
||||||
|
public ResponseEntity<Void> postExecutionResults(
|
||||||
|
@PathVariable("id") Long executionId,
|
||||||
|
@RequestBody Map<String, Object> result) {
|
||||||
|
return agentService.postResults(executionId, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/stop")
|
||||||
|
public ResponseEntity<Void> stopExecution(@PathVariable("id") Long executionId) {
|
||||||
|
return agentService.stopExecution(executionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/rerun")
|
||||||
|
public ResponseEntity<Void> rerunExecution(@PathVariable("id") Long executionId) {
|
||||||
|
return agentService.rerunExecution(executionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteExecution(@PathVariable("id") Long executionId) {
|
||||||
|
return executionService.deleteById(executionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Group;
|
||||||
|
import com.autopilot.localagent_cloud.model.AgentGroupMapping;
|
||||||
|
import com.autopilot.localagent_cloud.service.GroupService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/groups")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class GroupController {
|
||||||
|
|
||||||
|
private final GroupService groupService;
|
||||||
|
|
||||||
|
public GroupController(GroupService groupService) {
|
||||||
|
this.groupService = groupService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Group>> getGroups(HttpServletRequest req) {
|
||||||
|
return groupService.getAll(orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Group> createGroup(@RequestBody Group group, HttpServletRequest req) {
|
||||||
|
return groupService.create(group, orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Group> updateGroup(@PathVariable("id") Long id, @RequestBody Group updated) {
|
||||||
|
return groupService.update(id, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteGroup(@PathVariable("id") Long id) {
|
||||||
|
return groupService.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/agents")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getAgentsInGroup(@PathVariable("id") Long groupId) {
|
||||||
|
return groupService.getAgentsInGroup(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/agents")
|
||||||
|
public ResponseEntity<AgentGroupMapping> addAgentToGroup(
|
||||||
|
@PathVariable("id") Long groupId,
|
||||||
|
@RequestBody Map<String, String> body) {
|
||||||
|
return groupService.addAgent(groupId, body.get("agentId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{groupId}/agents/{agentId}")
|
||||||
|
public ResponseEntity<Void> removeAgentFromGroup(
|
||||||
|
@PathVariable("groupId") Long groupId,
|
||||||
|
@PathVariable("agentId") String agentId) {
|
||||||
|
return groupService.removeAgent(groupId, agentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestSuite;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestSuiteRepository;
|
||||||
|
import com.autopilot.localagent_cloud.service.ExecutionService;
|
||||||
|
import com.autopilot.localagent_cloud.service.TestSuiteService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gap 4: CI/CD Pipeline Integration Controller
|
||||||
|
*
|
||||||
|
* Provides API-key-authenticated endpoints that DevOps tools (Jenkins, GitHub Actions,
|
||||||
|
* GitLab CI) can call to trigger test suite executions and poll for results.
|
||||||
|
*
|
||||||
|
* Authentication: Pass your AutoPilot API Key in the Authorization header:
|
||||||
|
* Authorization: Bearer ap_live_<your_key>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1")
|
||||||
|
public class PipelineController {
|
||||||
|
|
||||||
|
private final TestSuiteService testSuiteService;
|
||||||
|
private final ExecutionService executionService;
|
||||||
|
private final TestSuiteRepository testSuiteRepository;
|
||||||
|
|
||||||
|
public PipelineController(TestSuiteService testSuiteService,
|
||||||
|
ExecutionService executionService,
|
||||||
|
TestSuiteRepository testSuiteRepository) {
|
||||||
|
this.testSuiteService = testSuiteService;
|
||||||
|
this.executionService = executionService;
|
||||||
|
this.testSuiteRepository = testSuiteRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a test suite by its ID.
|
||||||
|
* Supports optional body overrides: browser, environmentId, targetGroupId.
|
||||||
|
*
|
||||||
|
* POST /api/v1/suites/{id}/trigger
|
||||||
|
* Body (optional):
|
||||||
|
* {
|
||||||
|
* "browser": "firefox",
|
||||||
|
* "environmentId": 2,
|
||||||
|
* "targetGroupId": 1
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@PostMapping("/suites/{id}/trigger")
|
||||||
|
public ResponseEntity<Map<String, Object>> triggerSuiteById(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestBody(required = false) Map<String, Object> overrides,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
Map<String, Object> runBody = overrides != null ? new HashMap<>(overrides) : new HashMap<>();
|
||||||
|
return executeAndBuildResponse(id, runBody, orgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a test suite by its exact name. Useful for CI scripts where you know
|
||||||
|
* the suite name but not the numeric ID.
|
||||||
|
*
|
||||||
|
* POST /api/v1/suites/trigger-by-name
|
||||||
|
* Body:
|
||||||
|
* {
|
||||||
|
* "suiteName": "Smoke Tests",
|
||||||
|
* "browser": "chrome",
|
||||||
|
* "environmentId": 1
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@PostMapping("/suites/trigger-by-name")
|
||||||
|
public ResponseEntity<Map<String, Object>> triggerSuiteByName(
|
||||||
|
@RequestBody Map<String, Object> body,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
String suiteName = (String) body.get("suiteName");
|
||||||
|
if (suiteName == null || suiteName.isBlank()) {
|
||||||
|
Map<String, Object> err = new HashMap<>();
|
||||||
|
err.put("error", "suiteName is required");
|
||||||
|
return ResponseEntity.badRequest().body(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<TestSuite> suite = testSuiteRepository.findAll().stream()
|
||||||
|
.filter(s -> orgId.equals(s.getOrgId()) && suiteName.equalsIgnoreCase(s.getName()))
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (suite.isEmpty()) {
|
||||||
|
Map<String, Object> err = new HashMap<>();
|
||||||
|
err.put("error", "Test Suite not found: " + suiteName);
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> runBody = new HashMap<>(body);
|
||||||
|
runBody.remove("suiteName");
|
||||||
|
return executeAndBuildResponse(suite.get().getId(), runBody, orgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll the status of an execution by its ID.
|
||||||
|
* CI tools can call this in a loop to wait for completion.
|
||||||
|
*
|
||||||
|
* GET /api/v1/executions/{id}/status
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@GetMapping("/executions/{id}/status")
|
||||||
|
public ResponseEntity<Map<String, Object>> getExecutionStatus(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
ResponseEntity<Map<String, Object>> res = executionService.getById(id);
|
||||||
|
if (res.getStatusCode() != HttpStatus.OK || res.getBody() == null) {
|
||||||
|
Map<String, Object> queuedResponse = new HashMap<>();
|
||||||
|
queuedResponse.put("status", "queued");
|
||||||
|
queuedResponse.put("message", "Waiting for agent to pick up the job...");
|
||||||
|
return ResponseEntity.ok(queuedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(buildExecutionStatusResponse(res.getBody()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll the latest execution status for a scheduler/trigger job.
|
||||||
|
*
|
||||||
|
* GET /api/v1/schedulers/{id}/execution-status
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@GetMapping("/schedulers/{id}/execution-status")
|
||||||
|
public ResponseEntity<Map<String, Object>> getSchedulerExecutionStatus(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
Long orgId = orgId(req);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
ResponseEntity<Map<String, Object>> res = executionService.getBySchedulerId(id);
|
||||||
|
if (res.getStatusCode() != HttpStatus.OK || res.getBody() == null) {
|
||||||
|
Map<String, Object> queuedResponse = new HashMap<>();
|
||||||
|
queuedResponse.put("status", "queued");
|
||||||
|
queuedResponse.put("message", "Waiting for Local Agent to pick up the job...");
|
||||||
|
return ResponseEntity.ok(queuedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(buildExecutionStatusResponse(res.getBody()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, Object>> executeAndBuildResponse(Long suiteId, Map<String, Object> runBody, Long orgId) {
|
||||||
|
try {
|
||||||
|
ResponseEntity<Map<String, Object>> res = testSuiteService.run(suiteId, runBody, orgId);
|
||||||
|
|
||||||
|
if (res.getStatusCode() == HttpStatus.OK || res.getStatusCode() == HttpStatus.CREATED) {
|
||||||
|
Map<String, Object> resBody = res.getBody();
|
||||||
|
if (resBody != null && resBody.containsKey("scheduler")) {
|
||||||
|
com.autopilot.localagent_cloud.model.Scheduler sched =
|
||||||
|
(com.autopilot.localagent_cloud.model.Scheduler) resBody.get("scheduler");
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("schedulerId", sched.getId());
|
||||||
|
response.put("suiteId", suiteId);
|
||||||
|
response.put("status", "queued");
|
||||||
|
response.put("message", "Test Suite queued successfully.");
|
||||||
|
response.put("statusUrl", "/api/v1/schedulers/" + sched.getId() + "/execution-status");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResponseEntity.status(res.getStatusCode()).body(res.getBody());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Map<String, Object> err = new HashMap<>();
|
||||||
|
err.put("error", "Failed to trigger Test Suite");
|
||||||
|
err.put("details", e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> buildExecutionStatusResponse(Map<String, Object> executionData) {
|
||||||
|
com.autopilot.localagent_cloud.model.Execution execution =
|
||||||
|
(com.autopilot.localagent_cloud.model.Execution) executionData.get("execution");
|
||||||
|
java.util.List<com.autopilot.localagent_cloud.model.StepResult> steps =
|
||||||
|
(java.util.List<com.autopilot.localagent_cloud.model.StepResult>) executionData.get("steps");
|
||||||
|
|
||||||
|
int passed = 0, failed = 0;
|
||||||
|
if (steps != null) {
|
||||||
|
for (com.autopilot.localagent_cloud.model.StepResult step : steps) {
|
||||||
|
if (step.getResultStatus() != null && step.getResultStatus() == 1) passed++;
|
||||||
|
else if (step.getResultStatus() != null && step.getResultStatus() == 2) failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int total = passed + failed;
|
||||||
|
double passPercentage = total > 0 ? ((double) passed / total) * 100.0 : 0.0;
|
||||||
|
|
||||||
|
long durationMs = 0;
|
||||||
|
if (execution.getCreatedAt() != null && execution.getFinishedAt() != null) {
|
||||||
|
durationMs = java.time.Duration.between(execution.getCreatedAt(), execution.getFinishedAt()).toMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// CI/CD friendly exit code: 0 = success, 1 = failed, 2 = still running
|
||||||
|
int exitCode = "completed".equals(execution.getStatus()) ? (failed > 0 ? 1 : 0) : 2;
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("executionId", execution.getId());
|
||||||
|
response.put("status", execution.getStatus());
|
||||||
|
response.put("exitCode", exitCode);
|
||||||
|
response.put("passedCount", passed);
|
||||||
|
response.put("failedCount", failed);
|
||||||
|
response.put("totalCount", total);
|
||||||
|
response.put("passPercentage", Math.round(passPercentage * 100.0) / 100.0);
|
||||||
|
response.put("durationMs", durationMs);
|
||||||
|
response.put("aiAnalysis", execution.getAiAnalysis());
|
||||||
|
response.put("startedAt", execution.getCreatedAt());
|
||||||
|
response.put("completedAt", execution.getFinishedAt());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Organisation;
|
||||||
|
import com.autopilot.localagent_cloud.repository.OrganisationRepository;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/public/orgs")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class PublicOrgController {
|
||||||
|
|
||||||
|
private final OrganisationRepository orgRepository;
|
||||||
|
|
||||||
|
public PublicOrgController(OrganisationRepository orgRepository) {
|
||||||
|
this.orgRepository = orgRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/by-subdomain/{subdomain}")
|
||||||
|
public ResponseEntity<Map<String, String>> getOrgBySubdomain(@PathVariable String subdomain) {
|
||||||
|
Organisation org = orgRepository.findBySubdomain(subdomain);
|
||||||
|
if (org == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"name", org.getName(),
|
||||||
|
"subdomain", org.getSubdomain()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Scheduler;
|
||||||
|
import com.autopilot.localagent_cloud.service.SchedulerService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/schedulers")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class SchedulerController {
|
||||||
|
|
||||||
|
private final SchedulerService schedulerService;
|
||||||
|
|
||||||
|
public SchedulerController(SchedulerService schedulerService) {
|
||||||
|
this.schedulerService = schedulerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Scheduler>> getSchedulers(HttpServletRequest req) {
|
||||||
|
return schedulerService.getAll(orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Scheduler> createScheduler(@RequestBody Map<String, Object> body, HttpServletRequest req) {
|
||||||
|
return schedulerService.create(body, orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Scheduler> updateScheduler(@PathVariable("id") Long id, @RequestBody Map<String, Object> body) {
|
||||||
|
return schedulerService.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteScheduler(@PathVariable("id") Long id) {
|
||||||
|
return schedulerService.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.UrlResource;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/screenshots")
|
||||||
|
public class ScreenshotController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ScreenshotController.class);
|
||||||
|
private static final Path SCREENSHOTS_DIR = Paths.get("data/screenshots").toAbsolutePath().normalize();
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{fileName:.+}")
|
||||||
|
public ResponseEntity<Resource> getScreenshotFile(
|
||||||
|
@PathVariable("fileName") String fileName,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
|
||||||
|
// Security: require authenticated user
|
||||||
|
if (orgId(req) == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Security: prevent path traversal by resolving inside the base dir and verifying it stays within
|
||||||
|
Path resolvedPath = SCREENSHOTS_DIR.resolve(fileName).normalize();
|
||||||
|
if (!resolvedPath.startsWith(SCREENSHOTS_DIR)) {
|
||||||
|
logger.warn("Path traversal attempt blocked: {}", fileName);
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = resolvedPath.toFile();
|
||||||
|
if (!file.exists() || !file.isFile()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource resource = new UrlResource(file.toURI());
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + file.getName() + "\"")
|
||||||
|
.contentType(MediaType.IMAGE_PNG)
|
||||||
|
.body(resource);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
logger.error("Failed to serve screenshot file: {}", fileName, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestCase;
|
||||||
|
import com.autopilot.localagent_cloud.service.TestCaseService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/test-cases")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class TestCaseController {
|
||||||
|
|
||||||
|
private final TestCaseService testCaseService;
|
||||||
|
|
||||||
|
public TestCaseController(TestCaseService testCaseService) {
|
||||||
|
this.testCaseService = testCaseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<TestCase>> getTestCases(HttpServletRequest req) {
|
||||||
|
return testCaseService.getAll(orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getTestCaseDetail(@PathVariable("id") Long id) {
|
||||||
|
return testCaseService.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Map<String, Object>> createTestCase(
|
||||||
|
@RequestBody Map<String, Object> body, HttpServletRequest req) {
|
||||||
|
return testCaseService.create(body, orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateTestCase(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestBody Map<String, Object> body) {
|
||||||
|
return testCaseService.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteTestCase(@PathVariable("id") Long id) {
|
||||||
|
return testCaseService.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestCaseGroup;
|
||||||
|
import com.autopilot.localagent_cloud.service.TestCaseGroupService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/test-case-groups")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class TestCaseGroupController {
|
||||||
|
|
||||||
|
private final TestCaseGroupService testCaseGroupService;
|
||||||
|
|
||||||
|
public TestCaseGroupController(TestCaseGroupService testCaseGroupService) {
|
||||||
|
this.testCaseGroupService = testCaseGroupService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<TestCaseGroup>> getTestCaseGroups(HttpServletRequest req) {
|
||||||
|
return testCaseGroupService.getAll(orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getTestCaseGroupDetail(@PathVariable("id") Long id) {
|
||||||
|
return testCaseGroupService.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Map<String, Object>> createTestCaseGroup(
|
||||||
|
@RequestBody Map<String, Object> body, HttpServletRequest req) {
|
||||||
|
return testCaseGroupService.create(body, orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateTestCaseGroup(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestBody Map<String, Object> body) {
|
||||||
|
return testCaseGroupService.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteTestCaseGroup(@PathVariable("id") Long id) {
|
||||||
|
return testCaseGroupService.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.AuditLog;
|
||||||
|
import com.autopilot.localagent_cloud.model.TestSuite;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AuditLogRepository;
|
||||||
|
import com.autopilot.localagent_cloud.service.TestSuiteService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/test-suites")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class TestSuiteController {
|
||||||
|
|
||||||
|
private final TestSuiteService testSuiteService;
|
||||||
|
private final AuditLogRepository auditLogRepository;
|
||||||
|
|
||||||
|
public TestSuiteController(TestSuiteService testSuiteService, AuditLogRepository auditLogRepository) {
|
||||||
|
this.testSuiteService = testSuiteService;
|
||||||
|
this.auditLogRepository = auditLogRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserEmail(HttpServletRequest req) {
|
||||||
|
Object e = req.getAttribute("email");
|
||||||
|
return e != null ? e.toString() : "SYSTEM";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long orgId(HttpServletRequest req) {
|
||||||
|
Object o = req.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<TestSuite>> getTestSuites(HttpServletRequest req) {
|
||||||
|
return testSuiteService.getAll(orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getTestSuiteDetail(@PathVariable("id") Long id) {
|
||||||
|
return testSuiteService.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Map<String, Object>> createTestSuite(
|
||||||
|
@RequestBody Map<String, Object> body, HttpServletRequest req) {
|
||||||
|
ResponseEntity<Map<String, Object>> res = testSuiteService.create(body, orgId(req));
|
||||||
|
if (res.getStatusCode().is2xxSuccessful() && res.getBody() != null) {
|
||||||
|
TestSuite suite = (TestSuite) res.getBody().get("suite");
|
||||||
|
auditLogRepository.save(new AuditLog(orgId(req), getUserEmail(req), "CREATE", "TEST_SUITE", suite.getId().toString(), "Created Test Suite: " + suite.getName()));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateTestSuite(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestBody Map<String, Object> body, HttpServletRequest req) {
|
||||||
|
ResponseEntity<Map<String, Object>> res = testSuiteService.update(id, body);
|
||||||
|
if (res.getStatusCode().is2xxSuccessful() && res.getBody() != null) {
|
||||||
|
TestSuite suite = (TestSuite) res.getBody().get("suite");
|
||||||
|
auditLogRepository.save(new AuditLog(orgId(req), getUserEmail(req), "UPDATE", "TEST_SUITE", suite.getId().toString(), "Updated Test Suite: " + suite.getName()));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/run")
|
||||||
|
public ResponseEntity<Map<String, Object>> runTestSuite(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestBody(required = false) Map<String, Object> body,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
return testSuiteService.run(id, body, orgId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteTestSuite(@PathVariable("id") Long id, HttpServletRequest req) {
|
||||||
|
ResponseEntity<Void> res = testSuiteService.delete(id);
|
||||||
|
if (res.getStatusCode().is2xxSuccessful()) {
|
||||||
|
auditLogRepository.save(new AuditLog(orgId(req), getUserEmail(req), "DELETE", "TEST_SUITE", id.toString(), "Deleted Test Suite ID: " + id));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.autopilot.localagent_cloud.controller;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Variable;
|
||||||
|
import com.autopilot.localagent_cloud.repository.VariableRepository;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/variables")
|
||||||
|
public class VariableController {
|
||||||
|
|
||||||
|
// Industry-standard sentinel — used to detect "user did not change the secret value"
|
||||||
|
private static final String SECRET_MASK = "••••••••";
|
||||||
|
|
||||||
|
private final VariableRepository variableRepository;
|
||||||
|
|
||||||
|
public VariableController(VariableRepository variableRepository) {
|
||||||
|
this.variableRepository = variableRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getOrgId(HttpServletRequest request) {
|
||||||
|
// Use the orgId already extracted and validated by JwtAuthFilter
|
||||||
|
Object o = request.getAttribute("orgId");
|
||||||
|
return o != null ? ((Number) o).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Variable>> getAll(
|
||||||
|
@RequestParam(required = false) String scope,
|
||||||
|
@RequestParam(required = false) Long scopeId,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
List<Variable> vars;
|
||||||
|
if (scope != null && scopeId != null) {
|
||||||
|
vars = variableRepository.findByOrgIdAndScopeAndScopeId(orgId, scope.toUpperCase(), scopeId);
|
||||||
|
} else if (scope != null) {
|
||||||
|
vars = variableRepository.findByOrgIdAndScope(orgId, scope.toUpperCase());
|
||||||
|
} else {
|
||||||
|
vars = variableRepository.findByOrgId(orgId);
|
||||||
|
}
|
||||||
|
// Bug fix: mask secret values using the clean constant (was a corrupted UTF-8 literal before)
|
||||||
|
vars.forEach(v -> { if (Boolean.TRUE.equals(v.getIsSecret())) v.setValue(SECRET_MASK); });
|
||||||
|
return ResponseEntity.ok(vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Variable> create(@RequestBody Variable variable, HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
variable.setOrgId(orgId);
|
||||||
|
if (variable.getScope() == null) variable.setScope("GLOBAL");
|
||||||
|
return ResponseEntity.ok(variableRepository.save(variable));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<Variable> update(@PathVariable Long id,
|
||||||
|
@RequestBody Variable body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
|
||||||
|
return variableRepository.findById(id)
|
||||||
|
.filter(v -> orgId.equals(v.getOrgId())) // Tenant isolation check
|
||||||
|
.map(v -> {
|
||||||
|
v.setKeyName(body.getKeyName());
|
||||||
|
// Bug fix: only update value if it's not the sentinel mask string
|
||||||
|
// (was broken before because the corrupted UTF-8 literal never matched)
|
||||||
|
if (body.getValue() != null && !SECRET_MASK.equals(body.getValue())) {
|
||||||
|
v.setValue(body.getValue());
|
||||||
|
}
|
||||||
|
v.setScope(body.getScope() != null ? body.getScope() : v.getScope());
|
||||||
|
v.setScopeId(body.getScopeId());
|
||||||
|
v.setIsSecret(body.getIsSecret());
|
||||||
|
return ResponseEntity.ok(variableRepository.save(v));
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id, HttpServletRequest request) {
|
||||||
|
Long orgId = getOrgId(request);
|
||||||
|
if (orgId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
return variableRepository.findById(id)
|
||||||
|
.filter(v -> orgId.equals(v.getOrgId())) // Tenant isolation check
|
||||||
|
.map(v -> { variableRepository.delete(v); return ResponseEntity.ok().<Void>build(); })
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.autopilot.localagent_cloud.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class RunRequest {
|
||||||
|
public RunResult result;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.autopilot.localagent_cloud.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class RunResult {
|
||||||
|
public String environmentId;
|
||||||
|
public String environmentName;
|
||||||
|
public String projectId;
|
||||||
|
public String applicationId;
|
||||||
|
public String applicationName;
|
||||||
|
public String platformId;
|
||||||
|
public String platformName;
|
||||||
|
public Integer browserTypeId;
|
||||||
|
public String browserTypeName;
|
||||||
|
public String groupId;
|
||||||
|
public String groupName;
|
||||||
|
public String referenceId;
|
||||||
|
public String serviceUrl;
|
||||||
|
public String request_handler;
|
||||||
|
public String iterationval;
|
||||||
|
public List<Map<String, List<TestCaseIteration>>> testCase;
|
||||||
|
|
||||||
|
// Mutated status
|
||||||
|
public Integer result_status;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.autopilot.localagent_cloud.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class TestCaseIteration {
|
||||||
|
public List<TestStep> testSteps;
|
||||||
|
|
||||||
|
// Mutated status
|
||||||
|
public Integer executed_status;
|
||||||
|
public Integer result_status;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.autopilot.localagent_cloud.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class TestStep {
|
||||||
|
public String step_result_id;
|
||||||
|
public String actionName;
|
||||||
|
public String locatorName;
|
||||||
|
public String objectDetail;
|
||||||
|
public String data;
|
||||||
|
public String screenShot;
|
||||||
|
public String screenshot_path;
|
||||||
|
public String stepDesc;
|
||||||
|
public String errorLog;
|
||||||
|
|
||||||
|
// Mutated status
|
||||||
|
public Integer executed_status;
|
||||||
|
public Integer result_status;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.autopilot.localagent_cloud.governance;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage, Logging & Governance Layer
|
||||||
|
* Handles audit trails, decision logging, and compliance reporting.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/governance")
|
||||||
|
public class AuditController {
|
||||||
|
|
||||||
|
@GetMapping("/audit-logs")
|
||||||
|
public ResponseEntity<List<String>> getAuditLogs(@RequestParam(required = false) String buildId) {
|
||||||
|
// TODO: Fetch immutable execution logs and manual override decisions
|
||||||
|
return ResponseEntity.ok(Collections.singletonList("Initial audit log: Build pipeline created"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.autopilot.localagent_cloud.ingestion;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Data Ingestion Layer
|
||||||
|
* Handles incoming webhooks and test scripts from CI/CD systems.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/ingestion")
|
||||||
|
public class TestIngestionController {
|
||||||
|
|
||||||
|
@PostMapping("/webhook")
|
||||||
|
public ResponseEntity<String> receiveWebhook(@RequestBody String payload) {
|
||||||
|
// TODO: Parse webhook payload from GitHub/Jenkins and enqueue for execution
|
||||||
|
return ResponseEntity.ok("Webhook received and queued for processing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "agents")
|
||||||
|
public class Agent {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String os;
|
||||||
|
|
||||||
|
@Column(name = "agent_version")
|
||||||
|
private String agentVersion;
|
||||||
|
|
||||||
|
@Column(name = "last_seen_at", nullable = false)
|
||||||
|
private LocalDateTime lastSeenAt;
|
||||||
|
|
||||||
|
@Column(name = "capabilities_json", columnDefinition = "text")
|
||||||
|
private String capabilitiesJson;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
lastSeenAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
lastSeenAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOs() {
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOs(String os) {
|
||||||
|
this.os = os;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAgentVersion() {
|
||||||
|
return agentVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAgentVersion(String agentVersion) {
|
||||||
|
this.agentVersion = agentVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getLastSeenAt() {
|
||||||
|
return lastSeenAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastSeenAt(LocalDateTime lastSeenAt) {
|
||||||
|
this.lastSeenAt = lastSeenAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCapabilitiesJson() {
|
||||||
|
return capabilitiesJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCapabilitiesJson(String capabilitiesJson) {
|
||||||
|
this.capabilitiesJson = capabilitiesJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "agent_group_mappings")
|
||||||
|
public class AgentGroupMapping {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "agent_id", nullable = false)
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
@Column(name = "group_id", nullable = false)
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getAgentId() { return agentId; }
|
||||||
|
public void setAgentId(String agentId) { this.agentId = agentId; }
|
||||||
|
|
||||||
|
public Long getGroupId() { return groupId; }
|
||||||
|
public void setGroupId(Long groupId) { this.groupId = groupId; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "agent_tokens")
|
||||||
|
public class AgentToken {
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||||
|
@Column(name = "org_id", nullable = false) private Long orgId;
|
||||||
|
@Column(nullable = false, unique = true) private String token;
|
||||||
|
@Column private String label;
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt;
|
||||||
|
@PrePersist protected void onCreate() { this.createdAt = LocalDateTime.now(); }
|
||||||
|
public Long getId() { return id; } public void setId(Long id) { this.id = id; }
|
||||||
|
public Long getOrgId() { return orgId; } public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
public String getToken() { return token; } public void setToken(String token) { this.token = token; }
|
||||||
|
public String getLabel() { return label; } public void setLabel(String label) { this.label = label; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "api_keys")
|
||||||
|
public class ApiKey {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id", nullable = false)
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "last_used_at")
|
||||||
|
private LocalDateTime lastUsedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getToken() { return token; }
|
||||||
|
public void setToken(String token) { this.token = token; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getLastUsedAt() { return lastUsedAt; }
|
||||||
|
public void setLastUsedAt(LocalDateTime lastUsedAt) { this.lastUsedAt = lastUsedAt; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "app_users")
|
||||||
|
public class AppUser {
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||||
|
@Column(name = "org_id", nullable = false) private Long orgId;
|
||||||
|
@Column(nullable = false, unique = true) private String email;
|
||||||
|
@Column(name = "password_hash", nullable = false) private String passwordHash;
|
||||||
|
@Column(name = "full_name") private String fullName;
|
||||||
|
@Column(nullable = false) private String role = "admin";
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt;
|
||||||
|
@Column(name = "requires_password_change") private boolean requiresPasswordChange = false;
|
||||||
|
@PrePersist protected void onCreate() { this.createdAt = LocalDateTime.now(); }
|
||||||
|
public Long getId() { return id; } public void setId(Long id) { this.id = id; }
|
||||||
|
public Long getOrgId() { return orgId; } public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
public String getEmail() { return email; } public void setEmail(String email) { this.email = email; }
|
||||||
|
public String getPasswordHash() { return passwordHash; } public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
|
||||||
|
public String getFullName() { return fullName; } public void setFullName(String fullName) { this.fullName = fullName; }
|
||||||
|
public String getRole() { return role; } public void setRole(String role) { this.role = role; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
public boolean isRequiresPasswordChange() { return requiresPasswordChange; } public void setRequiresPasswordChange(boolean requiresPasswordChange) { this.requiresPasswordChange = requiresPasswordChange; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "audit_logs")
|
||||||
|
public class AuditLog {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id", nullable = false)
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(name = "user_email")
|
||||||
|
private String userEmail;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String action;
|
||||||
|
|
||||||
|
@Column(name = "entity_type", nullable = false)
|
||||||
|
private String entityType;
|
||||||
|
|
||||||
|
@Column(name = "entity_id")
|
||||||
|
private String entityId;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
private String details;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuditLog() {}
|
||||||
|
|
||||||
|
public AuditLog(Long orgId, String userEmail, String action, String entityType, String entityId, String details) {
|
||||||
|
this.orgId = orgId;
|
||||||
|
this.userEmail = userEmail;
|
||||||
|
this.action = action;
|
||||||
|
this.entityType = entityType;
|
||||||
|
this.entityId = entityId;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
|
||||||
|
public String getUserEmail() { return userEmail; }
|
||||||
|
public void setUserEmail(String userEmail) { this.userEmail = userEmail; }
|
||||||
|
|
||||||
|
public String getAction() { return action; }
|
||||||
|
public void setAction(String action) { this.action = action; }
|
||||||
|
|
||||||
|
public String getEntityType() { return entityType; }
|
||||||
|
public void setEntityType(String entityType) { this.entityType = entityType; }
|
||||||
|
|
||||||
|
public String getEntityId() { return entityId; }
|
||||||
|
public void setEntityId(String entityId) { this.entityId = entityId; }
|
||||||
|
|
||||||
|
public String getDetails() { return details; }
|
||||||
|
public void setDetails(String details) { this.details = details; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "datasets")
|
||||||
|
public class Dataset {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(name = "name", nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "headers", columnDefinition = "text")
|
||||||
|
private String headers;
|
||||||
|
|
||||||
|
@Column(name = "rows", columnDefinition = "text")
|
||||||
|
private String rows;
|
||||||
|
|
||||||
|
@Column(name = "row_count")
|
||||||
|
private Integer rowCount;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
public String getHeaders() { return headers; }
|
||||||
|
public void setHeaders(String headers) { this.headers = headers; }
|
||||||
|
public String getRows() { return rows; }
|
||||||
|
public void setRows(String rows) { this.rows = rows; }
|
||||||
|
public Integer getRowCount() { return rowCount; }
|
||||||
|
public void setRowCount(Integer rowCount) { this.rowCount = rowCount; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "device_pairings")
|
||||||
|
public class DevicePairing {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "pairing_code", nullable = false, unique = true)
|
||||||
|
private String pairingCode;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String status = "PENDING"; // PENDING, PAIRED, EXPIRED
|
||||||
|
|
||||||
|
@Column(name = "agent_token")
|
||||||
|
private String agentToken;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "expires_at", nullable = false)
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
if (this.expiresAt == null) {
|
||||||
|
this.expiresAt = this.createdAt.plusMinutes(15); // Valid for 15 minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public String getPairingCode() { return pairingCode; }
|
||||||
|
public void setPairingCode(String pairingCode) { this.pairingCode = pairingCode; }
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
public String getAgentToken() { return agentToken; }
|
||||||
|
public void setAgentToken(String agentToken) { this.agentToken = agentToken; }
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
public LocalDateTime getExpiresAt() { return expiresAt; }
|
||||||
|
public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "environments")
|
||||||
|
public class Environment {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(name = "name", nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "description", columnDefinition = "text")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() { this.createdAt = LocalDateTime.now(); }
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "executions")
|
||||||
|
public class Execution {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(name = "org_execution_id")
|
||||||
|
private Long orgExecutionId;
|
||||||
|
|
||||||
|
@Column(name = "environment_id")
|
||||||
|
private Long environmentId;
|
||||||
|
|
||||||
|
@Column(name = "target_group_id")
|
||||||
|
private Long targetGroupId;
|
||||||
|
|
||||||
|
@Column(name = "browser_type")
|
||||||
|
private String browserType;
|
||||||
|
|
||||||
|
@Column(name = "browser_version")
|
||||||
|
private String browserVersion;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String status; // QUEUED, RUNNING, SUCCESS, FAILED
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "finished_at")
|
||||||
|
private LocalDateTime finishedAt;
|
||||||
|
|
||||||
|
@Column(name = "environment_json", columnDefinition = "text")
|
||||||
|
private String environmentJson;
|
||||||
|
|
||||||
|
@Column(name = "scheduler_id")
|
||||||
|
private Long schedulerId;
|
||||||
|
|
||||||
|
@Column(name = "ai_analysis", columnDefinition = "text")
|
||||||
|
private String aiAnalysis;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getFinishedAt() {
|
||||||
|
return finishedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFinishedAt(LocalDateTime finishedAt) {
|
||||||
|
this.finishedAt = finishedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEnvironmentJson() {
|
||||||
|
return environmentJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnvironmentJson(String environmentJson) {
|
||||||
|
this.environmentJson = environmentJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
|
||||||
|
public Long getOrgExecutionId() { return orgExecutionId; }
|
||||||
|
public void setOrgExecutionId(Long orgExecutionId) { this.orgExecutionId = orgExecutionId; }
|
||||||
|
|
||||||
|
public Long getEnvironmentId() { return environmentId; }
|
||||||
|
public void setEnvironmentId(Long environmentId) { this.environmentId = environmentId; }
|
||||||
|
|
||||||
|
public Long getTargetGroupId() { return targetGroupId; }
|
||||||
|
public void setTargetGroupId(Long targetGroupId) { this.targetGroupId = targetGroupId; }
|
||||||
|
|
||||||
|
public String getBrowserType() { return browserType; }
|
||||||
|
public void setBrowserType(String browserType) { this.browserType = browserType; }
|
||||||
|
|
||||||
|
public String getBrowserVersion() { return browserVersion; }
|
||||||
|
public void setBrowserVersion(String browserVersion) { this.browserVersion = browserVersion; }
|
||||||
|
|
||||||
|
public Long getSchedulerId() { return schedulerId; }
|
||||||
|
public void setSchedulerId(Long schedulerId) { this.schedulerId = schedulerId; }
|
||||||
|
|
||||||
|
public String getAiAnalysis() { return aiAnalysis; }
|
||||||
|
public void setAiAnalysis(String aiAnalysis) { this.aiAnalysis = aiAnalysis; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "groups")
|
||||||
|
public class Group {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "jobs")
|
||||||
|
public class Job {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "execution_id")
|
||||||
|
private Long executionId;
|
||||||
|
|
||||||
|
@Column(name = "agent_id")
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
@Column(name = "target_group_id")
|
||||||
|
private Long targetGroupId;
|
||||||
|
|
||||||
|
@Column(name = "test_case_id")
|
||||||
|
private Long testCaseId;
|
||||||
|
|
||||||
|
@Column(name = "browser_type")
|
||||||
|
private String browserType;
|
||||||
|
|
||||||
|
@Column(name = "browser_version")
|
||||||
|
private String browserVersion;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String status; // QUEUED, ASSIGNED, COMPLETED, TIMEOUT
|
||||||
|
|
||||||
|
@Column(name = "lease_expires_at")
|
||||||
|
private LocalDateTime leaseExpiresAt;
|
||||||
|
|
||||||
|
@Column(name = "payload_json", nullable = false, columnDefinition = "text")
|
||||||
|
private String payloadJson;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getExecutionId() {
|
||||||
|
return executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExecutionId(Long executionId) {
|
||||||
|
this.executionId = executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAgentId() {
|
||||||
|
return agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAgentId(String agentId) {
|
||||||
|
this.agentId = agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getLeaseExpiresAt() {
|
||||||
|
return leaseExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLeaseExpiresAt(LocalDateTime leaseExpiresAt) {
|
||||||
|
this.leaseExpiresAt = leaseExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPayloadJson() {
|
||||||
|
return payloadJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPayloadJson(String payloadJson) {
|
||||||
|
this.payloadJson = payloadJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrowserType() {
|
||||||
|
return browserType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrowserType(String browserType) {
|
||||||
|
this.browserType = browserType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrowserVersion() {
|
||||||
|
return browserVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrowserVersion(String browserVersion) {
|
||||||
|
this.browserVersion = browserVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTargetGroupId() {
|
||||||
|
return targetGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTargetGroupId(Long targetGroupId) {
|
||||||
|
this.targetGroupId = targetGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTestCaseId() {
|
||||||
|
return testCaseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTestCaseId(Long testCaseId) {
|
||||||
|
this.testCaseId = testCaseId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "organisations")
|
||||||
|
public class Organisation {
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
@Column(nullable = false) private String name;
|
||||||
|
@Column(nullable = false) private String plan = "trial";
|
||||||
|
@Column(unique = true) private String subdomain;
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt;
|
||||||
|
@PrePersist protected void onCreate() { this.createdAt = LocalDateTime.now(); }
|
||||||
|
@Column(name = "public_id", unique = true) private String publicId;
|
||||||
|
// getters/setters for id, name, plan, createdAt, subdomain, publicId
|
||||||
|
public Long getId() { return id; } public void setId(Long id) { this.id = id; }
|
||||||
|
public String getName() { return name; } public void setName(String name) { this.name = name; }
|
||||||
|
public String getPlan() { return plan; } public void setPlan(String plan) { this.plan = plan; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
public String getSubdomain() { return subdomain; } public void setSubdomain(String subdomain) { this.subdomain = subdomain; }
|
||||||
|
public String getPublicId() { return publicId; } public void setPublicId(String publicId) { this.publicId = publicId; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "schedulers")
|
||||||
|
public class Scheduler {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(name = "test_suite_name", nullable = false)
|
||||||
|
private String testSuiteName;
|
||||||
|
|
||||||
|
@Column(name = "execution_type", nullable = false)
|
||||||
|
private String executionType;
|
||||||
|
|
||||||
|
@Column(name = "cron_expression")
|
||||||
|
private String cronExpression;
|
||||||
|
|
||||||
|
@Column(name = "browser_type", nullable = false)
|
||||||
|
private String browserType;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "test_suite_id")
|
||||||
|
private Long testSuiteId;
|
||||||
|
|
||||||
|
@Column(name = "environment_id")
|
||||||
|
private Long environmentId;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "target_group_id")
|
||||||
|
private Long targetGroupId;
|
||||||
|
|
||||||
|
@Column(name = "browser_version")
|
||||||
|
private String browserVersion;
|
||||||
|
|
||||||
|
// ─── Outlook-style scheduling fields ──────────────────────────────────────
|
||||||
|
|
||||||
|
/** The specific date to run (used for 'once' and as the start date for recurring) */
|
||||||
|
@Column(name = "scheduled_date")
|
||||||
|
private LocalDate scheduledDate;
|
||||||
|
|
||||||
|
/** The time of day to run */
|
||||||
|
@Column(name = "scheduled_time")
|
||||||
|
private LocalTime scheduledTime;
|
||||||
|
|
||||||
|
/** Recurrence pattern: 'once', 'daily', 'weekly', 'monthly' */
|
||||||
|
@Column(name = "recurrence_type")
|
||||||
|
private String recurrenceType;
|
||||||
|
|
||||||
|
/** Comma-separated day abbreviations for weekly recurrence, e.g. "MON,WED,FRI" */
|
||||||
|
@Column(name = "recurrence_days")
|
||||||
|
private String recurrenceDays;
|
||||||
|
|
||||||
|
/** Optional end date for recurring schedules */
|
||||||
|
@Column(name = "recurrence_end_date")
|
||||||
|
private LocalDate recurrenceEndDate;
|
||||||
|
|
||||||
|
@Column(name = "timezone")
|
||||||
|
private String timezone;
|
||||||
|
|
||||||
|
// ———————————————————————————————————————————————————————————————————————————
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTestSuiteName() {
|
||||||
|
return testSuiteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTestSuiteName(String testSuiteName) {
|
||||||
|
this.testSuiteName = testSuiteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExecutionType() {
|
||||||
|
return executionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExecutionType(String executionType) {
|
||||||
|
this.executionType = executionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCronExpression() {
|
||||||
|
return cronExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCronExpression(String cronExpression) {
|
||||||
|
this.cronExpression = cronExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrowserType() {
|
||||||
|
return browserType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrowserType(String browserType) {
|
||||||
|
this.browserType = browserType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTestSuiteId() {
|
||||||
|
return testSuiteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTestSuiteId(Long testSuiteId) {
|
||||||
|
this.testSuiteId = testSuiteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getScheduledDate() {
|
||||||
|
return scheduledDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScheduledDate(LocalDate scheduledDate) {
|
||||||
|
this.scheduledDate = scheduledDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalTime getScheduledTime() {
|
||||||
|
return scheduledTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScheduledTime(LocalTime scheduledTime) {
|
||||||
|
this.scheduledTime = scheduledTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRecurrenceType() {
|
||||||
|
return recurrenceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecurrenceType(String recurrenceType) {
|
||||||
|
this.recurrenceType = recurrenceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRecurrenceDays() {
|
||||||
|
return recurrenceDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecurrenceDays(String recurrenceDays) {
|
||||||
|
this.recurrenceDays = recurrenceDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getRecurrenceEndDate() {
|
||||||
|
return recurrenceEndDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecurrenceEndDate(LocalDate recurrenceEndDate) {
|
||||||
|
this.recurrenceEndDate = recurrenceEndDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimezone() {
|
||||||
|
return timezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimezone(String timezone) {
|
||||||
|
this.timezone = timezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
|
||||||
|
public Long getEnvironmentId() { return environmentId; }
|
||||||
|
public void setEnvironmentId(Long environmentId) { this.environmentId = environmentId; }
|
||||||
|
|
||||||
|
public Long getTargetGroupId() { return targetGroupId; }
|
||||||
|
public void setTargetGroupId(Long targetGroupId) { this.targetGroupId = targetGroupId; }
|
||||||
|
|
||||||
|
public String getBrowserVersion() { return browserVersion; }
|
||||||
|
public void setBrowserVersion(String browserVersion) { this.browserVersion = browserVersion; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "screenshots")
|
||||||
|
public class Screenshot {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "execution_id")
|
||||||
|
private Long executionId;
|
||||||
|
|
||||||
|
@Column(name = "step_result_id")
|
||||||
|
private Long stepResultId;
|
||||||
|
|
||||||
|
@Column(name = "file_name", nullable = false)
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@Column(name = "content_type", nullable = false)
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
@Column(name = "storage_path", nullable = false)
|
||||||
|
private String storagePath;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getExecutionId() {
|
||||||
|
return executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExecutionId(Long executionId) {
|
||||||
|
this.executionId = executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getStepResultId() {
|
||||||
|
return stepResultId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStepResultId(Long stepResultId) {
|
||||||
|
this.stepResultId = stepResultId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileName(String fileName) {
|
||||||
|
this.fileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentType(String contentType) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStoragePath() {
|
||||||
|
return storagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStoragePath(String storagePath) {
|
||||||
|
this.storagePath = storagePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "step_results")
|
||||||
|
public class StepResult {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "execution_id")
|
||||||
|
private Long executionId;
|
||||||
|
|
||||||
|
@Column(name = "step_index", nullable = false)
|
||||||
|
private Integer stepIndex;
|
||||||
|
|
||||||
|
@Column(name = "action_name", nullable = false)
|
||||||
|
private String actionName;
|
||||||
|
|
||||||
|
@Column(name = "executed_status", nullable = false)
|
||||||
|
private Integer executedStatus;
|
||||||
|
|
||||||
|
@Column(name = "result_status", nullable = false)
|
||||||
|
private Integer resultStatus;
|
||||||
|
|
||||||
|
@Column(name = "error_json", columnDefinition = "text")
|
||||||
|
private String errorJson;
|
||||||
|
|
||||||
|
@Column(name = "actual_value", columnDefinition = "text")
|
||||||
|
private String actualValue;
|
||||||
|
|
||||||
|
@Column(name = "step_type")
|
||||||
|
private String stepType = "ACTION";
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getExecutionId() {
|
||||||
|
return executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExecutionId(Long executionId) {
|
||||||
|
this.executionId = executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStepIndex() {
|
||||||
|
return stepIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStepIndex(Integer stepIndex) {
|
||||||
|
this.stepIndex = stepIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getActionName() {
|
||||||
|
return actionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActionName(String actionName) {
|
||||||
|
this.actionName = actionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getExecutedStatus() {
|
||||||
|
return executedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExecutedStatus(Integer executedStatus) {
|
||||||
|
this.executedStatus = executedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getResultStatus() {
|
||||||
|
return resultStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResultStatus(Integer resultStatus) {
|
||||||
|
this.resultStatus = resultStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorJson() {
|
||||||
|
return errorJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorJson(String errorJson) {
|
||||||
|
this.errorJson = errorJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getActualValue() {
|
||||||
|
return actualValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActualValue(String actualValue) {
|
||||||
|
this.actualValue = actualValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStepType() {
|
||||||
|
return stepType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStepType(String stepType) {
|
||||||
|
this.stepType = stepType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "test_cases")
|
||||||
|
public class TestCase {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String status = "active";
|
||||||
|
|
||||||
|
@Column(name = "is_component")
|
||||||
|
private Boolean isComponent = false;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
|
||||||
|
public Boolean getIsComponent() { return isComponent; }
|
||||||
|
public void setIsComponent(Boolean isComponent) { this.isComponent = isComponent; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "test_case_groups")
|
||||||
|
public class TestCaseGroup {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String status = "active";
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "test_case_group_mappings")
|
||||||
|
public class TestCaseGroupMapping {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "test_case_group_id", nullable = false)
|
||||||
|
private Long testCaseGroupId;
|
||||||
|
|
||||||
|
@Column(name = "test_case_id", nullable = false)
|
||||||
|
private Long testCaseId;
|
||||||
|
|
||||||
|
@Column(name = "case_order", nullable = false)
|
||||||
|
private Integer caseOrder = 0;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Long getTestCaseGroupId() { return testCaseGroupId; }
|
||||||
|
public void setTestCaseGroupId(Long testCaseGroupId) { this.testCaseGroupId = testCaseGroupId; }
|
||||||
|
|
||||||
|
public Long getTestCaseId() { return testCaseId; }
|
||||||
|
public void setTestCaseId(Long testCaseId) { this.testCaseId = testCaseId; }
|
||||||
|
|
||||||
|
public Integer getCaseOrder() { return caseOrder; }
|
||||||
|
public void setCaseOrder(Integer caseOrder) { this.caseOrder = caseOrder; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "test_steps")
|
||||||
|
public class TestStep {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "test_case_id", nullable = false)
|
||||||
|
private Long testCaseId;
|
||||||
|
|
||||||
|
@Column(name = "step_order", nullable = false)
|
||||||
|
private Integer stepOrder;
|
||||||
|
|
||||||
|
@Column(name = "action_name", nullable = false)
|
||||||
|
private String actionName;
|
||||||
|
|
||||||
|
@Column(name = "locator_type")
|
||||||
|
private String locatorType;
|
||||||
|
|
||||||
|
@Column(name = "locator_value")
|
||||||
|
private String locatorValue;
|
||||||
|
|
||||||
|
@Column(name = "test_data")
|
||||||
|
private String testData;
|
||||||
|
|
||||||
|
@Column(name = "step_type", nullable = false)
|
||||||
|
private String stepType = "ACTION"; // ACTION or VERIFY
|
||||||
|
|
||||||
|
@Column(name = "expected_value", columnDefinition = "text")
|
||||||
|
private String expectedValue;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Long getTestCaseId() { return testCaseId; }
|
||||||
|
public void setTestCaseId(Long testCaseId) { this.testCaseId = testCaseId; }
|
||||||
|
|
||||||
|
public Integer getStepOrder() { return stepOrder; }
|
||||||
|
public void setStepOrder(Integer stepOrder) { this.stepOrder = stepOrder; }
|
||||||
|
|
||||||
|
public String getActionName() { return actionName; }
|
||||||
|
public void setActionName(String actionName) { this.actionName = actionName; }
|
||||||
|
|
||||||
|
public String getLocatorType() { return locatorType; }
|
||||||
|
public void setLocatorType(String locatorType) { this.locatorType = locatorType; }
|
||||||
|
|
||||||
|
public String getLocatorValue() { return locatorValue; }
|
||||||
|
public void setLocatorValue(String locatorValue) { this.locatorValue = locatorValue; }
|
||||||
|
|
||||||
|
public String getTestData() { return testData; }
|
||||||
|
public void setTestData(String testData) { this.testData = testData; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getStepType() { return stepType; }
|
||||||
|
public void setStepType(String stepType) { this.stepType = stepType; }
|
||||||
|
|
||||||
|
public String getExpectedValue() { return expectedValue; }
|
||||||
|
public void setExpectedValue(String expectedValue) { this.expectedValue = expectedValue; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "test_suites")
|
||||||
|
public class TestSuite {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "browser_type", nullable = false)
|
||||||
|
private String browserType = "chrome";
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String status = "active";
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getBrowserType() { return browserType; }
|
||||||
|
public void setBrowserType(String browserType) { this.browserType = browserType; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "test_suite_group_mappings")
|
||||||
|
public class TestSuiteGroupMapping {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "test_suite_id", nullable = false)
|
||||||
|
private Long testSuiteId;
|
||||||
|
|
||||||
|
@Column(name = "test_case_group_id", nullable = false)
|
||||||
|
private Long testCaseGroupId;
|
||||||
|
|
||||||
|
@Column(name = "group_order", nullable = false)
|
||||||
|
private Integer groupOrder = 0;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Long getTestSuiteId() { return testSuiteId; }
|
||||||
|
public void setTestSuiteId(Long testSuiteId) { this.testSuiteId = testSuiteId; }
|
||||||
|
|
||||||
|
public Long getTestCaseGroupId() { return testCaseGroupId; }
|
||||||
|
public void setTestCaseGroupId(Long testCaseGroupId) { this.testCaseGroupId = testCaseGroupId; }
|
||||||
|
|
||||||
|
public Integer getGroupOrder() { return groupOrder; }
|
||||||
|
public void setGroupOrder(Integer groupOrder) { this.groupOrder = groupOrder; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.autopilot.localagent_cloud.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "variables")
|
||||||
|
public class Variable {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "org_id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@Column(name = "scope", nullable = false)
|
||||||
|
private String scope = "GLOBAL"; // GLOBAL, SUITE, ENVIRONMENT
|
||||||
|
|
||||||
|
@Column(name = "scope_id")
|
||||||
|
private Long scopeId;
|
||||||
|
|
||||||
|
@Column(name = "key_name", nullable = false)
|
||||||
|
private String keyName;
|
||||||
|
|
||||||
|
@Column(name = "value", columnDefinition = "text")
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
@Column(name = "is_secret")
|
||||||
|
private Boolean isSecret = false;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() { this.createdAt = LocalDateTime.now(); }
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public Long getOrgId() { return orgId; }
|
||||||
|
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
||||||
|
public String getScope() { return scope; }
|
||||||
|
public void setScope(String scope) { this.scope = scope; }
|
||||||
|
public Long getScopeId() { return scopeId; }
|
||||||
|
public void setScopeId(Long scopeId) { this.scopeId = scopeId; }
|
||||||
|
public String getKeyName() { return keyName; }
|
||||||
|
public void setKeyName(String keyName) { this.keyName = keyName; }
|
||||||
|
public String getValue() { return value; }
|
||||||
|
public void setValue(String value) { this.value = value; }
|
||||||
|
public Boolean getIsSecret() { return isSecret; }
|
||||||
|
public void setIsSecret(Boolean isSecret) { this.isSecret = isSecret; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.AgentGroupMapping;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface AgentGroupMappingRepository extends JpaRepository<AgentGroupMapping, Long> {
|
||||||
|
List<AgentGroupMapping> findByGroupId(Long groupId);
|
||||||
|
List<AgentGroupMapping> findByAgentId(String agentId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
void deleteByAgentIdAndGroupId(String agentId, Long groupId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Agent;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface AgentRepository extends JpaRepository<Agent, String> {
|
||||||
|
List<Agent> findByOrgId(Long orgId);
|
||||||
|
List<Agent> findAll();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.AgentToken;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface AgentTokenRepository extends JpaRepository<AgentToken, Long> {
|
||||||
|
Optional<AgentToken> findByToken(String token);
|
||||||
|
java.util.List<AgentToken> findByOrgId(Long orgId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.ApiKey;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ApiKeyRepository extends JpaRepository<ApiKey, Long> {
|
||||||
|
List<ApiKey> findByOrgId(Long orgId);
|
||||||
|
Optional<ApiKey> findByToken(String token);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.AppUser;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface AppUserRepository extends JpaRepository<AppUser, Long> {
|
||||||
|
Optional<AppUser> findByEmail(String email);
|
||||||
|
boolean existsByEmail(String email);
|
||||||
|
java.util.List<AppUser> findByOrgId(Long orgId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.AuditLog;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
|
||||||
|
List<AuditLog> findByOrgIdOrderByCreatedAtDesc(Long orgId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Dataset;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface DatasetRepository extends JpaRepository<Dataset, Long> {
|
||||||
|
List<Dataset> findByOrgId(Long orgId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.DevicePairing;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface DevicePairingRepository extends JpaRepository<DevicePairing, Long> {
|
||||||
|
Optional<DevicePairing> findByPairingCode(String pairingCode);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Environment;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface EnvironmentRepository extends JpaRepository<Environment, Long> {
|
||||||
|
List<Environment> findByOrgId(Long orgId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Execution;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ExecutionRepository extends JpaRepository<Execution, Long> {
|
||||||
|
List<Execution> findAllByOrderByIdDesc();
|
||||||
|
long countByOrgId(Long orgId);
|
||||||
|
List<Execution> findByStatusAndCreatedAtBefore(String status, java.time.LocalDateTime cutoff);
|
||||||
|
Optional<Execution> findFirstBySchedulerIdOrderByIdDesc(Long schedulerId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Group;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface GroupRepository extends JpaRepository<Group, Long> {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Job;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface JobRepository extends JpaRepository<Job, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT j FROM Job j WHERE j.status = 'QUEUED' " +
|
||||||
|
"AND (j.agentId = :agentId OR j.agentId IS NULL) " +
|
||||||
|
"AND (j.targetGroupId IS NULL OR j.targetGroupId IN :groupIDs) " +
|
||||||
|
"ORDER BY j.id ASC")
|
||||||
|
List<Job> findNextAvailableJobs(@Param("agentId") String agentId, @Param("groupIDs") List<Long> groupIDs, Pageable pageable);
|
||||||
|
|
||||||
|
List<Job> findByExecutionId(Long executionId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Organisation;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface OrganisationRepository extends JpaRepository<Organisation, Long> {
|
||||||
|
Organisation findBySubdomain(String subdomain);
|
||||||
|
Organisation findByPublicId(String publicId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Scheduler;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SchedulerRepository extends JpaRepository<Scheduler, Long> {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Screenshot;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ScreenshotRepository extends JpaRepository<Screenshot, Long> {
|
||||||
|
List<Screenshot> findByExecutionId(Long executionId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.StepResult;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface StepResultRepository extends JpaRepository<StepResult, Long> {
|
||||||
|
List<StepResult> findByExecutionId(Long executionId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestCaseGroupMapping;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TestCaseGroupMappingRepository extends JpaRepository<TestCaseGroupMapping, Long> {
|
||||||
|
List<TestCaseGroupMapping> findByTestCaseGroupIdOrderByCaseOrder(Long testCaseGroupId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
void deleteByTestCaseGroupId(Long testCaseGroupId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestCaseGroup;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TestCaseGroupRepository extends JpaRepository<TestCaseGroup, Long> {
|
||||||
|
List<TestCaseGroup> findAllByOrderByIdDesc();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestCase;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TestCaseRepository extends JpaRepository<TestCase, Long> {
|
||||||
|
List<TestCase> findAllByOrderByIdDesc();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestStep;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TestStepRepository extends JpaRepository<TestStep, Long> {
|
||||||
|
List<TestStep> findByTestCaseIdOrderByStepOrder(Long testCaseId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
void deleteByTestCaseId(Long testCaseId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestSuiteGroupMapping;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TestSuiteGroupMappingRepository extends JpaRepository<TestSuiteGroupMapping, Long> {
|
||||||
|
List<TestSuiteGroupMapping> findByTestSuiteIdOrderByGroupOrder(Long testSuiteId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
void deleteByTestSuiteId(Long testSuiteId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestSuite;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TestSuiteRepository extends JpaRepository<TestSuite, Long> {
|
||||||
|
List<TestSuite> findAllByOrderByIdDesc();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.autopilot.localagent_cloud.repository;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Variable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface VariableRepository extends JpaRepository<Variable, Long> {
|
||||||
|
List<Variable> findByOrgId(Long orgId);
|
||||||
|
List<Variable> findByOrgIdAndScope(Long orgId, String scope);
|
||||||
|
List<Variable> findByOrgIdAndScopeAndScopeId(Long orgId, String scope, Long scopeId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,508 @@
|
||||||
|
package com.autopilot.localagent_cloud.service;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Agent;
|
||||||
|
import com.autopilot.localagent_cloud.model.AgentToken;
|
||||||
|
import com.autopilot.localagent_cloud.model.Execution;
|
||||||
|
import com.autopilot.localagent_cloud.model.Scheduler;
|
||||||
|
import com.autopilot.localagent_cloud.model.Screenshot;
|
||||||
|
import com.autopilot.localagent_cloud.model.StepResult;
|
||||||
|
import com.autopilot.localagent_cloud.model.TestCaseGroupMapping;
|
||||||
|
import com.autopilot.localagent_cloud.model.TestStep;
|
||||||
|
import com.autopilot.localagent_cloud.model.TestSuite;
|
||||||
|
import com.autopilot.localagent_cloud.model.TestSuiteGroupMapping;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AgentRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AgentTokenRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.ExecutionRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.SchedulerRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.ScreenshotRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.StepResultRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestCaseGroupMappingRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestStepRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestSuiteGroupMappingRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestSuiteRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.VariableRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.OrganisationRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AgentGroupMappingRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.JobRepository;
|
||||||
|
import com.autopilot.localagent_cloud.model.Organisation;
|
||||||
|
import com.autopilot.localagent_cloud.model.Variable;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AgentService {
|
||||||
|
|
||||||
|
private final AgentRepository agentRepository;
|
||||||
|
private final AgentTokenRepository agentTokenRepository;
|
||||||
|
private final SchedulerRepository schedulerRepository;
|
||||||
|
private final ExecutionRepository executionRepository;
|
||||||
|
private final TestSuiteRepository testSuiteRepository;
|
||||||
|
private final TestSuiteGroupMappingRepository testSuiteGroupMappingRepository;
|
||||||
|
private final TestCaseGroupMappingRepository testCaseGroupMappingRepository;
|
||||||
|
private final TestStepRepository testStepRepository;
|
||||||
|
private final ScreenshotRepository screenshotRepository;
|
||||||
|
private final StepResultRepository stepResultRepository;
|
||||||
|
private final OrganisationRepository organisationRepository;
|
||||||
|
private final VariableRepository variableRepository;
|
||||||
|
private final AgentGroupMappingRepository agentGroupMappingRepository;
|
||||||
|
private final JobRepository jobRepository;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final S3Service s3Service;
|
||||||
|
|
||||||
|
public AgentService(AgentRepository agentRepository,
|
||||||
|
AgentTokenRepository agentTokenRepository,
|
||||||
|
SchedulerRepository schedulerRepository,
|
||||||
|
ExecutionRepository executionRepository,
|
||||||
|
TestSuiteRepository testSuiteRepository,
|
||||||
|
TestSuiteGroupMappingRepository testSuiteGroupMappingRepository,
|
||||||
|
TestCaseGroupMappingRepository testCaseGroupMappingRepository,
|
||||||
|
TestStepRepository testStepRepository,
|
||||||
|
ScreenshotRepository screenshotRepository,
|
||||||
|
StepResultRepository stepResultRepository,
|
||||||
|
OrganisationRepository organisationRepository,
|
||||||
|
VariableRepository variableRepository,
|
||||||
|
AgentGroupMappingRepository agentGroupMappingRepository,
|
||||||
|
JobRepository jobRepository,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
S3Service s3Service) {
|
||||||
|
this.agentRepository = agentRepository;
|
||||||
|
this.agentTokenRepository = agentTokenRepository;
|
||||||
|
this.schedulerRepository = schedulerRepository;
|
||||||
|
this.executionRepository = executionRepository;
|
||||||
|
this.testSuiteRepository = testSuiteRepository;
|
||||||
|
this.testSuiteGroupMappingRepository = testSuiteGroupMappingRepository;
|
||||||
|
this.testCaseGroupMappingRepository = testCaseGroupMappingRepository;
|
||||||
|
this.testStepRepository = testStepRepository;
|
||||||
|
this.screenshotRepository = screenshotRepository;
|
||||||
|
this.stepResultRepository = stepResultRepository;
|
||||||
|
this.organisationRepository = organisationRepository;
|
||||||
|
this.variableRepository = variableRepository;
|
||||||
|
this.agentGroupMappingRepository = agentGroupMappingRepository;
|
||||||
|
this.jobRepository = jobRepository;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.s3Service = s3Service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<List<Agent>> getAll(Long orgId) {
|
||||||
|
List<Agent> list = orgId != null
|
||||||
|
? agentRepository.findAll().stream().filter(a -> orgId.equals(a.getOrgId())).toList()
|
||||||
|
: agentRepository.findAll();
|
||||||
|
return ResponseEntity.ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Map<String, Object>> register(Map<String, Object> body) {
|
||||||
|
String agentId = (String) body.get("id");
|
||||||
|
String agentTokenStr = (String) body.get("agentToken");
|
||||||
|
|
||||||
|
if (agentId == null || agentId.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Missing agent id"));
|
||||||
|
}
|
||||||
|
if (agentTokenStr == null || agentTokenStr.isBlank()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", "Missing agentToken"));
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentToken token = agentTokenRepository.findByToken(agentTokenStr).orElse(null);
|
||||||
|
if (token == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", "Invalid agentToken"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Agent agent = agentRepository.findById(agentId).orElse(new Agent());
|
||||||
|
agent.setId(agentId);
|
||||||
|
agent.setOrgId(token.getOrgId());
|
||||||
|
agent.setName((String) body.getOrDefault("name", agentId));
|
||||||
|
agent.setOs((String) body.get("os"));
|
||||||
|
agent.setAgentVersion((String) body.get("agentVersion"));
|
||||||
|
agent.setCapabilitiesJson((String) body.get("capabilitiesJson"));
|
||||||
|
agent.setLastSeenAt(java.time.LocalDateTime.now());
|
||||||
|
|
||||||
|
agentRepository.save(agent);
|
||||||
|
|
||||||
|
String orgName = "Unknown Organisation";
|
||||||
|
if (token.getOrgId() != null) {
|
||||||
|
Organisation org = organisationRepository.findById(token.getOrgId()).orElse(null);
|
||||||
|
if (org != null) {
|
||||||
|
orgName = org.getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("status", "success");
|
||||||
|
response.put("orgName", orgName);
|
||||||
|
response.put("tokenLabel", token.getLabel());
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> heartbeat(String id) {
|
||||||
|
agentRepository.findById(id).ifPresent(agent -> {
|
||||||
|
agent.setLastSeenAt(java.time.LocalDateTime.now());
|
||||||
|
agentRepository.save(agent);
|
||||||
|
});
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public ResponseEntity<Map<String, Object>> getNextJob(String agentId) {
|
||||||
|
Agent agent = agentRepository.findById(agentId).orElse(null);
|
||||||
|
if (agent == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.setLastSeenAt(java.time.LocalDateTime.now());
|
||||||
|
agentRepository.save(agent);
|
||||||
|
|
||||||
|
Long agentOrgId = agent.getOrgId();
|
||||||
|
|
||||||
|
List<Long> agentGroupIds = agentGroupMappingRepository.findByAgentId(agentId).stream()
|
||||||
|
.map(com.autopilot.localagent_cloud.model.AgentGroupMapping::getGroupId)
|
||||||
|
.toList();
|
||||||
|
List<Long> groupIDsForQuery = agentGroupIds.isEmpty() ? List.of(-1L) : agentGroupIds;
|
||||||
|
|
||||||
|
String agentBrowserVersion = "";
|
||||||
|
try {
|
||||||
|
if (agent.getCapabilitiesJson() != null) {
|
||||||
|
Map<String, Object> caps = objectMapper.readValue(agent.getCapabilitiesJson(), Map.class);
|
||||||
|
if (caps.containsKey("browserVersion")) {
|
||||||
|
agentBrowserVersion = (String) caps.get("browserVersion");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {}
|
||||||
|
final String finalAgentBrowserVer = agentBrowserVersion;
|
||||||
|
|
||||||
|
// Phase 1: Try to pick up an existing QUEUED job
|
||||||
|
List<com.autopilot.localagent_cloud.model.Job> candidateJobs = jobRepository.findNextAvailableJobs(agentId, groupIDsForQuery, PageRequest.of(0, 50));
|
||||||
|
com.autopilot.localagent_cloud.model.Job selectedJob = candidateJobs.stream().findFirst().orElse(null);
|
||||||
|
|
||||||
|
if (selectedJob != null) {
|
||||||
|
selectedJob.setStatus("ASSIGNED");
|
||||||
|
selectedJob.setAgentId(agentId);
|
||||||
|
selectedJob.setLeaseExpiresAt(java.time.LocalDateTime.now().plusMinutes(10));
|
||||||
|
jobRepository.save(selectedJob);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, Object> runRequest = objectMapper.readValue(selectedJob.getPayloadJson(), Map.class);
|
||||||
|
runRequest.put("jobId", selectedJob.getId()); // Inject jobId for the agent
|
||||||
|
|
||||||
|
Map<String, Object> jobDto = new HashMap<>();
|
||||||
|
jobDto.put("executionId", selectedJob.getExecutionId());
|
||||||
|
jobDto.put("jobId", selectedJob.getId());
|
||||||
|
jobDto.put("payloadJson", objectMapper.writeValueAsString(runRequest));
|
||||||
|
return ResponseEntity.ok(jobDto);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Unwrap a Scheduler into multiple QUEUED Jobs
|
||||||
|
List<Scheduler> activeJobs = schedulerRepository.findAll().stream()
|
||||||
|
.filter(s -> "now".equals(s.getExecutionType()) && "active".equals(s.getStatus()))
|
||||||
|
.filter(s -> agentOrgId == null || agentOrgId.equals(s.getOrgId()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (activeJobs.isEmpty()) {
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Scheduler job = activeJobs.get(0);
|
||||||
|
job.setStatus("processing");
|
||||||
|
schedulerRepository.save(job);
|
||||||
|
|
||||||
|
Execution execution = new Execution();
|
||||||
|
execution.setOrgId(agentOrgId);
|
||||||
|
|
||||||
|
long count = agentOrgId != null ? executionRepository.countByOrgId(agentOrgId) : 0;
|
||||||
|
execution.setOrgExecutionId(count + 1);
|
||||||
|
execution.setEnvironmentId(job.getEnvironmentId());
|
||||||
|
execution.setTargetGroupId(job.getTargetGroupId());
|
||||||
|
execution.setBrowserType(job.getBrowserType());
|
||||||
|
execution.setBrowserVersion(job.getBrowserVersion());
|
||||||
|
execution.setSchedulerId(job.getId());
|
||||||
|
|
||||||
|
execution.setEnvironmentJson("{\"referenceId\":\"" + job.getTestSuiteName() + "\",\"browserTypeName\":\"" + job.getBrowserType() + "\"}");
|
||||||
|
execution.setStatus("running");
|
||||||
|
execution.setCreatedAt(java.time.LocalDateTime.now());
|
||||||
|
execution = executionRepository.save(execution);
|
||||||
|
|
||||||
|
// Resolve Variables: GLOBAL and ENVIRONMENT
|
||||||
|
Map<String, String> executionVariables = new HashMap<>();
|
||||||
|
if (agentOrgId != null) {
|
||||||
|
List<Variable> globalVars = variableRepository.findByOrgIdAndScope(agentOrgId, "GLOBAL");
|
||||||
|
for (Variable v : globalVars) {
|
||||||
|
executionVariables.put(v.getKeyName(), v.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.getEnvironmentId() != null) {
|
||||||
|
List<Variable> envVars = variableRepository.findByOrgIdAndScopeAndScopeId(agentOrgId, "ENVIRONMENT", job.getEnvironmentId());
|
||||||
|
for (Variable v : envVars) {
|
||||||
|
executionVariables.put(v.getKeyName(), v.getValue()); // overrides global
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.getTestSuiteId() != null) {
|
||||||
|
List<TestSuiteGroupMapping> groupMappings = testSuiteGroupMappingRepository.findByTestSuiteIdOrderByGroupOrder(job.getTestSuiteId());
|
||||||
|
for (TestSuiteGroupMapping gm : groupMappings) {
|
||||||
|
List<TestCaseGroupMapping> caseMappings = testCaseGroupMappingRepository.findByTestCaseGroupIdOrderByCaseOrder(gm.getTestCaseGroupId());
|
||||||
|
for (TestCaseGroupMapping cm : caseMappings) {
|
||||||
|
List<TestStep> rawSteps = testStepRepository.findByTestCaseIdOrderByStepOrder(cm.getTestCaseId());
|
||||||
|
List<Map<String, Object>> processedSteps = new ArrayList<>();
|
||||||
|
for (TestStep s : rawSteps) {
|
||||||
|
if ("CALL_COMPONENT".equals(s.getActionName())) {
|
||||||
|
try {
|
||||||
|
Long compId = Long.valueOf(s.getLocatorValue());
|
||||||
|
List<TestStep> compSteps = testStepRepository.findByTestCaseIdOrderByStepOrder(compId);
|
||||||
|
for (TestStep cs : compSteps) {
|
||||||
|
processedSteps.add(stepToMapAndReplace(cs, executionVariables));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {}
|
||||||
|
} else {
|
||||||
|
processedSteps.add(stepToMapAndReplace(s, executionVariables));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> iter = new HashMap<>();
|
||||||
|
iter.put("testCaseId", cm.getTestCaseId());
|
||||||
|
iter.put("steps", processedSteps);
|
||||||
|
|
||||||
|
Map<String, Object> runRequest = new HashMap<>();
|
||||||
|
runRequest.put("executionId", execution.getId());
|
||||||
|
runRequest.put("browserType", job.getBrowserType());
|
||||||
|
runRequest.put("iterations", List.of(iter)); // ONLY THIS TEST CASE!
|
||||||
|
runRequest.put("variables", executionVariables);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String payloadJson = objectMapper.writeValueAsString(runRequest);
|
||||||
|
com.autopilot.localagent_cloud.model.Job newJob = new com.autopilot.localagent_cloud.model.Job();
|
||||||
|
newJob.setExecutionId(execution.getId());
|
||||||
|
newJob.setTestCaseId(cm.getTestCaseId());
|
||||||
|
newJob.setTargetGroupId(job.getTargetGroupId());
|
||||||
|
newJob.setBrowserType(job.getBrowserType());
|
||||||
|
newJob.setBrowserVersion(job.getBrowserVersion());
|
||||||
|
newJob.setStatus("QUEUED");
|
||||||
|
newJob.setPayloadJson(payloadJson);
|
||||||
|
jobRepository.save(newJob);
|
||||||
|
} catch (Exception e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bug fix: replaced unbounded recursion with a simple retry — pick up one of the jobs just created.
|
||||||
|
// The recursive approach had no depth limit and could cause a StackOverflowError
|
||||||
|
// if job creation failed unexpectedly.
|
||||||
|
List<com.autopilot.localagent_cloud.model.Job> retryJobs = jobRepository.findNextAvailableJobs(
|
||||||
|
agentId, groupIDsForQuery, PageRequest.of(0, 50));
|
||||||
|
com.autopilot.localagent_cloud.model.Job retryJob = retryJobs.stream().filter(j -> {
|
||||||
|
if (j.getBrowserVersion() == null || j.getBrowserVersion().isBlank()) return true;
|
||||||
|
if (finalAgentBrowserVer.isBlank()) return false;
|
||||||
|
String reqMajor = j.getBrowserVersion().split("\\.")[0];
|
||||||
|
String agentMajor = finalAgentBrowserVer.split("\\.")[0];
|
||||||
|
return reqMajor.equals(agentMajor);
|
||||||
|
}).findFirst().orElse(null);
|
||||||
|
|
||||||
|
if (retryJob != null) {
|
||||||
|
retryJob.setStatus("ASSIGNED");
|
||||||
|
retryJob.setAgentId(agentId);
|
||||||
|
retryJob.setLeaseExpiresAt(java.time.LocalDateTime.now().plusMinutes(10));
|
||||||
|
jobRepository.save(retryJob);
|
||||||
|
try {
|
||||||
|
Map<String, Object> runRequest = objectMapper.readValue(retryJob.getPayloadJson(), Map.class);
|
||||||
|
runRequest.put("jobId", retryJob.getId());
|
||||||
|
Map<String, Object> jobDto = new HashMap<>();
|
||||||
|
jobDto.put("executionId", retryJob.getExecutionId());
|
||||||
|
jobDto.put("jobId", retryJob.getId());
|
||||||
|
jobDto.put("payloadJson", objectMapper.writeValueAsString(runRequest));
|
||||||
|
return ResponseEntity.ok(jobDto);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public ResponseEntity<Void> postResults(Long executionId, Map<String, Object> result) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> runResult = result;
|
||||||
|
if (result.containsKey("result")) {
|
||||||
|
runResult = (Map<String, Object>) result.get("result");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correctly parse the overall result_status sent by the Java Agent
|
||||||
|
String finalStatus = "completed";
|
||||||
|
if (runResult.containsKey("result_status")) {
|
||||||
|
Object rs = runResult.get("result_status");
|
||||||
|
if (rs instanceof Number && ((Number) rs).intValue() == 0) {
|
||||||
|
finalStatus = "failed";
|
||||||
|
}
|
||||||
|
} else if (result.containsKey("status")) {
|
||||||
|
finalStatus = (String) result.get("status");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String statusToSave = finalStatus;
|
||||||
|
|
||||||
|
Long jobId = null;
|
||||||
|
if (result.containsKey("jobId")) {
|
||||||
|
jobId = ((Number) result.get("jobId")).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobId != null) {
|
||||||
|
jobRepository.findById(jobId).ifPresent(job -> {
|
||||||
|
job.setStatus(statusToSave);
|
||||||
|
jobRepository.save(job);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if all jobs are completed
|
||||||
|
List<com.autopilot.localagent_cloud.model.Job> allJobs = jobRepository.findByExecutionId(executionId);
|
||||||
|
boolean allDone = allJobs.stream().allMatch(j -> "completed".equals(j.getStatus()) || "failed".equals(j.getStatus()));
|
||||||
|
boolean anyFailed = allJobs.stream().anyMatch(j -> "failed".equals(j.getStatus()));
|
||||||
|
|
||||||
|
if (allDone && !allJobs.isEmpty()) {
|
||||||
|
executionRepository.findById(executionId).ifPresent(exec -> {
|
||||||
|
exec.setStatus(anyFailed ? "failed" : "completed");
|
||||||
|
exec.setFinishedAt(java.time.LocalDateTime.now());
|
||||||
|
executionRepository.save(exec);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback if jobId is not provided
|
||||||
|
executionRepository.findById(executionId).ifPresent(exec -> {
|
||||||
|
exec.setStatus(statusToSave);
|
||||||
|
exec.setFinishedAt(java.time.LocalDateTime.now());
|
||||||
|
executionRepository.save(exec);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.List<Map<String, Object>> testCaseList = (java.util.List<Map<String, Object>>) runResult.get("testCase");
|
||||||
|
if (testCaseList != null && !testCaseList.isEmpty()) {
|
||||||
|
Map<String, Object> firstIterationMap = testCaseList.get(0);
|
||||||
|
java.util.List<Map<String, Object>> iterations = (java.util.List<Map<String, Object>>) firstIterationMap.get("iteration1");
|
||||||
|
if (iterations != null && !iterations.isEmpty()) {
|
||||||
|
java.util.List<Map<String, Object>> testSteps = (java.util.List<Map<String, Object>>) iterations.get(0).get("testSteps");
|
||||||
|
if (testSteps != null) {
|
||||||
|
for (int i = 0; i < testSteps.size(); i++) {
|
||||||
|
Map<String, Object> stepData = testSteps.get(i);
|
||||||
|
|
||||||
|
StepResult sr = new StepResult();
|
||||||
|
sr.setExecutionId(executionId);
|
||||||
|
sr.setStepIndex(i + 1);
|
||||||
|
sr.setActionName((String) stepData.getOrDefault("actionName", "unknown"));
|
||||||
|
sr.setStepType((String) stepData.getOrDefault("stepType", "ACTION"));
|
||||||
|
|
||||||
|
Object execStatus = stepData.get("executed_status");
|
||||||
|
sr.setExecutedStatus(execStatus != null ? (Integer) execStatus : 0);
|
||||||
|
|
||||||
|
Object resStatus = stepData.get("result_status");
|
||||||
|
sr.setResultStatus(resStatus != null ? (Integer) resStatus : 0);
|
||||||
|
|
||||||
|
sr.setErrorJson((String) stepData.getOrDefault("errorLog", ""));
|
||||||
|
sr.setActualValue((String) stepData.get("actualValue"));
|
||||||
|
sr = stepResultRepository.save(sr);
|
||||||
|
|
||||||
|
String base64 = (String) stepData.get("screenshotBase64");
|
||||||
|
if (base64 != null && !base64.isEmpty()) {
|
||||||
|
byte[] imageBytes = java.util.Base64.getDecoder().decode(base64);
|
||||||
|
String fileName = "exec_" + executionId + "_step_" + sr.getId() + "_" + System.currentTimeMillis() + ".png";
|
||||||
|
|
||||||
|
String s3Url = s3Service.uploadImage(fileName, imageBytes);
|
||||||
|
|
||||||
|
Screenshot sc = new Screenshot();
|
||||||
|
sc.setExecutionId(executionId);
|
||||||
|
sc.setStepResultId(sr.getId());
|
||||||
|
sc.setFileName(fileName);
|
||||||
|
sc.setContentType("image/png");
|
||||||
|
sc.setStoragePath(s3Url);
|
||||||
|
screenshotRepository.save(sc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Bug fix: was silently swallowing all exceptions and returning 200 OK even when data was lost.
|
||||||
|
// Now logs the error properly and returns 500 so the agent knows it must retry.
|
||||||
|
org.slf4j.LoggerFactory.getLogger(AgentService.class)
|
||||||
|
.error("Critical error processing results for executionId={}: {}", executionId, ex.getMessage(), ex);
|
||||||
|
return ResponseEntity.status(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> stopExecution(Long executionId) {
|
||||||
|
executionRepository.findById(executionId).ifPresent(exec -> {
|
||||||
|
if ("running".equals(exec.getStatus()) || "queued".equals(exec.getStatus())) {
|
||||||
|
exec.setStatus("aborted");
|
||||||
|
exec.setFinishedAt(java.time.LocalDateTime.now());
|
||||||
|
executionRepository.save(exec);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> rerunExecution(Long executionId) {
|
||||||
|
executionRepository.findById(executionId).ifPresent(exec -> {
|
||||||
|
try {
|
||||||
|
com.fasterxml.jackson.databind.JsonNode env = objectMapper.readTree(exec.getEnvironmentJson());
|
||||||
|
String suiteName = env.path("referenceId").asText();
|
||||||
|
String browser = env.path("browserTypeName").asText("chrome");
|
||||||
|
|
||||||
|
TestSuite suite = testSuiteRepository.findAll().stream()
|
||||||
|
.filter(s -> s.getName().equals(suiteName))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
|
||||||
|
Scheduler scheduler = new Scheduler();
|
||||||
|
scheduler.setTestSuiteName(suiteName);
|
||||||
|
if (suite != null) {
|
||||||
|
scheduler.setTestSuiteId(suite.getId());
|
||||||
|
}
|
||||||
|
scheduler.setExecutionType("now");
|
||||||
|
scheduler.setBrowserType(browser);
|
||||||
|
scheduler.setStatus("active");
|
||||||
|
scheduler.setOrgId(exec.getOrgId());
|
||||||
|
schedulerRepository.save(scheduler);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> stepToMapAndReplace(TestStep step, Map<String, String> vars) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("id", step.getId());
|
||||||
|
map.put("stepOrder", step.getStepOrder());
|
||||||
|
map.put("actionName", step.getActionName());
|
||||||
|
map.put("stepType", step.getStepType());
|
||||||
|
map.put("locatorType", step.getLocatorType());
|
||||||
|
map.put("locatorValue", replaceVars(step.getLocatorValue(), vars));
|
||||||
|
map.put("testData", replaceVars(step.getTestData(), vars));
|
||||||
|
map.put("expectedValue", replaceVars(step.getExpectedValue(), vars));
|
||||||
|
map.put("description", step.getDescription());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String replaceVars(String text, Map<String, String> vars) {
|
||||||
|
if (text == null || vars == null || vars.isEmpty()) return text;
|
||||||
|
String result = text;
|
||||||
|
for (Map.Entry<String, String> entry : vars.entrySet()) {
|
||||||
|
if (entry.getValue() != null) {
|
||||||
|
result = result.replace("{{" + entry.getKey() + "}}", entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,393 @@
|
||||||
|
package com.autopilot.localagent_cloud.service;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Agent;
|
||||||
|
import com.autopilot.localagent_cloud.model.Execution;
|
||||||
|
import com.autopilot.localagent_cloud.model.Job;
|
||||||
|
import com.autopilot.localagent_cloud.model.Scheduler;
|
||||||
|
import com.autopilot.localagent_cloud.model.StepResult;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AgentRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.ExecutionRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.JobRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.SchedulerRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.StepResultRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestSuiteRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AnalyticsService {
|
||||||
|
|
||||||
|
private final ExecutionRepository executionRepository;
|
||||||
|
private final SchedulerRepository schedulerRepository;
|
||||||
|
private final StepResultRepository stepResultRepository;
|
||||||
|
private final AgentRepository agentRepository;
|
||||||
|
private final JobRepository jobRepository;
|
||||||
|
private final TestSuiteRepository testSuiteRepository;
|
||||||
|
|
||||||
|
public AnalyticsService(ExecutionRepository executionRepository,
|
||||||
|
SchedulerRepository schedulerRepository,
|
||||||
|
StepResultRepository stepResultRepository,
|
||||||
|
AgentRepository agentRepository,
|
||||||
|
JobRepository jobRepository,
|
||||||
|
TestSuiteRepository testSuiteRepository) {
|
||||||
|
this.executionRepository = executionRepository;
|
||||||
|
this.schedulerRepository = schedulerRepository;
|
||||||
|
this.stepResultRepository = stepResultRepository;
|
||||||
|
this.agentRepository = agentRepository;
|
||||||
|
this.jobRepository = jobRepository;
|
||||||
|
this.testSuiteRepository = testSuiteRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the "Top Flaky Test Suites" for an org.
|
||||||
|
*
|
||||||
|
* Flakiness Score is defined as:
|
||||||
|
* (number of runs that were NOT all-pass AND not-all-fail) / total runs * 100
|
||||||
|
* A suite must have at least 3 runs to be considered.
|
||||||
|
*
|
||||||
|
* Returns top 5 flaky suites sorted by flakiness score descending.
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getFlakySuites(Long orgId, int limit) {
|
||||||
|
// 1. Get all executions for the org
|
||||||
|
List<Execution> allExecs = executionRepository.findAllByOrderByIdDesc()
|
||||||
|
.stream()
|
||||||
|
.filter(e -> orgId == null || orgId.equals(e.getOrgId()))
|
||||||
|
.filter(e -> e.getSchedulerId() != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 2. Build a map of schedulerId -> Scheduler (for suite name)
|
||||||
|
Map<Long, String> schedulerNames = new HashMap<>();
|
||||||
|
Map<Long, Long> schedulerSuiteIds = new HashMap<>();
|
||||||
|
schedulerRepository.findAll().forEach(s -> {
|
||||||
|
schedulerNames.put(s.getId(), s.getTestSuiteName());
|
||||||
|
schedulerSuiteIds.put(s.getId(), s.getTestSuiteId());
|
||||||
|
});
|
||||||
|
|
||||||
|
Set<Long> existingSuiteIds = testSuiteRepository.findAll().stream().map(s -> s.getId()).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 3. Group executions by testSuiteName via schedulerId
|
||||||
|
Map<String, List<Execution>> bySuite = allExecs.stream()
|
||||||
|
.filter(e -> {
|
||||||
|
Long suiteId = schedulerSuiteIds.get(e.getSchedulerId());
|
||||||
|
return suiteId != null && existingSuiteIds.contains(suiteId);
|
||||||
|
})
|
||||||
|
.collect(Collectors.groupingBy(e -> {
|
||||||
|
String name = schedulerNames.get(e.getSchedulerId());
|
||||||
|
return name != null ? name : "Suite #" + e.getSchedulerId();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 4. For each suite, calculate flakiness
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<Execution>> entry : bySuite.entrySet()) {
|
||||||
|
String suiteName = entry.getKey();
|
||||||
|
List<Execution> runs = entry.getValue();
|
||||||
|
|
||||||
|
// Need at least 2 runs to determine flakiness
|
||||||
|
if (runs.size() < 2) continue;
|
||||||
|
|
||||||
|
int totalRuns = runs.size();
|
||||||
|
int passedRuns = 0;
|
||||||
|
int failedRuns = 0;
|
||||||
|
int partialRuns = 0; // Mixed pass/fail steps = flaky
|
||||||
|
|
||||||
|
for (Execution exec : runs) {
|
||||||
|
String status = exec.getStatus();
|
||||||
|
if (status == null) continue;
|
||||||
|
if (status.equalsIgnoreCase("success") || status.equalsIgnoreCase("completed")) {
|
||||||
|
passedRuns++;
|
||||||
|
} else if (status.equalsIgnoreCase("failed")) {
|
||||||
|
failedRuns++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flakiness: suite that doesn't consistently pass or fail
|
||||||
|
// High flakiness = many alternating results
|
||||||
|
int notConsistent = totalRuns - Math.max(passedRuns, failedRuns);
|
||||||
|
// Score from 0-100
|
||||||
|
double flakinessScore = totalRuns > 0 ? ((double) Math.min(passedRuns, failedRuns) / totalRuns) * 100.0 : 0;
|
||||||
|
|
||||||
|
if (flakinessScore == 0 && (passedRuns == 0 || failedRuns == 0)) {
|
||||||
|
// Consistent suite - not flaky
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> suiteData = new LinkedHashMap<>();
|
||||||
|
suiteData.put("suiteName", suiteName);
|
||||||
|
suiteData.put("totalRuns", totalRuns);
|
||||||
|
suiteData.put("passedRuns", passedRuns);
|
||||||
|
suiteData.put("failedRuns", failedRuns);
|
||||||
|
suiteData.put("flakinessScore", Math.round(flakinessScore * 10.0) / 10.0);
|
||||||
|
suiteData.put("trend", failedRuns > passedRuns ? "deteriorating" : "improving");
|
||||||
|
result.add(suiteData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by flakiness score descending, return top N
|
||||||
|
result.sort((a, b) -> Double.compare(
|
||||||
|
(double) b.get("flakinessScore"),
|
||||||
|
(double) a.get("flakinessScore")
|
||||||
|
));
|
||||||
|
|
||||||
|
return result.stream().limit(limit).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns per-suite performance stats for the leaderboard:
|
||||||
|
* suiteName, totalRuns, passedRuns, failedRuns, successRate, avgDurationSecs, lastRunAt
|
||||||
|
* Sorted by successRate descending (best performers first).
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getSuitePerformance(Long orgId, int limit) {
|
||||||
|
// Get all terminal executions (not QUEUED/RUNNING) linked to a scheduler
|
||||||
|
List<Execution> allExecs = executionRepository.findAllByOrderByIdDesc()
|
||||||
|
.stream()
|
||||||
|
.filter(e -> orgId == null || orgId.equals(e.getOrgId()))
|
||||||
|
.filter(e -> e.getSchedulerId() != null)
|
||||||
|
.filter(e -> {
|
||||||
|
String s = e.getStatus();
|
||||||
|
return s != null && !s.equalsIgnoreCase("queued") && !s.equalsIgnoreCase("running");
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Build scheduler name lookup
|
||||||
|
Map<Long, String> schedulerNames = new HashMap<>();
|
||||||
|
Map<Long, Long> schedulerSuiteIds = new HashMap<>();
|
||||||
|
schedulerRepository.findAll().forEach(s -> {
|
||||||
|
schedulerNames.put(s.getId(), s.getTestSuiteName());
|
||||||
|
schedulerSuiteIds.put(s.getId(), s.getTestSuiteId());
|
||||||
|
});
|
||||||
|
|
||||||
|
Set<Long> existingSuiteIds = testSuiteRepository.findAll().stream().map(s -> s.getId()).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// Group by suite name
|
||||||
|
Map<String, List<Execution>> bySuite = allExecs.stream()
|
||||||
|
.filter(e -> {
|
||||||
|
Long suiteId = schedulerSuiteIds.get(e.getSchedulerId());
|
||||||
|
return suiteId != null && existingSuiteIds.contains(suiteId);
|
||||||
|
})
|
||||||
|
.collect(Collectors.groupingBy(e -> {
|
||||||
|
String name = schedulerNames.get(e.getSchedulerId());
|
||||||
|
return name != null ? name : "Suite #" + e.getSchedulerId();
|
||||||
|
}));
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<Execution>> entry : bySuite.entrySet()) {
|
||||||
|
String suiteName = entry.getKey();
|
||||||
|
List<Execution> runs = entry.getValue();
|
||||||
|
int totalRuns = runs.size();
|
||||||
|
|
||||||
|
long passedRuns = runs.stream().filter(e ->
|
||||||
|
e.getStatus().equalsIgnoreCase("success") || e.getStatus().equalsIgnoreCase("completed")
|
||||||
|
).count();
|
||||||
|
long failedRuns = runs.stream().filter(e -> e.getStatus().equalsIgnoreCase("failed")).count();
|
||||||
|
|
||||||
|
double successRate = totalRuns > 0 ? (double) passedRuns / totalRuns * 100.0 : 0.0;
|
||||||
|
|
||||||
|
// Average duration in seconds (only for executions that have both timestamps)
|
||||||
|
OptionalDouble avgDuration = runs.stream()
|
||||||
|
.filter(e -> e.getCreatedAt() != null && e.getFinishedAt() != null)
|
||||||
|
.mapToLong(e -> java.time.Duration.between(e.getCreatedAt(), e.getFinishedAt()).getSeconds())
|
||||||
|
.average();
|
||||||
|
|
||||||
|
// Most recent run
|
||||||
|
Optional<LocalDateTime> lastRun = runs.stream()
|
||||||
|
.map(Execution::getCreatedAt)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.max(Comparator.naturalOrder());
|
||||||
|
|
||||||
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
data.put("suiteName", suiteName);
|
||||||
|
data.put("totalRuns", totalRuns);
|
||||||
|
data.put("passedRuns", passedRuns);
|
||||||
|
data.put("failedRuns", failedRuns);
|
||||||
|
data.put("successRate", Math.round(successRate * 10.0) / 10.0);
|
||||||
|
data.put("avgDurationSecs", avgDuration.isPresent() ? Math.round(avgDuration.getAsDouble()) : null);
|
||||||
|
data.put("lastRunAt", lastRun.orElse(null));
|
||||||
|
result.add(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by successRate descending, then by totalRuns descending as tiebreaker
|
||||||
|
result.sort((a, b) -> {
|
||||||
|
int cmp = Double.compare((double) b.get("successRate"), (double) a.get("successRate"));
|
||||||
|
if (cmp != 0) return cmp;
|
||||||
|
return Integer.compare((int) b.get("totalRuns"), (int) a.get("totalRuns"));
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.stream().limit(limit).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates live fleet health: agent statuses + queue depth.
|
||||||
|
* An agent is considered ONLINE if it sent a heartbeat within the last 2 minutes.
|
||||||
|
* An agent is RUNNING if it has an ASSIGNED job with a valid lease.
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getFleetHealth(Long orgId) {
|
||||||
|
// Get all agents for this org
|
||||||
|
List<Agent> agents = orgId != null
|
||||||
|
? agentRepository.findByOrgId(orgId)
|
||||||
|
: agentRepository.findAll();
|
||||||
|
|
||||||
|
LocalDateTime onlineThreshold = LocalDateTime.now().minusMinutes(2);
|
||||||
|
|
||||||
|
// Get all currently assigned jobs (to detect which agents are running)
|
||||||
|
List<Job> assignedJobs = jobRepository.findAll().stream()
|
||||||
|
.filter(j -> "ASSIGNED".equalsIgnoreCase(j.getStatus()))
|
||||||
|
.filter(j -> j.getLeaseExpiresAt() != null && j.getLeaseExpiresAt().isAfter(LocalDateTime.now()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Set<String> busyAgentIds = assignedJobs.stream()
|
||||||
|
.map(Job::getAgentId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// Count queue depth (QUEUED jobs with no agent yet)
|
||||||
|
long jobQueueDepth = jobRepository.findAll().stream()
|
||||||
|
.filter(j -> "QUEUED".equalsIgnoreCase(j.getStatus()))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// Also count Schedulers that are waiting to be unwrapped by an agent
|
||||||
|
long activeSchedulers = schedulerRepository.findAll().stream()
|
||||||
|
.filter(s -> "now".equals(s.getExecutionType()) && "active".equals(s.getStatus()))
|
||||||
|
.filter(s -> orgId == null || orgId.equals(s.getOrgId()))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
long queueDepth = jobQueueDepth + activeSchedulers;
|
||||||
|
|
||||||
|
// Build per-agent summaries
|
||||||
|
List<Map<String, Object>> agentDetails = new ArrayList<>();
|
||||||
|
int onlineCount = 0;
|
||||||
|
int runningCount = 0;
|
||||||
|
int offlineCount = 0;
|
||||||
|
|
||||||
|
for (Agent agent : agents) {
|
||||||
|
boolean isOnline = agent.getLastSeenAt() != null && agent.getLastSeenAt().isAfter(onlineThreshold);
|
||||||
|
boolean isRunning = isOnline && busyAgentIds.contains(agent.getId());
|
||||||
|
|
||||||
|
String agentStatus;
|
||||||
|
if (isRunning) { agentStatus = "running"; runningCount++; }
|
||||||
|
else if (isOnline) { agentStatus = "idle"; onlineCount++; }
|
||||||
|
else { agentStatus = "offline"; offlineCount++; }
|
||||||
|
|
||||||
|
Map<String, Object> detail = new LinkedHashMap<>();
|
||||||
|
detail.put("id", agent.getId());
|
||||||
|
detail.put("name", agent.getName());
|
||||||
|
detail.put("os", agent.getOs());
|
||||||
|
detail.put("agentVersion", agent.getAgentVersion());
|
||||||
|
detail.put("status", agentStatus);
|
||||||
|
detail.put("lastSeenAt", agent.getLastSeenAt());
|
||||||
|
agentDetails.add(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: running first, then idle, then offline
|
||||||
|
agentDetails.sort((a, b) -> {
|
||||||
|
String[] order = {"running", "idle", "offline"};
|
||||||
|
int ai = Arrays.asList(order).indexOf(a.get("status").toString());
|
||||||
|
int bi = Arrays.asList(order).indexOf(b.get("status").toString());
|
||||||
|
return Integer.compare(ai, bi);
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("totalAgents", agents.size());
|
||||||
|
result.put("onlineCount", onlineCount);
|
||||||
|
result.put("runningCount", runningCount);
|
||||||
|
result.put("offlineCount", offlineCount);
|
||||||
|
result.put("queueDepth", queueDepth);
|
||||||
|
result.put("agents", agentDetails);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the "Time Saved" ROI metric for the org.
|
||||||
|
*
|
||||||
|
* Assumptions (industry-standard estimates):
|
||||||
|
* - Manual test execution time: 15 minutes per test run
|
||||||
|
* - QA engineer hourly rate: $50 USD
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* totalRuns, totalAutomatedSecs, manualEstimateSecs, timeSavedSecs,
|
||||||
|
* hoursSaved, dollarsSaved, avgDurationSecs,
|
||||||
|
* todayRuns, weekRuns,
|
||||||
|
* manualMinsPerRun (assumption), hourlyRate (assumption)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getTimeSavedRoi(Long orgId) {
|
||||||
|
final int MANUAL_MINS_PER_RUN = 15; // assumed manual time per test run
|
||||||
|
final double HOURLY_RATE = 50.0; // assumed QA hourly rate in USD
|
||||||
|
|
||||||
|
List<Execution> completedExecs = executionRepository.findAllByOrderByIdDesc()
|
||||||
|
.stream()
|
||||||
|
.filter(e -> orgId == null || orgId.equals(e.getOrgId()))
|
||||||
|
.filter(e -> {
|
||||||
|
String s = e.getStatus();
|
||||||
|
return s != null && (s.equalsIgnoreCase("success")
|
||||||
|
|| s.equalsIgnoreCase("completed")
|
||||||
|
|| s.equalsIgnoreCase("failed"));
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
int totalRuns = completedExecs.size();
|
||||||
|
|
||||||
|
// Sum actual automated execution durations
|
||||||
|
long totalAutomatedSecs = completedExecs.stream()
|
||||||
|
.filter(e -> e.getCreatedAt() != null && e.getFinishedAt() != null)
|
||||||
|
.mapToLong(e -> Math.max(0, java.time.Duration.between(e.getCreatedAt(), e.getFinishedAt()).getSeconds()))
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
OptionalDouble avgDur = completedExecs.stream()
|
||||||
|
.filter(e -> e.getCreatedAt() != null && e.getFinishedAt() != null)
|
||||||
|
.mapToLong(e -> Math.max(0, java.time.Duration.between(e.getCreatedAt(), e.getFinishedAt()).getSeconds()))
|
||||||
|
.average();
|
||||||
|
|
||||||
|
// Manual estimate: each run would take MANUAL_MINS_PER_RUN minutes
|
||||||
|
long manualEstimateSecs = (long) totalRuns * MANUAL_MINS_PER_RUN * 60L;
|
||||||
|
|
||||||
|
// Time saved = manual - automated (floored at 0)
|
||||||
|
long timeSavedSecs = Math.max(0, manualEstimateSecs - totalAutomatedSecs);
|
||||||
|
|
||||||
|
double hoursSaved = timeSavedSecs / 3600.0;
|
||||||
|
double dollarsSaved = hoursSaved * HOURLY_RATE;
|
||||||
|
|
||||||
|
// Today and this week counts
|
||||||
|
LocalDateTime startOfToday = LocalDateTime.now().toLocalDate().atStartOfDay();
|
||||||
|
LocalDateTime startOfWeek = startOfToday.minusDays(6);
|
||||||
|
|
||||||
|
long todayRuns = completedExecs.stream()
|
||||||
|
.filter(e -> e.getCreatedAt() != null && e.getCreatedAt().isAfter(startOfToday))
|
||||||
|
.count();
|
||||||
|
long weekRuns = completedExecs.stream()
|
||||||
|
.filter(e -> e.getCreatedAt() != null && e.getCreatedAt().isAfter(startOfWeek))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// Daily breakdown for last 14 days (for sparkline)
|
||||||
|
List<Map<String, Object>> dailyBreakdown = new ArrayList<>();
|
||||||
|
for (int i = 13; i >= 0; i--) {
|
||||||
|
LocalDateTime dayStart = startOfToday.minusDays(i);
|
||||||
|
LocalDateTime dayEnd = dayStart.plusDays(1);
|
||||||
|
long dayRuns = completedExecs.stream()
|
||||||
|
.filter(e -> e.getCreatedAt() != null
|
||||||
|
&& !e.getCreatedAt().isBefore(dayStart)
|
||||||
|
&& e.getCreatedAt().isBefore(dayEnd))
|
||||||
|
.count();
|
||||||
|
long daySavedSecs = dayRuns * MANUAL_MINS_PER_RUN * 60L;
|
||||||
|
Map<String, Object> day = new LinkedHashMap<>();
|
||||||
|
day.put("date", dayStart.toLocalDate().toString());
|
||||||
|
day.put("runs", dayRuns);
|
||||||
|
day.put("savedMins", daySavedSecs / 60);
|
||||||
|
dailyBreakdown.add(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("totalRuns", totalRuns);
|
||||||
|
result.put("totalAutomatedSecs", totalAutomatedSecs);
|
||||||
|
result.put("manualEstimateSecs", manualEstimateSecs);
|
||||||
|
result.put("timeSavedSecs", timeSavedSecs);
|
||||||
|
result.put("hoursSaved", Math.round(hoursSaved * 10.0) / 10.0);
|
||||||
|
result.put("dollarsSaved", Math.round(dollarsSaved * 100.0) / 100.0);
|
||||||
|
result.put("avgDurationSecs", avgDur.isPresent() ? Math.round(avgDur.getAsDouble()) : 0);
|
||||||
|
result.put("todayRuns", todayRuns);
|
||||||
|
result.put("weekRuns", weekRuns);
|
||||||
|
result.put("manualMinsPerRun", MANUAL_MINS_PER_RUN);
|
||||||
|
result.put("hourlyRate", HOURLY_RATE);
|
||||||
|
result.put("dailyBreakdown", dailyBreakdown);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
package com.autopilot.localagent_cloud.service;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Scheduler;
|
||||||
|
import com.autopilot.localagent_cloud.repository.SchedulerRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.scheduling.support.CronExpression;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CronExecutionEngine {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CronExecutionEngine.class);
|
||||||
|
private final SchedulerRepository schedulerRepository;
|
||||||
|
|
||||||
|
public CronExecutionEngine(SchedulerRepository schedulerRepository) {
|
||||||
|
this.schedulerRepository = schedulerRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs at the 0th second of every minute
|
||||||
|
@Scheduled(cron = "0 * * * * *")
|
||||||
|
public void evaluateCronSchedules() {
|
||||||
|
LocalDateTime serverNow = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
|
||||||
|
logger.debug("Evaluating cron schedules at: {}", serverNow);
|
||||||
|
|
||||||
|
List<Scheduler> activeSchedules = schedulerRepository.findAll().stream()
|
||||||
|
.filter(s -> "scheduled".equals(s.getExecutionType()) && "active".equals(s.getStatus()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (Scheduler schedule : activeSchedules) {
|
||||||
|
try {
|
||||||
|
LocalDateTime now = serverNow;
|
||||||
|
if (schedule.getTimezone() != null && !schedule.getTimezone().isBlank()) {
|
||||||
|
try {
|
||||||
|
now = LocalDateTime.now(java.time.ZoneId.of(schedule.getTimezone())).truncatedTo(ChronoUnit.MINUTES);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Invalid timezone '{}' for schedule ID {}", schedule.getTimezone(), schedule.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRunOutlookSchedule(schedule, now)) {
|
||||||
|
logger.info("Outlook schedule triggered for suite: {}", schedule.getTestSuiteName());
|
||||||
|
queueExecution(schedule);
|
||||||
|
// For 'once' recurrence, deactivate after triggering
|
||||||
|
if ("once".equals(schedule.getRecurrenceType())) {
|
||||||
|
schedule.setStatus("inactive");
|
||||||
|
schedulerRepository.save(schedule);
|
||||||
|
}
|
||||||
|
} else if (hasLegacyCron(schedule)) {
|
||||||
|
evaluateLegacyCron(schedule, now);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to evaluate scheduler ID {}: {}", schedule.getId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates Outlook-style fields (scheduledDate, scheduledTime, recurrenceType, etc.)
|
||||||
|
* Returns true if the schedule should fire at the given time.
|
||||||
|
*/
|
||||||
|
private boolean shouldRunOutlookSchedule(Scheduler s, LocalDateTime now) {
|
||||||
|
// Only use Outlook mode if recurrenceType is set
|
||||||
|
if (s.getRecurrenceType() == null || s.getRecurrenceType().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalTime scheduledTime = s.getScheduledTime();
|
||||||
|
if (scheduledTime == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check time match (truncated to minutes)
|
||||||
|
LocalTime nowTime = now.toLocalTime().truncatedTo(ChronoUnit.MINUTES);
|
||||||
|
LocalTime targetTime = scheduledTime.truncatedTo(ChronoUnit.MINUTES);
|
||||||
|
if (!nowTime.equals(targetTime)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check recurrence end date
|
||||||
|
if (s.getRecurrenceEndDate() != null && now.toLocalDate().isAfter(s.getRecurrenceEndDate())) {
|
||||||
|
// Past end date — deactivate
|
||||||
|
s.setStatus("inactive");
|
||||||
|
schedulerRepository.save(s);
|
||||||
|
logger.info("Deactivating scheduler ID {} — past recurrence end date", s.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (s.getRecurrenceType()) {
|
||||||
|
case "once": {
|
||||||
|
// Must match exact date and time
|
||||||
|
LocalDate scheduledDate = s.getScheduledDate();
|
||||||
|
if (scheduledDate == null) return false;
|
||||||
|
return now.toLocalDate().equals(scheduledDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "daily": {
|
||||||
|
// Check if start date has been reached
|
||||||
|
LocalDate scheduledDate = s.getScheduledDate();
|
||||||
|
if (scheduledDate != null && now.toLocalDate().isBefore(scheduledDate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true; // Time already matched above
|
||||||
|
}
|
||||||
|
|
||||||
|
case "weekly": {
|
||||||
|
// Check if start date has been reached
|
||||||
|
LocalDate scheduledDate = s.getScheduledDate();
|
||||||
|
if (scheduledDate != null && now.toLocalDate().isBefore(scheduledDate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Match day of week
|
||||||
|
String recurrenceDays = s.getRecurrenceDays();
|
||||||
|
if (recurrenceDays == null || recurrenceDays.isBlank()) return false;
|
||||||
|
String todayAbbr = now.getDayOfWeek().name().substring(0, 3); // MON, TUE, etc.
|
||||||
|
for (String day : recurrenceDays.split(",")) {
|
||||||
|
if (day.trim().equalsIgnoreCase(todayAbbr)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "monthly": {
|
||||||
|
// Check if start date has been reached
|
||||||
|
LocalDate scheduledDate = s.getScheduledDate();
|
||||||
|
if (scheduledDate == null) return false;
|
||||||
|
if (now.toLocalDate().isBefore(scheduledDate)) return false;
|
||||||
|
// Match day of month
|
||||||
|
return now.getDayOfMonth() == scheduledDate.getDayOfMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if this scheduler has a legacy cron expression and no Outlook fields */
|
||||||
|
private boolean hasLegacyCron(Scheduler s) {
|
||||||
|
return s.getCronExpression() != null
|
||||||
|
&& !s.getCronExpression().isBlank()
|
||||||
|
&& (s.getRecurrenceType() == null || s.getRecurrenceType().isBlank());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Evaluates legacy raw cron expression (original behavior) */
|
||||||
|
private void evaluateLegacyCron(Scheduler schedule, LocalDateTime now) {
|
||||||
|
try {
|
||||||
|
CronExpression cron = CronExpression.parse(schedule.getCronExpression());
|
||||||
|
LocalDateTime nextExecution = cron.next(now.minusSeconds(1));
|
||||||
|
if (nextExecution != null && nextExecution.truncatedTo(ChronoUnit.MINUTES).equals(now)) {
|
||||||
|
logger.info("Cron trigger matched for suite: {}", schedule.getTestSuiteName());
|
||||||
|
queueExecution(schedule);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to parse or evaluate cron expression: {} for scheduler ID: {}",
|
||||||
|
schedule.getCronExpression(), schedule.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a Spring cron expression string from Outlook-style fields.
|
||||||
|
* Useful for display purposes or for storing a computed cron on the entity.
|
||||||
|
*/
|
||||||
|
public static String buildCronFromOutlookFields(Scheduler s) {
|
||||||
|
if (s.getScheduledTime() == null || s.getRecurrenceType() == null) return null;
|
||||||
|
|
||||||
|
int hour = s.getScheduledTime().getHour();
|
||||||
|
int minute = s.getScheduledTime().getMinute();
|
||||||
|
|
||||||
|
return switch (s.getRecurrenceType()) {
|
||||||
|
case "once", "daily" -> String.format("0 %d %d * * *", minute, hour);
|
||||||
|
case "weekly" -> {
|
||||||
|
String days = (s.getRecurrenceDays() != null && !s.getRecurrenceDays().isBlank())
|
||||||
|
? s.getRecurrenceDays()
|
||||||
|
: "*";
|
||||||
|
yield String.format("0 %d %d * * %s", minute, hour, days);
|
||||||
|
}
|
||||||
|
case "monthly" -> {
|
||||||
|
int dayOfMonth = s.getScheduledDate() != null ? s.getScheduledDate().getDayOfMonth() : 1;
|
||||||
|
yield String.format("0 %d %d %d * *", minute, hour, dayOfMonth);
|
||||||
|
}
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queueExecution(Scheduler original) {
|
||||||
|
Scheduler job = new Scheduler();
|
||||||
|
job.setTestSuiteId(original.getTestSuiteId());
|
||||||
|
job.setTestSuiteName(original.getTestSuiteName());
|
||||||
|
job.setBrowserType(original.getBrowserType());
|
||||||
|
job.setBrowserVersion(original.getBrowserVersion());
|
||||||
|
job.setTargetGroupId(original.getTargetGroupId());
|
||||||
|
job.setExecutionType("now");
|
||||||
|
job.setStatus("active");
|
||||||
|
job.setOrgId(original.getOrgId());
|
||||||
|
job.setEnvironmentId(original.getEnvironmentId());
|
||||||
|
schedulerRepository.save(job);
|
||||||
|
logger.info("Queued test suite '{}' for execution via local agent polling", job.getTestSuiteName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.autopilot.localagent_cloud.service;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Execution;
|
||||||
|
import com.autopilot.localagent_cloud.model.Screenshot;
|
||||||
|
import com.autopilot.localagent_cloud.model.StepResult;
|
||||||
|
import com.autopilot.localagent_cloud.repository.ExecutionRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.ScreenshotRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.StepResultRepository;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ExecutionService {
|
||||||
|
|
||||||
|
private final ExecutionRepository executionRepository;
|
||||||
|
private final StepResultRepository stepResultRepository;
|
||||||
|
private final ScreenshotRepository screenshotRepository;
|
||||||
|
|
||||||
|
public ExecutionService(ExecutionRepository executionRepository,
|
||||||
|
StepResultRepository stepResultRepository,
|
||||||
|
ScreenshotRepository screenshotRepository) {
|
||||||
|
this.executionRepository = executionRepository;
|
||||||
|
this.stepResultRepository = stepResultRepository;
|
||||||
|
this.screenshotRepository = screenshotRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<List<Execution>> getAll(Long orgId) {
|
||||||
|
List<Execution> list = orgId != null
|
||||||
|
? executionRepository.findAll().stream().filter(e -> orgId.equals(e.getOrgId())).toList()
|
||||||
|
: executionRepository.findAll();
|
||||||
|
return ResponseEntity.ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Map<String, Object>> getById(Long id) {
|
||||||
|
return executionRepository.findById(id).map(exec -> {
|
||||||
|
List<StepResult> steps = stepResultRepository.findByExecutionId(id);
|
||||||
|
List<Screenshot> screenshots = screenshotRepository.findByExecutionId(id);
|
||||||
|
|
||||||
|
Map<String, Object> detail = new HashMap<>();
|
||||||
|
detail.put("execution", exec);
|
||||||
|
detail.put("steps", steps);
|
||||||
|
detail.put("screenshots", screenshots);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(detail);
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Map<String, Object>> getBySchedulerId(Long schedulerId) {
|
||||||
|
return executionRepository.findFirstBySchedulerIdOrderByIdDesc(schedulerId).map(exec -> {
|
||||||
|
List<StepResult> steps = stepResultRepository.findByExecutionId(exec.getId());
|
||||||
|
List<Screenshot> screenshots = screenshotRepository.findByExecutionId(exec.getId());
|
||||||
|
|
||||||
|
Map<String, Object> detail = new HashMap<>();
|
||||||
|
detail.put("execution", exec);
|
||||||
|
detail.put("steps", steps);
|
||||||
|
detail.put("screenshots", screenshots);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(detail);
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
public ResponseEntity<Void> deleteById(Long id) {
|
||||||
|
if (!executionRepository.existsById(id)) return ResponseEntity.notFound().build();
|
||||||
|
stepResultRepository.deleteAll(stepResultRepository.findByExecutionId(id));
|
||||||
|
screenshotRepository.deleteAll(screenshotRepository.findByExecutionId(id));
|
||||||
|
executionRepository.deleteById(id);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
package com.autopilot.localagent_cloud.service;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Group;
|
||||||
|
import com.autopilot.localagent_cloud.model.AgentGroupMapping;
|
||||||
|
import com.autopilot.localagent_cloud.repository.GroupRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AgentGroupMappingRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.AgentRepository;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class GroupService {
|
||||||
|
|
||||||
|
private final GroupRepository groupRepository;
|
||||||
|
private final AgentGroupMappingRepository agentGroupMappingRepository;
|
||||||
|
private final AgentRepository agentRepository;
|
||||||
|
|
||||||
|
public GroupService(GroupRepository groupRepository,
|
||||||
|
AgentGroupMappingRepository agentGroupMappingRepository,
|
||||||
|
AgentRepository agentRepository) {
|
||||||
|
this.groupRepository = groupRepository;
|
||||||
|
this.agentGroupMappingRepository = agentGroupMappingRepository;
|
||||||
|
this.agentRepository = agentRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<List<Group>> getAll(Long orgId) {
|
||||||
|
List<Group> list = orgId != null
|
||||||
|
? groupRepository.findAll().stream().filter(g -> orgId.equals(g.getOrgId())).toList()
|
||||||
|
: groupRepository.findAll();
|
||||||
|
return ResponseEntity.ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Group> create(Group group, Long orgId) {
|
||||||
|
if (group.getName() == null || group.getName().isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
group.setOrgId(orgId);
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(groupRepository.save(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Group> update(Long id, Group updated) {
|
||||||
|
return groupRepository.findById(id).map(existing -> {
|
||||||
|
if (updated.getName() != null) existing.setName(updated.getName());
|
||||||
|
if (updated.getDescription() != null) existing.setDescription(updated.getDescription());
|
||||||
|
return ResponseEntity.ok(groupRepository.save(existing));
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Void> delete(Long id) {
|
||||||
|
if (!groupRepository.existsById(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
groupRepository.deleteById(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getAgentsInGroup(Long groupId) {
|
||||||
|
List<AgentGroupMapping> mappings = agentGroupMappingRepository.findByGroupId(groupId);
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (AgentGroupMapping m : mappings) {
|
||||||
|
agentRepository.findById(m.getAgentId()).ifPresent(agent -> {
|
||||||
|
Map<String, Object> entry = new HashMap<>();
|
||||||
|
entry.put("mappingId", m.getId());
|
||||||
|
entry.put("agent", agent);
|
||||||
|
result.add(entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<AgentGroupMapping> addAgent(Long groupId, String agentId) {
|
||||||
|
if (agentId == null || agentId.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
if (!groupRepository.existsById(groupId)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
AgentGroupMapping mapping = new AgentGroupMapping();
|
||||||
|
mapping.setAgentId(agentId);
|
||||||
|
mapping.setGroupId(groupId);
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(agentGroupMappingRepository.save(mapping));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> removeAgent(Long groupId, String agentId) {
|
||||||
|
agentGroupMappingRepository.deleteByAgentIdAndGroupId(agentId, groupId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.autopilot.localagent_cloud.service;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Execution;
|
||||||
|
import com.autopilot.localagent_cloud.repository.ExecutionRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OrphanedExecutionSweeper {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OrphanedExecutionSweeper.class);
|
||||||
|
|
||||||
|
private final ExecutionRepository executionRepository;
|
||||||
|
|
||||||
|
public OrphanedExecutionSweeper(ExecutionRepository executionRepository) {
|
||||||
|
this.executionRepository = executionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run every 5 minutes (300000 ms)
|
||||||
|
@Scheduled(fixedRate = 300000)
|
||||||
|
@Transactional
|
||||||
|
public void sweepOrphanedExecutions() {
|
||||||
|
// If an execution is stuck in "running" for more than 15 minutes, it is orphaned
|
||||||
|
LocalDateTime cutoff = LocalDateTime.now().minusMinutes(15);
|
||||||
|
List<Execution> orphanedExecutions = executionRepository.findByStatusAndCreatedAtBefore("running", cutoff);
|
||||||
|
|
||||||
|
if (!orphanedExecutions.isEmpty()) {
|
||||||
|
logger.warn("Found {} orphaned executions stuck in 'running' state. Marking them as failed.", orphanedExecutions.size());
|
||||||
|
for (Execution execution : orphanedExecutions) {
|
||||||
|
execution.setStatus("failed");
|
||||||
|
execution.setFinishedAt(LocalDateTime.now());
|
||||||
|
executionRepository.save(execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.autopilot.localagent_cloud.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||||
|
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.core.sync.RequestBody;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class S3Service {
|
||||||
|
|
||||||
|
@Value("${aws.s3.bucket:autopilot}")
|
||||||
|
private String bucketName;
|
||||||
|
|
||||||
|
@Value("${aws.region:ap-south-1}")
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
@Value("${aws.access-key:}")
|
||||||
|
private String accessKey;
|
||||||
|
|
||||||
|
@Value("${aws.secret-key:}")
|
||||||
|
private String secretKey;
|
||||||
|
|
||||||
|
private S3Client s3Client;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
if (accessKey != null && !accessKey.isEmpty() && secretKey != null && !secretKey.isEmpty()) {
|
||||||
|
s3Client = S3Client.builder()
|
||||||
|
.region(Region.of(region))
|
||||||
|
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)))
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
// Fallback to default credentials provider (e.g. IAM role on EC2)
|
||||||
|
s3Client = S3Client.builder()
|
||||||
|
.region(Region.of(region))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String uploadImage(String fileName, byte[] imageBytes) {
|
||||||
|
String key = "screenshots/" + fileName;
|
||||||
|
|
||||||
|
PutObjectRequest putOb = PutObjectRequest.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.key(key)
|
||||||
|
.contentType("image/png")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
s3Client.putObject(putOb, RequestBody.fromBytes(imageBytes));
|
||||||
|
|
||||||
|
// Return public URL (assuming bucket allows public read)
|
||||||
|
return "https://" + bucketName + ".s3." + region + ".amazonaws.com/" + key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
package com.autopilot.localagent_cloud.service;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.Scheduler;
|
||||||
|
import com.autopilot.localagent_cloud.repository.SchedulerRepository;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SchedulerService {
|
||||||
|
|
||||||
|
private final SchedulerRepository schedulerRepository;
|
||||||
|
|
||||||
|
public SchedulerService(SchedulerRepository schedulerRepository) {
|
||||||
|
this.schedulerRepository = schedulerRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<List<Scheduler>> getAll(Long orgId) {
|
||||||
|
List<Scheduler> list = orgId != null
|
||||||
|
? schedulerRepository.findAll().stream().filter(s -> orgId.equals(s.getOrgId())).toList()
|
||||||
|
: schedulerRepository.findAll();
|
||||||
|
return ResponseEntity.ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Scheduler> create(Map<String, Object> body, Long orgId) {
|
||||||
|
Scheduler scheduler = buildSchedulerFromBody(new Scheduler(), body);
|
||||||
|
scheduler.setOrgId(orgId);
|
||||||
|
if (scheduler.getTestSuiteName() == null || scheduler.getTestSuiteName().isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
if (scheduler.getExecutionType() == null || scheduler.getExecutionType().isBlank()) {
|
||||||
|
scheduler.setExecutionType("now");
|
||||||
|
}
|
||||||
|
if (scheduler.getBrowserType() == null || scheduler.getBrowserType().isBlank()) {
|
||||||
|
scheduler.setBrowserType("chrome");
|
||||||
|
}
|
||||||
|
if (scheduler.getStatus() == null || scheduler.getStatus().isBlank()) {
|
||||||
|
scheduler.setStatus("active");
|
||||||
|
}
|
||||||
|
// Auto-compute cron expression from Outlook-style fields
|
||||||
|
if (scheduler.getRecurrenceType() != null && !scheduler.getRecurrenceType().isBlank()) {
|
||||||
|
String computed = CronExecutionEngine.buildCronFromOutlookFields(scheduler);
|
||||||
|
if (computed != null) scheduler.setCronExpression(computed);
|
||||||
|
}
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(schedulerRepository.save(scheduler));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Scheduler> update(Long id, Map<String, Object> body) {
|
||||||
|
return schedulerRepository.findById(id).map(existing -> {
|
||||||
|
buildSchedulerFromBody(existing, body);
|
||||||
|
// Auto-compute cron expression from Outlook-style fields
|
||||||
|
if (existing.getRecurrenceType() != null && !existing.getRecurrenceType().isBlank()) {
|
||||||
|
String computed = CronExecutionEngine.buildCronFromOutlookFields(existing);
|
||||||
|
if (computed != null) existing.setCronExpression(computed);
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(schedulerRepository.save(existing));
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Void> delete(Long id) {
|
||||||
|
if (!schedulerRepository.existsById(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
schedulerRepository.deleteById(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a JSON request body (from the Outlook-style scheduler form) onto a Scheduler entity.
|
||||||
|
* Handles both the legacy fields and the new Outlook-style fields.
|
||||||
|
*/
|
||||||
|
private Scheduler buildSchedulerFromBody(Scheduler s, Map<String, Object> body) {
|
||||||
|
if (body.containsKey("testSuiteName")) s.setTestSuiteName((String) body.get("testSuiteName"));
|
||||||
|
if (body.containsKey("executionType")) s.setExecutionType((String) body.get("executionType"));
|
||||||
|
if (body.containsKey("browserType")) s.setBrowserType((String) body.get("browserType"));
|
||||||
|
if (body.containsKey("status")) s.setStatus((String) body.get("status"));
|
||||||
|
if (body.containsKey("cronExpression")) s.setCronExpression((String) body.get("cronExpression"));
|
||||||
|
if (body.containsKey("testSuiteId") && body.get("testSuiteId") != null) {
|
||||||
|
s.setTestSuiteId(((Number) body.get("testSuiteId")).longValue());
|
||||||
|
}
|
||||||
|
if (body.containsKey("environmentId") && body.get("environmentId") != null) {
|
||||||
|
s.setEnvironmentId(((Number) body.get("environmentId")).longValue());
|
||||||
|
}
|
||||||
|
// Outlook-style fields
|
||||||
|
if (body.containsKey("recurrenceType")) s.setRecurrenceType((String) body.get("recurrenceType"));
|
||||||
|
if (body.containsKey("recurrenceDays")) s.setRecurrenceDays((String) body.get("recurrenceDays"));
|
||||||
|
if (body.containsKey("scheduledDate") && body.get("scheduledDate") != null) {
|
||||||
|
try { s.setScheduledDate(LocalDate.parse((String) body.get("scheduledDate"))); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
if (body.containsKey("scheduledTime") && body.get("scheduledTime") != null) {
|
||||||
|
try { s.setScheduledTime(LocalTime.parse((String) body.get("scheduledTime"))); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
if (body.containsKey("recurrenceEndDate") && body.get("recurrenceEndDate") != null) {
|
||||||
|
String endDateStr = (String) body.get("recurrenceEndDate");
|
||||||
|
if (!endDateStr.isBlank()) {
|
||||||
|
try { s.setRecurrenceEndDate(LocalDate.parse(endDateStr)); } catch (Exception ignored) {}
|
||||||
|
} else {
|
||||||
|
s.setRecurrenceEndDate(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.containsKey("timezone") && body.get("timezone") != null) {
|
||||||
|
s.setTimezone((String) body.get("timezone"));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
package com.autopilot.localagent_cloud.service;
|
||||||
|
|
||||||
|
import com.autopilot.localagent_cloud.model.TestCaseGroup;
|
||||||
|
import com.autopilot.localagent_cloud.model.TestCaseGroupMapping;
|
||||||
|
import com.autopilot.localagent_cloud.model.TestStep;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestCaseGroupMappingRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestCaseGroupRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestCaseRepository;
|
||||||
|
import com.autopilot.localagent_cloud.repository.TestStepRepository;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class TestCaseGroupService {
|
||||||
|
|
||||||
|
private final TestCaseGroupRepository testCaseGroupRepository;
|
||||||
|
private final TestCaseGroupMappingRepository testCaseGroupMappingRepository;
|
||||||
|
private final TestCaseRepository testCaseRepository;
|
||||||
|
private final TestStepRepository testStepRepository;
|
||||||
|
|
||||||
|
public TestCaseGroupService(TestCaseGroupRepository testCaseGroupRepository,
|
||||||
|
TestCaseGroupMappingRepository testCaseGroupMappingRepository,
|
||||||
|
TestCaseRepository testCaseRepository,
|
||||||
|
TestStepRepository testStepRepository) {
|
||||||
|
this.testCaseGroupRepository = testCaseGroupRepository;
|
||||||
|
this.testCaseGroupMappingRepository = testCaseGroupMappingRepository;
|
||||||
|
this.testCaseRepository = testCaseRepository;
|
||||||
|
this.testStepRepository = testStepRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<List<TestCaseGroup>> getAll(Long orgId) {
|
||||||
|
List<TestCaseGroup> list = orgId != null
|
||||||
|
? testCaseGroupRepository.findAllByOrderByIdDesc().stream().filter(g -> orgId.equals(g.getOrgId())).toList()
|
||||||
|
: testCaseGroupRepository.findAllByOrderByIdDesc();
|
||||||
|
return ResponseEntity.ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Map<String, Object>> getById(Long id) {
|
||||||
|
return testCaseGroupRepository.findById(id).map(group -> {
|
||||||
|
List<TestCaseGroupMapping> mappings = testCaseGroupMappingRepository.findByTestCaseGroupIdOrderByCaseOrder(id);
|
||||||
|
List<Map<String, Object>> testCases = new ArrayList<>();
|
||||||
|
for (TestCaseGroupMapping m : mappings) {
|
||||||
|
testCaseRepository.findById(m.getTestCaseId()).ifPresent(tc -> {
|
||||||
|
Map<String, Object> entry = new HashMap<>();
|
||||||
|
entry.put("testCase", tc);
|
||||||
|
entry.put("caseOrder", m.getCaseOrder());
|
||||||
|
entry.put("steps", testStepRepository.findByTestCaseIdOrderByStepOrder(tc.getId()));
|
||||||
|
testCases.add(entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Map<String, Object> detail = new HashMap<>();
|
||||||
|
detail.put("group", group);
|
||||||
|
detail.put("testCases", testCases);
|
||||||
|
return ResponseEntity.ok(detail);
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public ResponseEntity<Map<String, Object>> create(Map<String, Object> body, Long orgId) {
|
||||||
|
String name = (String) body.get("name");
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
TestCaseGroup group = new TestCaseGroup();
|
||||||
|
group.setName(name);
|
||||||
|
group.setDescription((String) body.get("description"));
|
||||||
|
group.setStatus(body.getOrDefault("status", "active").toString());
|
||||||
|
group.setOrgId(orgId);
|
||||||
|
group = testCaseGroupRepository.save(group);
|
||||||
|
|
||||||
|
List<Number> testCaseIdsRaw = (List<Number>) body.get("testCaseIds");
|
||||||
|
List<Number> testCaseIds = null;
|
||||||
|
if (testCaseIdsRaw != null) {
|
||||||
|
int order = 0;
|
||||||
|
testCaseIds = new ArrayList<>();
|
||||||
|
java.util.Set<Long> uniqueIds = new java.util.LinkedHashSet<>();
|
||||||
|
for (Number tcId : testCaseIdsRaw) {
|
||||||
|
if (uniqueIds.add(tcId.longValue())) {
|
||||||
|
testCaseIds.add(tcId);
|
||||||
|
TestCaseGroupMapping mapping = new TestCaseGroupMapping();
|
||||||
|
mapping.setTestCaseGroupId(group.getId());
|
||||||
|
mapping.setTestCaseId(tcId.longValue());
|
||||||
|
mapping.setCaseOrder(order++);
|
||||||
|
testCaseGroupMappingRepository.save(mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("group", group);
|
||||||
|
result.put("testCaseIds", testCaseIds);
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public ResponseEntity<Map<String, Object>> update(Long id, Map<String, Object> body) {
|
||||||
|
return testCaseGroupRepository.findById(id).map(existing -> {
|
||||||
|
if (body.containsKey("name")) existing.setName((String) body.get("name"));
|
||||||
|
if (body.containsKey("description")) existing.setDescription((String) body.get("description"));
|
||||||
|
if (body.containsKey("status")) existing.setStatus((String) body.get("status"));
|
||||||
|
testCaseGroupRepository.save(existing);
|
||||||
|
|
||||||
|
if (body.containsKey("testCaseIds")) {
|
||||||
|
testCaseGroupMappingRepository.deleteByTestCaseGroupId(id);
|
||||||
|
List<Number> testCaseIds = (List<Number>) body.get("testCaseIds");
|
||||||
|
int order = 0;
|
||||||
|
java.util.Set<Long> uniqueIds = new java.util.LinkedHashSet<>();
|
||||||
|
for (Number tcId : testCaseIds) {
|
||||||
|
if (uniqueIds.add(tcId.longValue())) {
|
||||||
|
TestCaseGroupMapping mapping = new TestCaseGroupMapping();
|
||||||
|
mapping.setTestCaseGroupId(id);
|
||||||
|
mapping.setTestCaseId(tcId.longValue());
|
||||||
|
mapping.setCaseOrder(order++);
|
||||||
|
testCaseGroupMappingRepository.save(mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("group", existing);
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<Void> delete(Long id) {
|
||||||
|
if (!testCaseGroupRepository.existsById(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
testCaseGroupRepository.deleteById(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue