Merge remote-tracking branch 'origin/fix/pipeline-239-test-failures' into fix/security
# Conflicts: # apps/api/src/knowledge/services/fulltext-search.spec.ts # apps/orchestrator/src/git/secret-scanner.service.spec.ts
This commit is contained in:
@@ -802,7 +802,7 @@ describe("ActivityService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle database errors gracefully when logging activity", async () => {
|
it("should handle database errors gracefully when logging activity (fire-and-forget)", async () => {
|
||||||
const input: CreateActivityLogInput = {
|
const input: CreateActivityLogInput = {
|
||||||
workspaceId: "workspace-123",
|
workspaceId: "workspace-123",
|
||||||
userId: "user-123",
|
userId: "user-123",
|
||||||
@@ -814,7 +814,9 @@ describe("ActivityService", () => {
|
|||||||
const dbError = new Error("Database connection failed");
|
const dbError = new Error("Database connection failed");
|
||||||
mockPrismaService.activityLog.create.mockRejectedValue(dbError);
|
mockPrismaService.activityLog.create.mockRejectedValue(dbError);
|
||||||
|
|
||||||
await expect(service.logActivity(input)).rejects.toThrow("Database connection failed");
|
// Activity logging is fire-and-forget - returns null on error instead of throwing
|
||||||
|
const result = await service.logActivity(input);
|
||||||
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle extremely large details objects", async () => {
|
it("should handle extremely large details objects", async () => {
|
||||||
@@ -1132,7 +1134,7 @@ describe("ActivityService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("database error handling", () => {
|
describe("database error handling", () => {
|
||||||
it("should handle database connection failures in logActivity", async () => {
|
it("should handle database connection failures in logActivity (fire-and-forget)", async () => {
|
||||||
const createInput: CreateActivityLogInput = {
|
const createInput: CreateActivityLogInput = {
|
||||||
workspaceId: "workspace-123",
|
workspaceId: "workspace-123",
|
||||||
userId: "user-123",
|
userId: "user-123",
|
||||||
@@ -1144,7 +1146,9 @@ describe("ActivityService", () => {
|
|||||||
const dbError = new Error("Connection refused");
|
const dbError = new Error("Connection refused");
|
||||||
mockPrismaService.activityLog.create.mockRejectedValue(dbError);
|
mockPrismaService.activityLog.create.mockRejectedValue(dbError);
|
||||||
|
|
||||||
await expect(service.logActivity(createInput)).rejects.toThrow("Connection refused");
|
// Activity logging is fire-and-forget - returns null on error instead of throwing
|
||||||
|
const result = await service.logActivity(createInput);
|
||||||
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle Prisma timeout errors in findAll", async () => {
|
it("should handle Prisma timeout errors in findAll", async () => {
|
||||||
|
|||||||
@@ -53,79 +53,79 @@ describe("CoordinatorIntegrationController - Security", () => {
|
|||||||
expect(guards).toContain(ApiKeyGuard);
|
expect(guards).toContain(ApiKeyGuard);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("POST /coordinator/jobs should require authentication", async () => {
|
it("POST /coordinator/jobs should require authentication", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({ headers: {} }),
|
getRequest: () => ({ headers: {} }),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("PATCH /coordinator/jobs/:id/status should require authentication", async () => {
|
it("PATCH /coordinator/jobs/:id/status should require authentication", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({ headers: {} }),
|
getRequest: () => ({ headers: {} }),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("PATCH /coordinator/jobs/:id/progress should require authentication", async () => {
|
it("PATCH /coordinator/jobs/:id/progress should require authentication", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({ headers: {} }),
|
getRequest: () => ({ headers: {} }),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("POST /coordinator/jobs/:id/complete should require authentication", async () => {
|
it("POST /coordinator/jobs/:id/complete should require authentication", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({ headers: {} }),
|
getRequest: () => ({ headers: {} }),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("POST /coordinator/jobs/:id/fail should require authentication", async () => {
|
it("POST /coordinator/jobs/:id/fail should require authentication", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({ headers: {} }),
|
getRequest: () => ({ headers: {} }),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("GET /coordinator/jobs/:id should require authentication", async () => {
|
it("GET /coordinator/jobs/:id should require authentication", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({ headers: {} }),
|
getRequest: () => ({ headers: {} }),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("GET /coordinator/health should require authentication", async () => {
|
it("GET /coordinator/health should require authentication", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({ headers: {} }),
|
getRequest: () => ({ headers: {} }),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Valid Authentication", () => {
|
describe("Valid Authentication", () => {
|
||||||
it("should allow requests with valid API key", async () => {
|
it("should allow requests with valid API key", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({
|
getRequest: () => ({
|
||||||
@@ -134,11 +134,11 @@ describe("CoordinatorIntegrationController - Security", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await guard.canActivate(mockContext as any);
|
const result = guard.canActivate(mockContext as never);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject requests with invalid API key", async () => {
|
it("should reject requests with invalid API key", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({
|
getRequest: () => ({
|
||||||
@@ -147,8 +147,8 @@ describe("CoordinatorIntegrationController - Security", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow("Invalid API key");
|
expect(() => guard.canActivate(mockContext as never)).toThrow("Invalid API key");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Federation Controller Tests
|
* Federation Controller Tests
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
import { Test, TestingModule } from "@nestjs/testing";
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { FederationController } from "./federation.controller";
|
import { FederationController } from "./federation.controller";
|
||||||
import { FederationService } from "./federation.service";
|
import { FederationService } from "./federation.service";
|
||||||
@@ -11,6 +11,8 @@ import { ConnectionService } from "./connection.service";
|
|||||||
import { FederationAgentService } from "./federation-agent.service";
|
import { FederationAgentService } from "./federation-agent.service";
|
||||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||||
import { AdminGuard } from "../auth/guards/admin.guard";
|
import { AdminGuard } from "../auth/guards/admin.guard";
|
||||||
|
import { CsrfGuard } from "../common/guards/csrf.guard";
|
||||||
|
import { WorkspaceGuard } from "../common/guards/workspace.guard";
|
||||||
import { FederationConnectionStatus } from "@prisma/client";
|
import { FederationConnectionStatus } from "@prisma/client";
|
||||||
import type { PublicInstanceIdentity } from "./types/instance.types";
|
import type { PublicInstanceIdentity } from "./types/instance.types";
|
||||||
import type { ConnectionDetails } from "./types/connection.types";
|
import type { ConnectionDetails } from "./types/connection.types";
|
||||||
@@ -60,7 +62,13 @@ describe("FederationController", () => {
|
|||||||
disconnectedAt: null,
|
disconnectedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store original env value
|
||||||
|
const originalDefaultWorkspaceId = process.env.DEFAULT_WORKSPACE_ID;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
// Set environment variable for tests that use getDefaultWorkspaceId()
|
||||||
|
process.env.DEFAULT_WORKSPACE_ID = "12345678-1234-4123-8123-123456789abc";
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [FederationController],
|
controllers: [FederationController],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -103,6 +111,10 @@ describe("FederationController", () => {
|
|||||||
.useValue({ canActivate: () => true })
|
.useValue({ canActivate: () => true })
|
||||||
.overrideGuard(AdminGuard)
|
.overrideGuard(AdminGuard)
|
||||||
.useValue({ canActivate: () => true })
|
.useValue({ canActivate: () => true })
|
||||||
|
.overrideGuard(CsrfGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
.overrideGuard(WorkspaceGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
controller = module.get<FederationController>(FederationController);
|
controller = module.get<FederationController>(FederationController);
|
||||||
@@ -111,6 +123,15 @@ describe("FederationController", () => {
|
|||||||
connectionService = module.get<ConnectionService>(ConnectionService);
|
connectionService = module.get<ConnectionService>(ConnectionService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original env value
|
||||||
|
if (originalDefaultWorkspaceId !== undefined) {
|
||||||
|
process.env.DEFAULT_WORKSPACE_ID = originalDefaultWorkspaceId;
|
||||||
|
} else {
|
||||||
|
delete process.env.DEFAULT_WORKSPACE_ID;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe("GET /instance", () => {
|
describe("GET /instance", () => {
|
||||||
it("should return public instance identity", async () => {
|
it("should return public instance identity", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ describe("FederationService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("generateKeypair", () => {
|
describe("generateKeypair", () => {
|
||||||
it("should generate valid RSA key pair", () => {
|
it("should generate valid RSA key pair", { timeout: 30000 }, () => {
|
||||||
// Act
|
// Act
|
||||||
const result = service.generateKeypair();
|
const result = service.generateKeypair();
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ describe("FederationService", () => {
|
|||||||
expect(result.privateKey).toContain("BEGIN PRIVATE KEY");
|
expect(result.privateKey).toContain("BEGIN PRIVATE KEY");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should generate different key pairs on each call", () => {
|
it("should generate different key pairs on each call", { timeout: 60000 }, () => {
|
||||||
// Act
|
// Act
|
||||||
const result1 = service.generateKeypair();
|
const result1 = service.generateKeypair();
|
||||||
const result2 = service.generateKeypair();
|
const result2 = service.generateKeypair();
|
||||||
@@ -199,7 +199,7 @@ describe("FederationService", () => {
|
|||||||
expect(result1.privateKey).not.toEqual(result2.privateKey);
|
expect(result1.privateKey).not.toEqual(result2.privateKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should generate RSA-4096 key pairs for future-proof security", () => {
|
it("should generate RSA-4096 key pairs for future-proof security", { timeout: 30000 }, () => {
|
||||||
// Act
|
// Act
|
||||||
const result = service.generateKeypair();
|
const result = service.generateKeypair();
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import { JOB_CREATED, JOB_STARTED, STEP_STARTED } from "./event-types";
|
|||||||
* NOTE: These tests require a real database connection with realistic data volume.
|
* NOTE: These tests require a real database connection with realistic data volume.
|
||||||
* Run with: pnpm test:api -- job-events.performance.spec.ts
|
* Run with: pnpm test:api -- job-events.performance.spec.ts
|
||||||
*/
|
*/
|
||||||
describe("JobEventsService Performance", () => {
|
const describeFn = process.env.DATABASE_URL ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeFn("JobEventsService Performance", () => {
|
||||||
let service: JobEventsService;
|
let service: JobEventsService;
|
||||||
let prisma: PrismaService;
|
let prisma: PrismaService;
|
||||||
let testJobId: string;
|
let testJobId: string;
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ async function isFulltextSearchConfigured(prisma: PrismaClient): Promise<boolean
|
|||||||
* Integration tests for PostgreSQL full-text search setup
|
* Integration tests for PostgreSQL full-text search setup
|
||||||
* Tests the tsvector column, GIN index, and automatic trigger
|
* Tests the tsvector column, GIN index, and automatic trigger
|
||||||
*
|
*
|
||||||
* NOTE: Tests that require the trigger/index will be skipped if the
|
* NOTE: These tests require a real database connection.
|
||||||
* database migration hasn't been applied. The first test (column exists)
|
* Skip when DATABASE_URL is not set. Tests that require the trigger/index
|
||||||
* will always run to validate the schema.
|
* will be skipped if the database migration hasn't been applied.
|
||||||
*/
|
*/
|
||||||
describe("Full-Text Search Setup (Integration)", () => {
|
const describeFn = process.env.DATABASE_URL ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeFn("Full-Text Search Setup (Integration)", () => {
|
||||||
let prisma: PrismaClient;
|
let prisma: PrismaClient;
|
||||||
let testWorkspaceId: string;
|
let testWorkspaceId: string;
|
||||||
let testUserId: string;
|
let testUserId: string;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ describe("PrismaService", () => {
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await service.$disconnect();
|
await service.$disconnect();
|
||||||
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be defined", () => {
|
it("should be defined", () => {
|
||||||
@@ -126,14 +127,19 @@ describe("PrismaService", () => {
|
|||||||
const workspaceId = "workspace-456";
|
const workspaceId = "workspace-456";
|
||||||
const executeRawSpy = vi.spyOn(service, "$executeRaw").mockResolvedValue(0);
|
const executeRawSpy = vi.spyOn(service, "$executeRaw").mockResolvedValue(0);
|
||||||
|
|
||||||
await service.$transaction(async (tx) => {
|
// Mock $transaction to execute the callback with a mock tx client
|
||||||
await service.setWorkspaceContext(userId, workspaceId, tx);
|
const mockTx = {
|
||||||
|
$executeRaw: vi.fn().mockResolvedValue(0),
|
||||||
|
};
|
||||||
|
vi.spyOn(service, "$transaction").mockImplementation(async (fn) => {
|
||||||
|
return fn(mockTx as never);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(executeRawSpy).toHaveBeenCalledTimes(2);
|
await service.$transaction(async (tx) => {
|
||||||
// Check that both session variables were set
|
await service.setWorkspaceContext(userId, workspaceId, tx as never);
|
||||||
expect(executeRawSpy).toHaveBeenNthCalledWith(1, expect.anything());
|
});
|
||||||
expect(executeRawSpy).toHaveBeenNthCalledWith(2, expect.anything());
|
|
||||||
|
expect(mockTx.$executeRaw).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work when called outside transaction using default client", async () => {
|
it("should work when called outside transaction using default client", async () => {
|
||||||
@@ -151,20 +157,48 @@ describe("PrismaService", () => {
|
|||||||
it("should execute function with workspace context set", async () => {
|
it("should execute function with workspace context set", async () => {
|
||||||
const userId = "user-123";
|
const userId = "user-123";
|
||||||
const workspaceId = "workspace-456";
|
const workspaceId = "workspace-456";
|
||||||
const executeRawSpy = vi.spyOn(service, "$executeRaw").mockResolvedValue(0);
|
|
||||||
|
// Mock $transaction to execute the callback with a mock tx client that has $executeRaw
|
||||||
|
const mockTx = {
|
||||||
|
$executeRaw: vi.fn().mockResolvedValue(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock both methods at the same time to avoid spy issues
|
||||||
|
const originalSetContext = service.setWorkspaceContext.bind(service);
|
||||||
|
const setContextCalls: [string, string, unknown][] = [];
|
||||||
|
service.setWorkspaceContext = vi.fn().mockImplementation((uid, wid, tx) => {
|
||||||
|
setContextCalls.push([uid, wid, tx]);
|
||||||
|
return Promise.resolve();
|
||||||
|
}) as typeof service.setWorkspaceContext;
|
||||||
|
|
||||||
|
service.$transaction = vi.fn().mockImplementation(async (fn) => {
|
||||||
|
return fn(mockTx as never);
|
||||||
|
}) as typeof service.$transaction;
|
||||||
|
|
||||||
const result = await service.withWorkspaceContext(userId, workspaceId, async () => {
|
const result = await service.withWorkspaceContext(userId, workspaceId, async () => {
|
||||||
return "test-result";
|
return "test-result";
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe("test-result");
|
expect(result).toBe("test-result");
|
||||||
expect(executeRawSpy).toHaveBeenCalledTimes(2);
|
expect(setContextCalls).toHaveLength(1);
|
||||||
|
expect(setContextCalls[0]).toEqual([userId, workspaceId, mockTx]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should pass transaction client to callback", async () => {
|
it("should pass transaction client to callback", async () => {
|
||||||
const userId = "user-123";
|
const userId = "user-123";
|
||||||
const workspaceId = "workspace-456";
|
const workspaceId = "workspace-456";
|
||||||
vi.spyOn(service, "$executeRaw").mockResolvedValue(0);
|
|
||||||
|
// Mock $transaction to execute the callback with a mock tx client
|
||||||
|
const mockTx = {
|
||||||
|
$executeRaw: vi.fn().mockResolvedValue(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setWorkspaceContext = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(undefined) as typeof service.setWorkspaceContext;
|
||||||
|
service.$transaction = vi.fn().mockImplementation(async (fn) => {
|
||||||
|
return fn(mockTx as never);
|
||||||
|
}) as typeof service.$transaction;
|
||||||
|
|
||||||
let receivedClient: unknown = null;
|
let receivedClient: unknown = null;
|
||||||
await service.withWorkspaceContext(userId, workspaceId, async (tx) => {
|
await service.withWorkspaceContext(userId, workspaceId, async (tx) => {
|
||||||
@@ -179,7 +213,18 @@ describe("PrismaService", () => {
|
|||||||
it("should handle errors from callback", async () => {
|
it("should handle errors from callback", async () => {
|
||||||
const userId = "user-123";
|
const userId = "user-123";
|
||||||
const workspaceId = "workspace-456";
|
const workspaceId = "workspace-456";
|
||||||
vi.spyOn(service, "$executeRaw").mockResolvedValue(0);
|
|
||||||
|
// Mock $transaction to execute the callback with a mock tx client
|
||||||
|
const mockTx = {
|
||||||
|
$executeRaw: vi.fn().mockResolvedValue(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setWorkspaceContext = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(undefined) as typeof service.setWorkspaceContext;
|
||||||
|
service.$transaction = vi.fn().mockImplementation(async (fn) => {
|
||||||
|
return fn(mockTx as never);
|
||||||
|
}) as typeof service.$transaction;
|
||||||
|
|
||||||
const error = new Error("Callback error");
|
const error = new Error("Callback error");
|
||||||
await expect(
|
await expect(
|
||||||
@@ -192,13 +237,19 @@ describe("PrismaService", () => {
|
|||||||
|
|
||||||
describe("clearWorkspaceContext", () => {
|
describe("clearWorkspaceContext", () => {
|
||||||
it("should clear workspace context variables", async () => {
|
it("should clear workspace context variables", async () => {
|
||||||
const executeRawSpy = vi.spyOn(service, "$executeRaw").mockResolvedValue(0);
|
// Mock $transaction to execute the callback with a mock tx client
|
||||||
|
const mockTx = {
|
||||||
await service.$transaction(async (tx) => {
|
$executeRaw: vi.fn().mockResolvedValue(0),
|
||||||
await service.clearWorkspaceContext(tx);
|
};
|
||||||
|
vi.spyOn(service, "$transaction").mockImplementation(async (fn) => {
|
||||||
|
return fn(mockTx as never);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(executeRawSpy).toHaveBeenCalledTimes(2);
|
await service.$transaction(async (tx) => {
|
||||||
|
await service.clearWorkspaceContext(tx as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTx.$executeRaw).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -374,25 +374,32 @@ describe("RunnerJobsService", () => {
|
|||||||
id: jobId,
|
id: jobId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
status: RunnerJobStatus.PENDING,
|
status: RunnerJobStatus.PENDING,
|
||||||
|
version: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockUpdatedJob = {
|
const mockUpdatedJob = {
|
||||||
...mockExistingJob,
|
...mockExistingJob,
|
||||||
status: RunnerJobStatus.CANCELLED,
|
status: RunnerJobStatus.CANCELLED,
|
||||||
completedAt: new Date(),
|
completedAt: new Date(),
|
||||||
|
version: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
mockPrismaService.runnerJob.findUnique.mockResolvedValue(mockExistingJob);
|
// First findUnique returns existing job, second returns updated job
|
||||||
mockPrismaService.runnerJob.update.mockResolvedValue(mockUpdatedJob);
|
mockPrismaService.runnerJob.findUnique
|
||||||
|
.mockResolvedValueOnce(mockExistingJob)
|
||||||
|
.mockResolvedValueOnce(mockUpdatedJob);
|
||||||
|
// updateMany returns count for optimistic locking
|
||||||
|
mockPrismaService.runnerJob.updateMany.mockResolvedValue({ count: 1 });
|
||||||
|
|
||||||
const result = await service.cancel(jobId, workspaceId);
|
const result = await service.cancel(jobId, workspaceId);
|
||||||
|
|
||||||
expect(result).toEqual(mockUpdatedJob);
|
expect(result).toEqual(mockUpdatedJob);
|
||||||
expect(prisma.runnerJob.update).toHaveBeenCalledWith({
|
expect(mockPrismaService.runnerJob.updateMany).toHaveBeenCalledWith({
|
||||||
where: { id: jobId, workspaceId },
|
where: { id: jobId, workspaceId, version: mockExistingJob.version },
|
||||||
data: {
|
data: {
|
||||||
status: RunnerJobStatus.CANCELLED,
|
status: RunnerJobStatus.CANCELLED,
|
||||||
completedAt: expect.any(Date),
|
completedAt: expect.any(Date),
|
||||||
|
version: { increment: 1 },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -405,17 +412,23 @@ describe("RunnerJobsService", () => {
|
|||||||
id: jobId,
|
id: jobId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
status: RunnerJobStatus.QUEUED,
|
status: RunnerJobStatus.QUEUED,
|
||||||
|
version: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
mockPrismaService.runnerJob.findUnique.mockResolvedValue(mockExistingJob);
|
const mockUpdatedJob = {
|
||||||
mockPrismaService.runnerJob.update.mockResolvedValue({
|
|
||||||
...mockExistingJob,
|
...mockExistingJob,
|
||||||
status: RunnerJobStatus.CANCELLED,
|
status: RunnerJobStatus.CANCELLED,
|
||||||
});
|
version: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockPrismaService.runnerJob.findUnique
|
||||||
|
.mockResolvedValueOnce(mockExistingJob)
|
||||||
|
.mockResolvedValueOnce(mockUpdatedJob);
|
||||||
|
mockPrismaService.runnerJob.updateMany.mockResolvedValue({ count: 1 });
|
||||||
|
|
||||||
await service.cancel(jobId, workspaceId);
|
await service.cancel(jobId, workspaceId);
|
||||||
|
|
||||||
expect(prisma.runnerJob.update).toHaveBeenCalled();
|
expect(mockPrismaService.runnerJob.updateMany).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw NotFoundException if job not found", async () => {
|
it("should throw NotFoundException if job not found", async () => {
|
||||||
|
|||||||
@@ -48,29 +48,29 @@ describe("StitcherController - Security", () => {
|
|||||||
expect(guards).toContain(ApiKeyGuard);
|
expect(guards).toContain(ApiKeyGuard);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("POST /stitcher/webhook should require authentication", async () => {
|
it("POST /stitcher/webhook should require authentication", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({ headers: {} }),
|
getRequest: () => ({ headers: {} }),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("POST /stitcher/dispatch should require authentication", async () => {
|
it("POST /stitcher/dispatch should require authentication", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({ headers: {} }),
|
getRequest: () => ({ headers: {} }),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Valid Authentication", () => {
|
describe("Valid Authentication", () => {
|
||||||
it("should allow requests with valid API key", async () => {
|
it("should allow requests with valid API key", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({
|
getRequest: () => ({
|
||||||
@@ -79,11 +79,11 @@ describe("StitcherController - Security", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await guard.canActivate(mockContext as any);
|
const result = guard.canActivate(mockContext as never);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject requests with invalid API key", async () => {
|
it("should reject requests with invalid API key", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({
|
getRequest: () => ({
|
||||||
@@ -92,11 +92,11 @@ describe("StitcherController - Security", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow("Invalid API key");
|
expect(() => guard.canActivate(mockContext as never)).toThrow("Invalid API key");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject requests with empty API key", async () => {
|
it("should reject requests with empty API key", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({
|
getRequest: () => ({
|
||||||
@@ -105,13 +105,13 @@ describe("StitcherController - Security", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow("No API key provided");
|
expect(() => guard.canActivate(mockContext as never)).toThrow("No API key provided");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Webhook Security", () => {
|
describe("Webhook Security", () => {
|
||||||
it("should prevent unauthorized webhook submissions", async () => {
|
it("should prevent unauthorized webhook submissions", () => {
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
getRequest: () => ({
|
getRequest: () => ({
|
||||||
@@ -125,7 +125,7 @@ describe("StitcherController - Security", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(mockContext as any)).rejects.toThrow(UnauthorizedException);
|
expect(() => guard.canActivate(mockContext as never)).toThrow(UnauthorizedException);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -434,17 +434,24 @@ SECRET=replace-me
|
|||||||
// Remove read permissions
|
// Remove read permissions
|
||||||
await fs.chmod(testFile, 0o000);
|
await fs.chmod(testFile, 0o000);
|
||||||
|
|
||||||
// Check if we're running as root (where chmod 0o000 won't prevent reads)
|
// Check if we can still read the file (e.g., running as root)
|
||||||
const isRoot = process.getuid?.() === 0;
|
let canReadAsRoot = false;
|
||||||
|
try {
|
||||||
|
await fs.readFile(testFile);
|
||||||
|
canReadAsRoot = true;
|
||||||
|
} catch {
|
||||||
|
// Expected behavior for non-root users
|
||||||
|
}
|
||||||
|
|
||||||
const result = await service.scanFile(testFile);
|
const result = await service.scanFile(testFile);
|
||||||
|
|
||||||
if (isRoot) {
|
if (canReadAsRoot) {
|
||||||
// Root can still read the file, so it will scan successfully
|
// Running as root - file is readable despite chmod 0o000
|
||||||
|
// Scanner will successfully scan the file
|
||||||
expect(result.scannedSuccessfully).toBe(true);
|
expect(result.scannedSuccessfully).toBe(true);
|
||||||
expect(result.hasSecrets).toBe(true); // Contains AWS key
|
expect(result.hasSecrets).toBe(true); // Contains AWS key
|
||||||
} else {
|
} else {
|
||||||
// Non-root user cannot read the file
|
// Normal user - file is unreadable
|
||||||
expect(result.scannedSuccessfully).toBe(false);
|
expect(result.scannedSuccessfully).toBe(false);
|
||||||
expect(result.scanError).toBeDefined();
|
expect(result.scanError).toBeDefined();
|
||||||
expect(result.hasSecrets).toBe(false); // Not "clean", just unscanned
|
expect(result.hasSecrets).toBe(false); // Not "clean", just unscanned
|
||||||
|
|||||||
Reference in New Issue
Block a user