diff --git a/src/modules/common/enum/testflow.enum.ts b/src/modules/common/enum/testflow.enum.ts
index 8e6b02c13..44420c330 100644
--- a/src/modules/common/enum/testflow.enum.ts
+++ b/src/modules/common/enum/testflow.enum.ts
@@ -32,12 +32,12 @@ export enum RequestDataTypeEnum {
export interface EmailData {
userName: string;
scheduleName: string;
- scheduleLastestRun: Date;
+ scheduleLastestRun: string;
scheduleRunResult: string;
scheduleRunPassedCount: number;
scheduleRunFailedCount: number;
scheduleRunTotalRequest: number;
- scheduleRunPassPercentage: number;
+ scheduleRunPassPercentage: string;
scheduleTotalTime: string | number;
scheduleRunEnvName: string;
isSuccess: boolean;
@@ -61,6 +61,7 @@ export interface DailyConfig {
export interface HourlyConfig {
type: RunCycleEnum.HOURLY;
+ executeAt: Date;
intervalHours: number;
startTime?: {
hour: number;
diff --git a/src/modules/views/testflowScheduleRunEmail.handlebars b/src/modules/views/testflowScheduleRunEmail.handlebars
index 37c613f37..4abe68f28 100644
--- a/src/modules/views/testflowScheduleRunEmail.handlebars
+++ b/src/modules/views/testflowScheduleRunEmail.handlebars
@@ -116,7 +116,7 @@
{{else if isFailed}}
❌️ Failed
{{else if isPartial}}
- ⚠️ Partially Failed({{scheduleRunPassedCount}}/{{scheduleRunTotalRequest}} Requests Failed)
+ ⚠️ Partially Failed({{scheduleRunFailedCount}}/{{scheduleRunTotalRequest}} Requests Failed)
{{/if}}
@@ -126,7 +126,7 @@
- Run Summary
+ Run Summary
|
@@ -143,7 +143,7 @@
| Pass Rate |
- {{scheduleRunPassPercentage}} |
+ {{scheduleRunPassPercentage}}% |
| Total Duration |
@@ -178,11 +178,21 @@
diff --git a/src/modules/workspace/controllers/testflow.controller.ts b/src/modules/workspace/controllers/testflow.controller.ts
index a79f8a896..5f4c75aa0 100644
--- a/src/modules/workspace/controllers/testflow.controller.ts
+++ b/src/modules/workspace/controllers/testflow.controller.ts
@@ -109,7 +109,7 @@ export class TestflowController {
* @description This will retrieve a specific Testflow using its ID,
* returning the Testflow object if found.
*/
- @Get("testflow/:testflowId")
+ @Get(":workspaceId/testflow/:testflowId")
@ApiOperation({
summary: "Get Individual Testflow",
description: "This will get individual testflow of a workspace",
@@ -121,10 +121,13 @@ export class TestflowController {
})
@ApiResponse({ status: 400, description: "Fetch Testflow Request Failed" })
async getTestflow(
+ @Param("workspaceId") workspaceId: string,
@Param("testflowId") testflowId: string,
@Res() res: FastifyReply,
+ @Req() request: ExtendedFastifyRequest,
) {
- const testflow = await this.testflowService.getTestflow(testflowId);
+ const user = request.user;
+ const testflow = await this.testflowService.getTestflow(workspaceId, testflowId, user._id);
const responseData = new ApiResponseService(
"Success",
HttpStatusCode.OK,
@@ -321,8 +324,8 @@ export class TestflowController {
const response = await this.testflowService.createTestflowSchedular(
createTestflowSchedularDto,
user,
- );
- const testflow = await this.testflowService.getTestflow(createTestflowSchedularDto.testflowId);
+ );
+ const testflow = await this.testflowService.getTestflow(createTestflowSchedularDto.workspaceId, createTestflowSchedularDto.testflowId, user._id);
const result = {
testflow,
schedule:response
@@ -359,7 +362,7 @@ export class TestflowController {
workspaceId,
user,
);
- const testflow = await this.testflowService.getTestflow(testflowId);
+ const testflow = await this.testflowService.getTestflow(workspaceId, testflowId, user._id);
const responseData = new ApiResponseService(
"Success",
HttpStatusCode.OK,
@@ -390,7 +393,7 @@ export class TestflowController {
workspaceId,
user,
);
- const testflow = await this.testflowService.getTestflow(testflowId);
+ const testflow = await this.testflowService.getTestflow(workspaceId, testflowId, user._id);
const responseData = new ApiResponseService(
"Success",
HttpStatusCode.OK,
@@ -421,7 +424,7 @@ export class TestflowController {
workspaceId,
user,
);
- const testflow = await this.testflowService.getTestflow(testflowId);
+ const testflow = await this.testflowService.getTestflow(workspaceId, testflowId, user._id);
const responseData = new ApiResponseService(
"Success",
HttpStatusCode.OK,
@@ -448,7 +451,7 @@ export class TestflowController {
) {
const user = request.user;
await this.testflowService.deleteScheduleRunHistory(workspaceId, testflowId, scheduleId, runHistoryId, user);
- const testflow = await this.testflowService.getTestflow(testflowId);
+ const testflow = await this.testflowService.getTestflow(workspaceId, testflowId, user._id);
const responseData = new ApiResponseService(
"Success",
HttpStatusCode.OK,
diff --git a/src/modules/workspace/repositories/testflow.repository.ts b/src/modules/workspace/repositories/testflow.repository.ts
index 7243b9054..f645ce0c1 100644
--- a/src/modules/workspace/repositories/testflow.repository.ts
+++ b/src/modules/workspace/repositories/testflow.repository.ts
@@ -206,7 +206,7 @@ export class TestflowRepository {
schedularId: string,
runHistoryItem: TestFlowSchedularRunHistory,
): Promise {
- const now = new Date();
+ const nowUtc = new Date().toISOString();
if (!testflowId || !schedularId) {
throw new Error("Both testflowId and schedularId are required");
}
@@ -215,8 +215,8 @@ export class TestflowRepository {
{
$inc: { "schedules.$[elem].executedCount": 1 },
$set: {
- "schedules.$[elem].lastExecuted": now,
- updatedAt: now,
+ "schedules.$[elem].lastExecuted": nowUtc,
+ updatedAt: nowUtc,
},
$push: {
"schedules.$[elem].schedularRunHistory": {
@@ -231,34 +231,65 @@ export class TestflowRepository {
);
}
- /**
+ /**
+ * Edit a schedular execution (run history item) in a testflow's schedule.
+ * @param {string} testflowId - The testflow document ID.
+ * @param {string} schedularId - The schedule ID.
+ * @param {Partial} updatedRunHistory - The updated fields for the run history item.
+ * @returns {Promise} - The result of the update operation.
+ */
+ async editSchedularExecution(
+ testflowId: string,
+ schedularId: string,
+ updatedRunHistory: Partial,
+ ): Promise {
+ if (!testflowId || !schedularId || !updatedRunHistory.id) {
+ throw new Error("testflowId, schedularId, and runHistoryId are required");
+ }
+ // Build the update object for only the provided fields
+ const setObj: Record = {};
+ for (const [key, value] of Object.entries(updatedRunHistory)) {
+ setObj[`schedules.$[elem].schedularRunHistory.$[run].${key}`] = value;
+ }
+ setObj["updatedAt"] = new Date();
+ return this.db.collection(Collections.TESTFLOW).updateOne(
+ { _id: new ObjectId(testflowId) },
+ { $set: setObj },
+ {
+ arrayFilters: [
+ { "elem.id": schedularId },
+ { "run.id": updatedRunHistory.id },
+ ],
+ },
+ );
+ }
+
+ /**
* Edit a schedular execution (run history item) in a testflow's schedule.
* @param {string} testflowId - The testflow document ID.
* @param {string} schedularId - The schedule ID.
- * @param {Partial} updatedRunHistory - The updated fields for the run history item.
+ * @param {Partial} updatedSchedular - The updated fields for the schedule item.
* @returns {Promise} - The result of the update operation.
*/
- async editSchedularExecution(
+ async editSchedular(
testflowId: string,
schedularId: string,
- updatedRunHistory: Partial,
+ updatedSchedular: Partial,
): Promise {
- if (!testflowId || !schedularId || !updatedRunHistory.id) {
- throw new Error("testflowId, schedularId, and runHistoryId are required");
+ if (!testflowId || !schedularId) {
+ throw new Error("testflowId and schedularId are required");
}
// Build the update object for only the provided fields
const setObj: Record = {};
- for (const [key, value] of Object.entries(updatedRunHistory)) {
- setObj[`schedules.$[elem].schedularRunHistory.$[run].${key}`] = value;
+ for (const [key, value] of Object.entries(updatedSchedular)) {
+ setObj[`schedules.$[elem].${key}`] = value;
}
- setObj["updatedAt"] = new Date();
return this.db.collection(Collections.TESTFLOW).updateOne(
{ _id: new ObjectId(testflowId) },
{ $set: setObj },
{
arrayFilters: [
{ "elem.id": schedularId },
- { "run.id": updatedRunHistory.id },
],
},
);
diff --git a/src/modules/workspace/services/testflow-run.service.ts b/src/modules/workspace/services/testflow-run.service.ts
index d4dc848f3..0c4c45d2c 100644
--- a/src/modules/workspace/services/testflow-run.service.ts
+++ b/src/modules/workspace/services/testflow-run.service.ts
@@ -73,33 +73,35 @@ export class TestflowRunService {
testflowId:string,
user?: DecodedUserObject,
): Promise => {
- // Fetch testflow and environment data
- const testflowDetails = await this.testflowRepository.get(testflowId);
- const workspaceID = await this.workspaceReposistory.get(workspaceId);
- const globalEnvironment = workspaceID.environments[0];
- const globalEnvDetails = await this.environmentReposistory.get(
- globalEnvironment.id.toString(),
- );
- let environmentData;
- if (environmentId) {
- environmentData =
- await this.environmentReposistory.get(environmentId);
- }
- const activeVariables = this.combineEnvironmentData(
- globalEnvDetails?.variable || [],
- environmentData?.variable || [],
- );
- // Build proxy URL
- const sparrowProxy = this.configService.get("sparrowProxy.baseUrl");
- const proxyUrl = `${sparrowProxy}/proxy/testflow/execute`;
- // Prepare request body for proxy API
- const body = {
- nodes: testflowDetails.nodes || [],
- variables: activeVariables || [],
- edges: testflowDetails.edges,
- userId: user?._id || new ObjectId("000000000000000000000000"),
- };
try {
+ // Fetch testflow and environment data
+ const testflowDetails = await this.testflowRepository.get(testflowId);
+ const workspaceID = await this.workspaceReposistory.get(workspaceId);
+ const globalEnvironment = workspaceID.environments[0];
+ const globalEnvDetails = await this.environmentReposistory.get(
+ globalEnvironment.id.toString(),
+ );
+ let environmentData;
+ if (environmentId) {
+ try{
+ environmentData =
+ await this.environmentReposistory.get(environmentId);
+ }catch(err){}
+ }
+ const activeVariables = this.combineEnvironmentData(
+ globalEnvDetails?.variable || [],
+ environmentData?.variable || [],
+ );
+ // Build proxy URL
+ const sparrowProxy = this.configService.get("sparrowProxy.baseUrl");
+ const proxyUrl = `${sparrowProxy}/proxy/testflow/execute`;
+ // Prepare request body for proxy API
+ const body = {
+ nodes: testflowDetails.nodes || [],
+ variables: activeVariables || [],
+ edges: testflowDetails.edges,
+ userId: user?._id || new ObjectId("000000000000000000000000"),
+ };
const response = await axios.post(proxyUrl, body, {
headers: {
"Content-Type": "application/json",
@@ -111,11 +113,18 @@ export class TestflowRunService {
nodes:testflowDetails.nodes,
edges:testflowDetails.edges,
}
- // Return only history or any relevant part
return finalResult;
} catch (error: any) {
- console.error("Testflow proxy execution failed:", error.message || error);
- throw new Error(error?.message || "Testflow execution failed.");
+ return {
+ result:{
+ history: {
+ status: "error"
+ }
+ },
+ environmentName: "",
+ nodes: [],
+ edges: [],
+ }
}
};
}
\ No newline at end of file
diff --git a/src/modules/workspace/services/testflow-schedular.service.ts b/src/modules/workspace/services/testflow-schedular.service.ts
index 8d5010363..6fc81fbd2 100644
--- a/src/modules/workspace/services/testflow-schedular.service.ts
+++ b/src/modules/workspace/services/testflow-schedular.service.ts
@@ -3,12 +3,13 @@ import { SchedulerRegistry } from "@nestjs/schedule";
import { CronJob } from "cron";
import { RunCycleEnum } from "@src/modules/common/enum/testflow.enum";
import { RunCycleConfig } from "@src/modules/common/enum/testflow.enum";
+import { TestflowRepository } from "../repositories/testflow.repository";
@Injectable()
export class TestflowSchedulerService {
private readonly logger = new Logger(TestflowSchedulerService.name);
- constructor(private schedulerRegistry: SchedulerRegistry) {}
+ constructor(private schedulerRegistry: SchedulerRegistry ) {}
/**
* Add a cron job
@@ -16,30 +17,102 @@ export class TestflowSchedulerService {
async addSchedulerJob(
runCycle: RunCycleConfig,
runApis: (schedularId: string) => Promise,
- jobName: string,
+ afterJobCreate: (cronExpression: string) => void,
cronExpression: string,
schedularId: string,
timezone: string = "UTC", // Always use UTC by default
): Promise {
if (!cronExpression) {
- console.error(`Invalid run cycle configuration for job ${jobName}`);
- this.logger.log(`Invalid run cycle configuration for job ${jobName}`);
+ this.logger.log(`Invalid run cycle configuration for job ${schedularId}`);
return false;
}
try {
- // Create cron job with UTC timezone
+ // For HOURLY rolling interval, create a one-time cron job for the next execution
+ if (runCycle.type === RunCycleEnum.HOURLY && runCycle.intervalHours) {
+
+ const scheduleNext = async (lastRun: Date) => {
+ // Calculate next run time
+ const nextRun = new Date(lastRun.getTime() + runCycle.intervalHours * 60 * 60 * 1000);
+ // Generate one-time cron expression for nextRun
+ const second = nextRun.getUTCSeconds();
+ const minute = nextRun.getUTCMinutes();
+ const hour = nextRun.getUTCHours();
+ const dayOfMonth = nextRun.getUTCDate();
+ const month = nextRun.getUTCMonth() + 1;
+ const oneTimeCron = `${second} ${minute} ${hour} ${dayOfMonth} ${month} *`;
+ const nextJobName = schedularId; // Always use scheduleId as job name
+ this.logger.log(`Next rolling interval job ${nextJobName} scheduled for ${nextRun.toISOString()} (cron: ${oneTimeCron})`);
+ // Remove any existing job with this name before adding
+ if (this.schedulerRegistry.doesExist("cron", nextJobName)) {
+ const oldJob = this.schedulerRegistry.getCronJob(nextJobName);
+ oldJob.stop();
+ this.schedulerRegistry.deleteCronJob(nextJobName);
+ }
+ const job = new CronJob(
+ oneTimeCron,
+ async () => {
+ this.logger.log(`Executing rolling interval job ${nextJobName} at ${new Date().toISOString()} (UTC)`);
+ if (runApis) {
+ try {
+ await runApis(schedularId);
+ } catch (error) {
+ this.logger.error(`Error executing job ${nextJobName}: ${error.message}`, error.stack);
+ }
+ }
+ job.stop();
+ this.schedulerRegistry.deleteCronJob(nextJobName);
+ // Schedule next job
+ await scheduleNext(nextRun);
+ },
+ null,
+ false,
+ timezone,
+ );
+ afterJobCreate(oneTimeCron);
+ this.schedulerRegistry.addCronJob(nextJobName, job);
+ job.start();
+ };
+ // Use the provided initial cron expression for the first run
+ const nextJobName = schedularId;
+ // Remove any existing job with this name before adding
+ if (this.schedulerRegistry.doesExist("cron", nextJobName)) {
+ const oldJob = this.schedulerRegistry.getCronJob(nextJobName);
+ oldJob.stop();
+ this.schedulerRegistry.deleteCronJob(nextJobName);
+ }
+ const job = new CronJob(
+ cronExpression,
+ async () => {
+ if (runApis) {
+ try {
+ await runApis(schedularId);
+ } catch (error) {
+ this.logger.error(`Error executing job ${nextJobName}: ${error.message}`, error.stack);
+ }
+ }
+ job.stop();
+ this.schedulerRegistry.deleteCronJob(nextJobName);
+ // After first run, start rolling with scheduleNext
+ await scheduleNext(new Date());
+ },
+ null,
+ false,
+ timezone,
+ );
+ this.schedulerRegistry.addCronJob(nextJobName, job);
+ job.start();
+ return true;
+ }
+ // Default: Create cron job with UTC timezone
const job = new CronJob(
cronExpression,
async () => {
- this.logger.log(
- `Executing job ${jobName} at ${new Date().toISOString()} (UTC)`
- );
if (runApis) {
try {
await runApis(schedularId);
} catch (error) {
this.logger.error(
- `Error executing job ${jobName}: ${error.message}`,
+ `Error executing job ${schedularId}: ${error.message}`,
error.stack
);
}
@@ -47,24 +120,19 @@ export class TestflowSchedulerService {
// Handle one-time jobs
if (runCycle.type === RunCycleEnum.ONCE) {
job.stop();
- this.schedulerRegistry.deleteCronJob(jobName);
- this.logger.log(`One-time scheduler ${jobName} completed and removed`);
+ this.schedulerRegistry.deleteCronJob(schedularId);
}
},
null, // onComplete callback
false, // start - we'll call start() manually
timezone, // Set timezone to UTC
);
- this.schedulerRegistry.addCronJob(jobName, job);
+ this.schedulerRegistry.addCronJob(schedularId, job);
job.start();
- this.logger.log(
- `Scheduler job ${jobName} registered with cycle: ${runCycle.type}, ` +
- `timezone: ${timezone}, cron: ${cronExpression}`
- );
return true;
} catch (error) {
this.logger.error(
- `Failed to create scheduler job ${jobName}: ${error.message}`,
+ `Failed to create scheduler job ${schedularId}: ${error.message}`,
error.stack
);
return false;
@@ -82,9 +150,6 @@ export class TestflowSchedulerService {
job.stop();
this.schedulerRegistry.deleteCronJob(jobName);
}
- this.logger.log(
- `Scheduler job ${jobName} (ID: ${schedulerId}) removed from DB + registry`,
- );
return true;
} catch (error) {
this.logger.warn(
@@ -102,7 +167,6 @@ export class TestflowSchedulerService {
if (this.schedulerRegistry.doesExist("cron", jobName)) {
const job = this.schedulerRegistry.getCronJob(jobName);
job.stop();
- this.logger.log(`Scheduler job ${jobName} paused`);
}
}
@@ -114,11 +178,14 @@ export class TestflowSchedulerService {
if (this.schedulerRegistry.doesExist("cron", jobName)) {
const job = this.schedulerRegistry.getCronJob(jobName);
job.start();
- this.logger.log(`Scheduler job ${jobName} resumed`);
}
}
private generateJobName(schedulerId: string): string {
- return `scheduler_${schedulerId}`;
+ return `${schedulerId}`;
+ }
+
+ public logAllCronJobs() {
+ return Array.from(this.schedulerRegistry.getCronJobs().keys());
}
}
diff --git a/src/modules/workspace/services/testflow.service.ts b/src/modules/workspace/services/testflow.service.ts
index e5bda4f6e..129e94040 100644
--- a/src/modules/workspace/services/testflow.service.ts
+++ b/src/modules/workspace/services/testflow.service.ts
@@ -47,6 +47,7 @@ import { v4 as uuidv4 } from "uuid";
import { TestflowSchedulerService } from "./testflow-schedular.service";
import {
DailyConfig,
+ DayOfWeek,
EmailData,
HourlyConfig,
NotificationReceiveType,
@@ -62,6 +63,7 @@ import { Logger } from "@nestjs/common";
import { OnModuleInit } from "@nestjs/common";
import { UserRepository } from "@src/modules/identity/repositories/user.repository";
import { EnvironmentRepository } from "../repositories/environment.repository";
+import { Collections } from "@src/modules/common/enum/database.collection.enum";
/**
* Testflow Service
@@ -82,31 +84,85 @@ export class TestflowService implements OnModuleInit {
private readonly environmentReposistory: EnvironmentRepository,
) {}
+ async getNextFutureCronExpression(pastCron: string, intervalHours: number): Promise {
+ // Expecting cron in format: 's m h * * *'
+ const parts = pastCron.trim().split(/\s+/);
+ if (parts.length !== 6) return pastCron;
+
+ let second = parseInt(parts[0], 10);
+ let minute = parseInt(parts[1], 10);
+ let hour = parseInt(parts[2], 10);
+ let day = parseInt(parts[3], 10);
+ let month = parseInt(parts[4], 10) - 1;
+
+ // Start from the past time
+ let now = new Date();
+ let next = new Date(Date.UTC(
+ now.getUTCFullYear(),
+ month,
+ day,
+ hour,
+ minute,
+ second,
+ 0
+ ));
+
+ // If the past time is in the past, keep adding interval until it's in the future
+ while (next <= now) {
+ next.setUTCHours(next.getUTCHours() + intervalHours);
+ }
+
+ // Return new cron expression
+ return `${next.getUTCSeconds()} ${next.getUTCMinutes()} ${next.getUTCHours()} ${next.getUTCDate()} ${next.getUTCMonth() + 1} *`;
+ }
+
async onModuleInit() {
- this.logger.log("Bootstrapping schedulers from DB...");
- const testflows = await this.testflowRepository.getAll();
- for (const tf of testflows) {
- if (!tf.schedules?.length) continue;
- for (const schedule of tf.schedules) {
- const runCycleConfig = this.buildRunCycleConfig(
- schedule.runConfiguration,
- );
- if (schedule.isActive && schedule.cronExpression) {
- await this.testflowSchedulerService.addSchedulerJob(
- runCycleConfig,
- this.getScheduledExecutionCallback(
- tf._id.toString(),
- schedule.environmentId,
- tf.workspaceId,
- schedule.id,
- ),
- schedule.schedularName,
- schedule.cronExpression,
- schedule.id,
- "UTC",
- );
+ try {
+ this.logger.log("Bootstrapping schedulers from DB...");
+ const testflows = await this.testflowRepository.getAll();
+ if (testflows.length === 0) {
+ this.logger.log("No testflows found — skipping scheduler bootstrap.");
+ return;
+ }
+ for (const tf of testflows) {
+ if (!tf.schedules?.length) continue;
+
+ for (const schedule of tf.schedules) {
+ try{
+ const runCycleConfig = this.buildRunCycleConfig(
+ schedule.runConfiguration,
+ );
+ if (schedule.isActive && schedule.cronExpression) {
+ let cronExpression = schedule.cronExpression;
+ if(schedule.runConfiguration.runCycle === RunCycleEnum.HOURLY){
+ const intervalHours = schedule.runConfiguration.intervalHours;
+ cronExpression = await this.getNextFutureCronExpression(cronExpression, intervalHours);
+ }
+ await this.testflowSchedulerService.addSchedulerJob(
+ runCycleConfig,
+ this.getScheduledExecutionCallback(
+ tf._id.toString(),
+ schedule.environmentId,
+ tf.workspaceId,
+ schedule.id,
+ ),
+ (_cronExpression: string)=>{
+ this.testflowRepository.editSchedular(tf._id.toString(), schedule.id, {
+ cronExpression: _cronExpression,
+ });
+ },
+ cronExpression,
+ schedule.id,
+ "UTC",
+ );
+ }
+ }catch(error){
+ this.logger.error("Error adding scheduler job:", error);
+ }
}
}
+ } catch (error) {
+ this.logger.error("Error during scheduler bootstrap:", error);
}
}
@@ -152,11 +208,36 @@ export class TestflowService implements OnModuleInit {
throw new NotFoundException("Schedule not found");
}
- let environmentName = "";
- if(updateScheduleDto?.environmentId){
- const environmentData = await this.environmentReposistory.get(updateScheduleDto?.environmentId);
- environmentName = environmentData?.name || "";
+ let environmentName = "";
+ if (updateScheduleDto?.environmentId) {
+ const environmentData = await this.environmentReposistory.get(
+ updateScheduleDto?.environmentId,
+ );
+ environmentName = environmentData?.name || "";
+ }
+
+ if(updateScheduleDto.runConfiguration){
+ const runCycleConfig = this.buildRunCycleConfig(updateScheduleDto.runConfiguration);
+ const cronExpression = this.generateCronExpression(runCycleConfig);
+ if (!cronExpression) {
+ updateScheduleDto.cronExpression = null;
+ }
+ else{
+ updateScheduleDto.cronExpression = cronExpression;
}
+ }else{
+ if(existingSchedular.runConfiguration.runCycle === RunCycleEnum.HOURLY){
+ const runCycleConfig = this.buildRunCycleConfig(existingSchedular.runConfiguration);
+ const cronExpression = this.generateCronExpression(runCycleConfig);
+ if (!cronExpression) {
+ updateScheduleDto.cronExpression = null;
+ }
+ else{
+ updateScheduleDto.cronExpression = cronExpression;
+ }
+ }
+ }
+
// Merge update fields, ensure id is present
const updatedSchedular: TestflowSchedular = {
...existingSchedular,
@@ -172,43 +253,42 @@ export class TestflowService implements OnModuleInit {
scheduleId,
updatedSchedular,
);
- // Optionally update cron job if runConfiguration or isActive changed
- if (
- updateScheduleDto.runConfiguration ||
- updateScheduleDto.isActive !== undefined
- ) {
- const schedular = await this.testflowRepository.getSchedularById(
- testflowId,
- scheduleId,
- );
- if (schedular) {
- // Remove old job
- await this.testflowSchedulerService.removeSchedulerJob(scheduleId);
- // If still active, re-add job
- if (schedular.isActive) {
- const runCycleConfig = this.buildRunCycleConfig(
- schedular.runConfiguration,
- );
- const cronExpression =
- schedular.cronExpression ||
- this.generateCronExpression(runCycleConfig);
- await this.testflowSchedulerService.addSchedulerJob(
- runCycleConfig,
- this.getScheduledExecutionCallback(
- testflowId,
- schedular.environmentId,
- workspaceId,
- scheduleId,
- user,
- ),
- schedular.schedularName,
- cronExpression,
+
+ const schedular = await this.testflowRepository.getSchedularById(
+ testflowId,
+ scheduleId,
+ );
+ if (schedular) {
+ // Remove old job
+ await this.testflowSchedulerService.removeSchedulerJob(scheduleId);
+ // If still active, re-add job
+ if (schedular.isActive) {
+ const runCycleConfig = this.buildRunCycleConfig(
+ schedular.runConfiguration,
+ );
+ const cronExpression = schedular.cronExpression;
+ await this.testflowSchedulerService.addSchedulerJob(
+ runCycleConfig,
+ this.getScheduledExecutionCallback(
+ testflowId,
+ schedular.environmentId,
+ workspaceId,
scheduleId,
- );
- }
+ user,
+ ),
+ (_cronExpression: string)=>{
+ this.testflowRepository.editSchedular(testflowId, scheduleId, {
+ cronExpression: _cronExpression,
+ });
+ },
+ cronExpression,
+ scheduleId,
+ "UTC",
+ );
}
}
- return result;
+
+ return result;
}
/**
@@ -317,8 +397,9 @@ export class TestflowService implements OnModuleInit {
* Fetches single testflow.
* @param id - Testflow id you want to fetch.
*/
- async getTestflow(id: string): Promise> {
- return await this.testflowRepository.get(id);
+ async getTestflow(workspaceId: string, testflowId: string, userId: ObjectId): Promise> {
+ await this.checkPermission(workspaceId, userId);
+ return await this.testflowRepository.get(testflowId);
}
/**
@@ -328,6 +409,9 @@ export class TestflowService implements OnModuleInit {
*/
async checkPermission(workspaceId: string, userid: ObjectId): Promise {
const workspace = await this.workspaceService.get(workspaceId);
+ if(workspace.workspaceType === WorkspaceType.PUBLIC){
+ return;
+ }
const hasPermission = workspace.users.some((user) => {
return user.id.toString() === userid.toString();
});
@@ -357,6 +441,16 @@ export class TestflowService implements OnModuleInit {
id,
user._id,
);
+
+ // Remove all associated cronjobs for this testflow
+ if (testflow?.schedules && Array.isArray(testflow.schedules)) {
+ for (const schedule of testflow.schedules) {
+ if (schedule?.id) {
+ await this.testflowSchedulerService.removeSchedulerJob(schedule.id);
+ }
+ }
+ }
+
const updateMessage = `"${testflow.name}" testflow is deleted from "${workspace.name}" workspace`;
const currentWorkspaceObject = new ObjectId(workspaceId);
const updateWorkspaceData: Partial = {
@@ -489,26 +583,7 @@ export class TestflowService implements OnModuleInit {
user: DecodedUserObject,
) {
try {
- const workspaceUsers = await this.workspaceReposistory.get(
- schedularData?.workspaceId,
- );
- if (!workspaceUsers) {
- throw new NotFoundException("Workspace not found.");
- }
- const userDetails = workspaceUsers.users.find(
- (item) => item.id === user._id.toString(),
- );
- if (!userDetails) {
- throw new NotFoundException("User not found in workspace.");
- }
- if (
- userDetails.role !== WorkspaceRole.ADMIN &&
- userDetails.role !== WorkspaceRole.EDITOR
- ) {
- throw new ForbiddenException(
- "User does not have permission to perform this action.",
- );
- }
+ await this.isWorkspaceAdminorEditor(schedularData?.workspaceId, user._id);
// Build cron config
const runCycleConfig = this.buildRunCycleConfig(
schedularData.runConfiguration,
@@ -521,8 +596,10 @@ export class TestflowService implements OnModuleInit {
throw new BadRequestException("Invalid run cycle configuration");
}
let environmentName = "";
- if(schedularData?.environmentId){
- const environmentData = await this.environmentReposistory.get(schedularData?.environmentId);
+ if (schedularData?.environmentId) {
+ const environmentData = await this.environmentReposistory.get(
+ schedularData?.environmentId,
+ );
environmentName = environmentData?.name || "";
}
// Save scheduler details in DB
@@ -557,7 +634,11 @@ export class TestflowService implements OnModuleInit {
schedulerId,
user,
),
- jobName,
+ (_cronExpression: string)=>{
+ this.testflowRepository.editSchedular(schedularData.testflowId, schedulerId, {
+ cronExpression: _cronExpression,
+ });
+ },
cronExpression,
schedulerId,
"UTC",
@@ -608,6 +689,7 @@ export class TestflowService implements OnModuleInit {
}
return {
type: RunCycleEnum.HOURLY,
+ executeAt: new Date(Date.now() + 1 * 60 * 1000),
intervalHours: runConfig.intervalHours,
startTime: runConfig.time
? this.parseTime(runConfig.time)
@@ -694,23 +776,33 @@ export class TestflowService implements OnModuleInit {
}
private generateHourlyCronExpression(config: HourlyConfig): string {
- const { intervalHours, startTime } = config;
- if (startTime) {
- const { hour, minute, second = 0 } = startTime;
- return `${second} ${minute} ${hour}-23/${intervalHours} * * *`;
- } else {
- const now = new Date();
- const utcHour = now.getUTCHours();
- const utcMinute = now.getUTCMinutes();
- const utcSecond = now.getUTCSeconds();
- return `${utcSecond} ${utcMinute} ${utcHour}-23/${intervalHours} * * *`;
+ const executeAt = config.executeAt;
+ const now = new Date();
+ if (executeAt <= now) {
+ return null;
}
+ const second = executeAt.getUTCSeconds();
+ const minute = executeAt.getUTCMinutes();
+ const hour = executeAt.getUTCHours();
+ const dayOfMonth = executeAt.getUTCDate();
+ const month = executeAt.getUTCMonth() + 1;
+ return `${second} ${minute} ${hour} ${dayOfMonth} ${month} *`;
}
private generateWeeklyCronExpression(config: WeeklyConfig): string {
const { days, time } = config;
const { hour, minute, second = 0 } = time;
- return `${second} ${minute} ${hour} * * ${days.join(",")}`;
+ const validDays = days.filter((day) => {
+ return day >= DayOfWeek.SUNDAY && day <= DayOfWeek.SATURDAY;
+ });
+ if (validDays.length === 0) {
+ throw new BadRequestException(
+ "Invalid days specified. Days must be valid DayOfWeek values (0=Sunday, 1=Monday, ..., 6=Saturday)",
+ );
+ }
+ const sortedDays = validDays.sort((a, b) => a - b);
+ const daysString = sortedDays.join(",");
+ return `${second} ${minute} ${hour} * * ${daysString}`;
}
public getScheduledExecutionCallback(
@@ -748,12 +840,12 @@ export class TestflowService implements OnModuleInit {
isScheduled,
status: "pending",
requests: [],
- responses:[],
+ responses: [],
nodes: [],
edges: [],
failedRequests: 0,
successRequests: 0,
- totalTime:"0 ms",
+ totalTime: "0 ms",
createdAt: new Date(),
};
//Save execution result in DB
@@ -774,6 +866,7 @@ export class TestflowService implements OnModuleInit {
nodes: response.nodes,
edges: response.edges,
...response.result.history,
+ status: response?.result?.history?.status || "error",
};
//Save execution result in DB
await this.testflowRepository.editSchedularExecution(
@@ -792,13 +885,19 @@ export class TestflowService implements OnModuleInit {
false,
);
}
- const data = response.result.history;
+ const data = response?.result?.history;
let scheduleRunResult;
- if (data.status === "fail" && data.successRequests < 1) {
+ if(!response?.status){
+ scheduleRunResult = "error";
+ }
+ else if (data?.status === "fail" && data?.successRequests < 1) {
scheduleRunResult = "failed";
- } else if (data.status === "success") {
+ } else if (data?.status === "success") {
scheduleRunResult = "success";
- } else {
+ } else if (data?.status === "error") {
+ scheduleRunResult = "error";
+ }
+ else {
scheduleRunResult = "partial";
}
const totalRequestCount = data.successRequests + data.failedRequests;
@@ -806,16 +905,27 @@ export class TestflowService implements OnModuleInit {
data.createdBy,
);
const successPercentage =
- (data.successRequests / totalRequestCount) * 100;
+ Math.round((data.successRequests / totalRequestCount) * 100 * 100) /
+ 100;
const emailData: EmailData = {
userName: userDetails?.name,
scheduleName: getSchedular.name,
- scheduleLastestRun: new Date(getSchedular.lastExecuted),
+ scheduleLastestRun:
+ new Date(getSchedular.lastExecuted).toLocaleString("en-US", {
+ timeZone: "UTC",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ }) + " UTC",
scheduleRunResult: scheduleRunResult,
scheduleRunPassedCount: data.successRequests,
scheduleRunFailedCount: data.failedRequests,
scheduleRunTotalRequest: data.successRequests + data.failedRequests,
- scheduleRunPassPercentage: successPercentage,
+ scheduleRunPassPercentage: successPercentage.toString(),
scheduleTotalTime: data.totalTime,
scheduleRunEnvName: response.environmentName,
isSuccess: data.successRequests === totalRequestCount,