Implement Tests for CF-API Flow (#1634)

Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
This commit is contained in:
HeshamHM28 2025-06-25 01:06:26 +03:00 committed by GitHub
parent 1ff32e2144
commit 6f5c2d7ad8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 9051 additions and 669 deletions

2
.gitignore vendored
View file

@ -172,7 +172,7 @@ cython_debug/
# IDE settings
.idea/
.vscode/
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

23
js/cf-api/.env.test Normal file
View file

@ -0,0 +1,23 @@
NODE_ENV=test
DATABASE_URL=postgresql://test:test@localhost:5432/test_db
KEY_VAULT_NAME=test-keyvault
STRIPE_SECRET_KEY=sk_test_mock
AUTH0_ISSUER_BASE_URL=https://test.auth0.com
AUTH0_CLIENT_ID=test-client-id
AUTH0_CLIENT_SECRET=test-client-secret
AUTH0_MANAGEMENT_CLIENT_ID=test-management-client-id
AUTH0_MANAGEMENT_CLIENT_SECRET=test-management-client-secret
GH_APP_ID=12345
GH_APP_USER_ID=67890
AZURE_CLIENT_ID=test-azure-client-id
AZURE_TENANT_ID=test-azure-tenant-id
AZURE_CLIENT_SECRET=test-azure-secret
SLACK_TOKEN=xoxb-test-token
SLACK_CHANNEL_ID=C123456789
NOTION_DATABASE_ID=test-notion-db-id
NOTION_API_TOKEN=secret_test_token
SECRET_KEY=test-secret-key
APPROVAL_REQUIRED_REPOS={}
QUALITY_MONITORING_REPOS={}
DISABLE_APPROVAL_SYSTEM=true
WEBAPP_URL=http://localhost:3000

View file

@ -1,14 +1,29 @@
import { ManagementClient } from "auth0"
// Create a singleton instance or allow injection
let managementClient: ManagementClient | null = null
export function getManagementClient(): ManagementClient {
if (!managementClient) {
managementClient = new ManagementClient({
domain: process.env.AUTH0_ISSUER_BASE_URL ?? "",
clientId: process.env.AUTH0_MANAGEMENT_CLIENT_ID ?? "",
clientSecret: process.env.AUTH0_MANAGEMENT_CLIENT_SECRET ?? "",
})
}
return managementClient
}
// For testing purposes
export function setManagementClient(client: ManagementClient | null) {
managementClient = client
}
export async function userNickname(userId: string): Promise<string | null> {
const m = new ManagementClient({
domain: process.env.AUTH0_ISSUER_BASE_URL ?? "",
clientId: process.env.AUTH0_MANAGEMENT_CLIENT_ID ?? "",
clientSecret: process.env.AUTH0_MANAGEMENT_CLIENT_SECRET ?? "",
})
const m = getManagementClient()
try {
const user = await m.users.get({ id: userId, fields: "nickname" })
return user.data?.nickname
return user.data?.nickname ?? null
} catch (error) {
console.log("Error getting user nickname:", error)
return null

View file

@ -0,0 +1,60 @@
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import { userNickname, setManagementClient } from "./auth0-mgmt"
describe("userNickname", () => {
let mockUsersGet: jest.MockedFunction<any>
let mockManagementClient: any
beforeEach(() => {
// Create mock for users.get
mockUsersGet = jest.fn()
// Create mock management client
mockManagementClient = {
users: {
get: mockUsersGet,
},
}
// Inject the mock
setManagementClient(mockManagementClient)
})
afterEach(() => {
// Clean up
setManagementClient(null)
})
it("should return nickname when user is found", async () => {
mockUsersGet.mockResolvedValue({ data: { nickname: "cool-user" } })
const nickname = await userNickname("user-123")
expect(nickname).toBe("cool-user")
expect(mockUsersGet).toHaveBeenCalledWith({ id: "user-123", fields: "nickname" })
})
it("should return null when nickname is missing", async () => {
mockUsersGet.mockResolvedValue({ data: {} })
const nickname = await userNickname("user-456")
expect(nickname).toBeNull()
expect(mockUsersGet).toHaveBeenCalledWith({ id: "user-456", fields: "nickname" })
})
it("should return null and log error on exception", async () => {
const error = new Error("API error")
mockUsersGet.mockRejectedValue(error)
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
const nickname = await userNickname("user-789")
expect(nickname).toBeNull()
expect(consoleSpy).toHaveBeenCalledWith("Error getting user nickname:", error)
expect(mockUsersGet).toHaveBeenCalledWith({ id: "user-789", fields: "nickname" })
consoleSpy.mockRestore()
})
})

View file

@ -1,14 +1,27 @@
import { Client, LogLevel } from "@notionhq/client"
export async function collectEmail(req, res): Promise<void> {
console.log("collecting waitlist emails")
try {
const { email, userId, username } = req.body
const notion = new Client({
let notionClient: Client | null = null
export function getNotionClient(): Client {
if (!notionClient) {
notionClient = new Client({
auth: process.env.NOTION_API_TOKEN,
logLevel: LogLevel.DEBUG,
})
// const email: string = req.query.email as string
}
return notionClient
}
// For testing purposes
export function setNotionClient(client: Client | null) {
notionClient = client
}
export async function collectEmail(req: any, res: any): Promise<void> {
console.log("collecting waitlist emails")
try {
const { email, userId, username } = req.body
const notion = getNotionClient()
await notion.pages.create({
parent: {
@ -34,7 +47,6 @@ export async function collectEmail(req, res): Promise<void> {
],
},
github_id: {
// What is a notion text type?
rich_text: [
{
text: {
@ -46,7 +58,7 @@ export async function collectEmail(req, res): Promise<void> {
},
})
res.status(200).send("Email collected successfully.")
} catch (error) {
} catch (error: any) {
console.log(`Error in /cfapi/collect-email: ${error}`)
console.log(`Error message: ${error.message}`)
console.log(`Error stack: ${error.stack}`)

View file

@ -29,6 +29,7 @@ import {
createAppInstallation,
createRepositoryMember,
getAppInstallationByInstalltionId,
prisma,
upsertRepository,
} from "@codeflash-ai/common"
import { AuthorizedUserReq } from "types.js"
@ -40,7 +41,176 @@ import {
} from "../github/optimization_approval.js"
import { registerRepositoryAndMember } from "./utils/github-repo-setup.js"
const prisma = new PrismaClient()
// Define a comprehensive interface for PR title and body generation
export interface PrContentBuilder {
buildResultHeader: typeof buildResultHeader
buildBenchmarkInfo: typeof buildBenchmarkInfo
buildResultDetails: typeof buildResultDetails
buildResultTestReport: typeof buildResultTestReport
buildResultFooter: typeof buildResultFooter
buildPrTitle: typeof buildPrTitle
}
// Create a standalone PR title and body function with injectable dependencies
export function createStandalonePRTitleAndBody(
prCommentFields: PrCommentFields,
existingTests: string,
generatedTests: string,
coverage_message: string,
newBranchName: string,
builder: PrContentBuilder,
): { title: string; body: string } {
const prCommentHeader = builder.buildResultHeader(prCommentFields)
const benchmarkInfo =
prCommentFields.benchmark_details && prCommentFields.benchmark_details.length > 0
? builder.buildBenchmarkInfo(prCommentFields)
: ""
const prCommentBody = builder.buildResultDetails(prCommentFields)
const prCommentTestReport = builder.buildResultTestReport(
prCommentFields,
existingTests,
generatedTests,
coverage_message,
)
const prCommentFooter = builder.buildResultFooter(newBranchName)
const title: string = builder.buildPrTitle(
prCommentFields.function_name,
prCommentFields.speedup_pct,
prCommentFields.speedup_x,
)
const body: string = benchmarkInfo
? `${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
: `${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
return { title, body }
}
// Create a wrapper function for createNewPullRequest to match test expectations
export async function createStandalonePullRequest(
installationOctokit: any,
owner: string,
repo: string,
title: string,
body: string,
newBranchName: string,
baseBranch: string,
) {
return await createNewPullRequest(
installationOctokit,
owner,
repo,
title,
body,
newBranchName,
baseBranch,
)
}
// Dependencies interface for easier testing
export interface CreatePrDependencies {
prisma: PrismaClient
userNickname: typeof userNickname
getInstallationOctokitByOwner: typeof getInstallationOctokitByOwner
isUserCollaborator: typeof isUserCollaborator
requiresApproval: typeof requiresApproval
requestApproval: typeof requestApproval
triggerCreatePr: typeof triggerCreatePr
posthog: typeof posthog
isDiffContentsWellFormed: typeof isDiffContentsWellFormed
githubApp: typeof githubApp
}
// Dependencies for triggerCreatePr
export interface TriggerCreatePrDependencies {
prisma: PrismaClient
fileDiffsToMap: typeof fileDiffsToMap
buildPrTitle: typeof buildPrTitle
createNewBranchFromDiffContents: typeof createNewBranchFromDiffContents
createStandalonePullRequest: typeof createStandalonePullRequest
addLabelToPullRequest: typeof addLabelToPullRequest
assignReviewer: typeof assignReviewer
posthog: typeof posthog
createStandalonePRTitleAndBody: typeof createStandalonePRTitleAndBody
prContentBuilder: PrContentBuilder
}
// Default dependencies
const defaultPrContentBuilder: PrContentBuilder = {
buildResultHeader,
buildBenchmarkInfo,
buildResultDetails,
buildResultTestReport,
buildResultFooter,
buildPrTitle,
}
let dependencies: CreatePrDependencies = {
prisma: new PrismaClient(),
userNickname,
getInstallationOctokitByOwner,
isUserCollaborator,
requiresApproval,
requestApproval,
triggerCreatePr,
posthog,
isDiffContentsWellFormed,
githubApp,
}
let triggerCreatePrDeps: TriggerCreatePrDependencies = {
prisma: new PrismaClient(),
fileDiffsToMap,
buildPrTitle,
createNewBranchFromDiffContents,
createStandalonePullRequest,
addLabelToPullRequest,
assignReviewer,
posthog,
createStandalonePRTitleAndBody,
prContentBuilder: defaultPrContentBuilder,
}
// For testing - allow dependency injection
export function setCreatePrDependencies(deps: Partial<CreatePrDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetCreatePrDependencies() {
dependencies = {
prisma: new PrismaClient(),
userNickname,
getInstallationOctokitByOwner,
isUserCollaborator,
requiresApproval,
requestApproval,
triggerCreatePr,
posthog,
isDiffContentsWellFormed,
githubApp,
}
}
export function setTriggerCreatePrDependencies(deps: Partial<TriggerCreatePrDependencies>) {
triggerCreatePrDeps = { ...triggerCreatePrDeps, ...deps }
}
export function resetTriggerCreatePrDependencies() {
triggerCreatePrDeps = {
prisma: new PrismaClient(),
fileDiffsToMap,
buildPrTitle,
createNewBranchFromDiffContents,
createStandalonePullRequest,
addLabelToPullRequest,
assignReviewer,
posthog,
createStandalonePRTitleAndBody,
prContentBuilder: defaultPrContentBuilder,
}
}
export async function createPr(req: Request, res: Response) {
try {
@ -56,31 +226,38 @@ export async function createPr(req: Request, res: Response) {
} = req.body
const userId = (req as any).userId
//traceId is optional to allow for backwards compatibility, can make this required in the future
const traceId = req.body.traceId || ""
//coveragePct is optional to allow for backwards compatibility, can make this required in the future
if (!repo || !owner || !baseBranch || !isDiffContentsWellFormed(diffContents)) {
if (!repo || !owner || !baseBranch || !dependencies.isDiffContentsWellFormed(diffContents)) {
res.status(400).send("Missing or malformed fields")
return
}
const nickname: string | null = await userNickname(userId)
const nickname: string | null = await dependencies.userNickname(userId)
if (nickname == null) {
res.status(401).json({ error: "Unauthorized" }) // Error getting user nickname
res.status(401).json({ error: "Unauthorized" })
return
}
const installationOctokit = await getInstallationOctokitByOwner(githubApp, owner, repo)
const installationOctokit = await dependencies.getInstallationOctokitByOwner(
dependencies.githubApp,
owner,
repo,
)
if (installationOctokit instanceof Error) {
res.status(401).send(installationOctokit.message)
return
}
const isCollaborator = await isUserCollaborator(installationOctokit, owner, repo, nickname)
const isCollaborator = await dependencies.isUserCollaborator(
installationOctokit,
owner,
repo,
nickname,
)
if (!isCollaborator) {
console.log(`${nickname} is not a collaborator on ${owner}/${repo}`)
res.status(401).json({ error: "Unauthorized" }) // User is not a collaborator
res.status(401).json({ error: "Unauthorized" })
return
}
// TODO: Remove this background upsert logic after ensuring all old repositories have been saved.
@ -91,9 +268,8 @@ export async function createPr(req: Request, res: Response) {
Sentry.captureException(err)
})
// Now check if approval is required
if (traceId && requiresApproval(owner, repo)) {
// Check existing approval status
const optimization = await prisma.optimization_features.findUnique({
if (traceId && dependencies.requiresApproval(owner, repo)) {
const optimization = await dependencies.prisma.optimization_features.findUnique({
where: { trace_id: traceId },
select: {
approval_required: true,
@ -101,7 +277,6 @@ export async function createPr(req: Request, res: Response) {
},
})
// Handle previously rejected requests
if (optimization?.approval_status === "rejected") {
return res.status(403).json({
status: "rejected",
@ -109,10 +284,9 @@ export async function createPr(req: Request, res: Response) {
})
}
// Handle approved requests
if (optimization?.approval_status === "approved") {
console.log(`Request ${traceId} was previously approved, continuing with PR creation`)
const prNumber = await triggerCreatePr(
const prNumber = await dependencies.triggerCreatePr(
owner,
repo,
baseBranch,
@ -132,10 +306,7 @@ export async function createPr(req: Request, res: Response) {
} else {
return res.status(500).send("Error creating pull request")
}
}
// Handle new or pending requests
else {
// Store request data for later processing
} else {
const requestData = {
type: "create-pr",
owner,
@ -149,8 +320,7 @@ export async function createPr(req: Request, res: Response) {
userId,
}
// Request approval
await requestApproval(
await dependencies.requestApproval(
traceId,
owner,
repo,
@ -167,8 +337,7 @@ export async function createPr(req: Request, res: Response) {
}
}
// No approval required, proceed with PR creation
const prNumber = await triggerCreatePr(
const prNumber = await dependencies.triggerCreatePr(
owner,
repo,
baseBranch,
@ -228,7 +397,7 @@ export async function createPr(req: Request, res: Response) {
if (error instanceof Error) {
console.log(`Error message: ${error.message}`)
console.log(`Error stack: ${error.stack}`)
posthog.capture({
dependencies.posthog.capture({
distinctId: (req as any).userId,
event: `cfapi-create-pr-failed-error-creating-standalone-pr`,
properties: {
@ -257,23 +426,21 @@ export async function triggerCreatePr(
traceId = "",
): Promise<number> {
try {
// User checks are already done in createPr
const diffContentsMap: Map<string, FileDiffContent> = fileDiffsToMap(diffContents)
const diffContentsMap: Map<string, FileDiffContent> =
triggerCreatePrDeps.fileDiffsToMap(diffContents)
// Generate a Base36-encoded timestamp
const timestamp = Date.now() // Current timestamp in milliseconds
const encodedTimestamp = timestamp.toString(36) // e.g., 'kf12oi0'
const timestamp = Date.now()
const encodedTimestamp = timestamp.toString(36)
// If you change this, please also change the regex in github-app.ts in the pull_request.closed event
const newBranchName = `codeflash/optimize-${prCommentFields.function_name}-${encodedTimestamp}`
const commitMessage =
buildPrTitle(
triggerCreatePrDeps.buildPrTitle(
prCommentFields.function_name,
prCommentFields.speedup_pct,
prCommentFields.speedup_x,
) + `\n${prCommentFields.optimization_explanation}`
const branchCreated = await createNewBranchFromDiffContents(
const branchCreated = await triggerCreatePrDeps.createNewBranchFromDiffContents(
installationOctokit,
owner,
repo,
@ -287,15 +454,17 @@ export async function triggerCreatePr(
throw new Error(`Failed to create branch ${newBranchName}`)
}
let { title, body } = getStandalonePRTitleAndBody(
// Use the injectable function instead of the hardcoded one
const { title, body } = triggerCreatePrDeps.createStandalonePRTitleAndBody(
prCommentFields,
existingTests,
generatedTests,
coverage_message,
newBranchName,
triggerCreatePrDeps.prContentBuilder,
)
// Open a new standalone Codeflash PR (that doesn't reference an original PR, likely just to main)
const newPrData = await createNewPullRequest(
const newPrData = await triggerCreatePrDeps.createStandalonePullRequest(
installationOctokit,
owner,
repo,
@ -304,8 +473,9 @@ export async function triggerCreatePr(
newBranchName,
baseBranch,
)
try {
await prisma.optimization_events.update({
await triggerCreatePrDeps.prisma.optimization_events.update({
where: { trace_id: traceId },
data: {
pr_id: String(newPrData.data.id),
@ -316,14 +486,23 @@ export async function triggerCreatePr(
} catch (eventError) {
console.error("Failed to update optimization event:", eventError)
}
await addLabelToPullRequest(installationOctokit, owner, repo, newPrData.data.number)
// Assign the user who initiated the create-pr as the reviewer
await assignReviewer(installationOctokit, owner, repo, newPrData.data.number, nickname)
await triggerCreatePrDeps.addLabelToPullRequest(
installationOctokit,
owner,
repo,
newPrData.data.number,
)
await triggerCreatePrDeps.assignReviewer(
installationOctokit,
owner,
repo,
newPrData.data.number,
nickname,
)
// Respond with the new PR details
console.log(`Created new PR #${newPrData.data.number} with branch ${newPrData.data.head.ref}`)
posthog.capture({
triggerCreatePrDeps.posthog.capture({
distinctId: userId,
event: `cfapi-create-pr-success-standalone-pr-created`,
properties: {
@ -336,7 +515,7 @@ export async function triggerCreatePr(
})
if (traceId !== "") {
let pull_request_db = await prisma.optimization_features.findUnique({
let pull_request_db = await triggerCreatePrDeps.prisma.optimization_features.findUnique({
where: {
trace_id: traceId,
},
@ -346,14 +525,13 @@ export async function triggerCreatePr(
})
if (pull_request_db) {
// the trace_id is not in the database then ignore it, because it should already exist by this stage.
if (pull_request_db.pull_request === null || pull_request_db.pull_request === undefined) {
pull_request_db.pull_request = {}
}
;(pull_request_db.pull_request as any).new_pr_url = newPrData.data.html_url
await prisma.optimization_features.update({
await triggerCreatePrDeps.prisma.optimization_features.update({
where: {
trace_id: traceId,
},
@ -376,17 +554,14 @@ export async function addRepositoryManually(req: AuthorizedUserReq, res: Respons
try {
const { repositories_added, installation } = req.body
// Check if required fields are missing
if (!repositories_added || !installation?.id) {
res.status(400).send("Missing required fields")
return
}
// Check if the installation exists, if not, create it
const installationExists = await getAppInstallationByInstalltionId(installation.id)
if (!installationExists) {
// Create the installation if it doesn't exist
await createAppInstallation({
installation_id: installation.id,
account_id: installation.account.id,
@ -396,10 +571,8 @@ export async function addRepositoryManually(req: AuthorizedUserReq, res: Respons
console.log(`Installation created for ID: ${installation.id}`)
}
// Process each repository in the list of added repositories
for (const repo of repositories_added) {
try {
// Upsert logic for repository creation or update
await prisma.repositories.upsert({
where: { github_repo_id: String(repo.id) },
update: {

View file

@ -1,47 +1,98 @@
import { userNickname } from "../auth0-mgmt.js"
import { getInstallationOctokitByOwner, isUserCollaborator } from "../github/github-utils.js"
import { githubApp } from "../github/github-app.js"
import { Request, Response } from "express"
import { AuthorizedUserReq } from "types.js"
// @ts-ignore
export async function isGitHubAppInstalled(req, res): Promise<void> {
const { owner, repo } = req.query
if (!owner || !repo) {
return res.status(400).send("Missing owner or repo query parameters")
// Dependencies interface for easier testing
export interface IsGitHubAppInstalledDependencies {
userNickname: typeof userNickname
getInstallationOctokitByOwner: typeof getInstallationOctokitByOwner
isUserCollaborator: typeof isUserCollaborator
githubApp: typeof githubApp
}
// Default dependencies
let dependencies: IsGitHubAppInstalledDependencies = {
userNickname,
getInstallationOctokitByOwner,
isUserCollaborator,
githubApp,
}
// For testing - allow dependency injection
export function setIsGitHubAppInstalledDependencies(
deps: Partial<IsGitHubAppInstalledDependencies>,
) {
dependencies = { ...dependencies, ...deps }
}
export function resetIsGitHubAppInstalledDependencies() {
dependencies = {
userNickname,
getInstallationOctokitByOwner,
isUserCollaborator,
githubApp,
}
}
export async function isGitHubAppInstalled(req: Request, res: Response): Promise<void> {
const { owner, repo } = req.query
if (!owner || !repo) {
res.status(400).send("Missing owner or repo query parameters")
return
}
const ownerStr = String(owner).trim()
const repoStr = String(repo).trim()
if (ownerStr === "" || repoStr === "") {
return res.status(400).send("Invalid owner or repo query parameters")
}
const nickname = await userNickname(req.userId)
if (nickname == null) {
return res.status(401).json({ error: "Unauthorized" }) // Error getting user nickname
res.status(400).send("Invalid owner or repo query parameters")
return
}
try {
const installationOctokit = await getInstallationOctokitByOwner(githubApp, ownerStr, repoStr)
if (installationOctokit instanceof Error) {
return res.status(401).send(installationOctokit.message)
const nickname = await dependencies.userNickname((req as AuthorizedUserReq).userId)
if (nickname == null) {
res.status(401).send("Unauthorized") // Error getting user nickname
return
}
const isCollaborator = await isUserCollaborator(
const installationOctokit = await dependencies.getInstallationOctokitByOwner(
dependencies.githubApp,
ownerStr,
repoStr,
)
if (installationOctokit instanceof Error) {
res.status(401).send(installationOctokit.message)
return
}
const isCollaborator = await dependencies.isUserCollaborator(
installationOctokit,
ownerStr,
repoStr,
nickname,
)
if (!isCollaborator) {
return res
res
.status(403)
.send(
`The authenticated user is not a collaborator on the repository ${ownerStr}/${repoStr}`,
)
return
}
res.json(true)
} catch (error: any) {
if (error.status === 404) {
return res
.status(404)
.send(`GitHub App is not installed on the repository ${ownerStr}/${repoStr}`)
res.status(404).send(`GitHub App is not installed on the repository ${ownerStr}/${repoStr}`)
return
}
console.error("Error checking GitHub App installation or collaborator status:", error)
res.status(500).send("Error checking GitHub App installation or collaborator status")
}

View file

@ -1,25 +1,58 @@
import { prisma } from "@codeflash-ai/common"
import { Request, Response } from "express"
export async function optimizationSuccess(req: any, res: any): Promise<void> {
// Dependencies interface for easier testing
export interface OptimizationSuccessDependencies {
prisma: {
optimization_events: {
updateMany: (params: any) => Promise<{ count: number }>
}
}
}
// Default dependencies
let dependencies: OptimizationSuccessDependencies = {
prisma,
}
// For testing - allow dependency injection
export function setOptimizationSuccessDependencies(deps: Partial<OptimizationSuccessDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetOptimizationSuccessDependencies() {
dependencies = {
prisma,
}
}
export async function optimizationSuccess(req: Request, res: Response): Promise<void> {
const { trace_id, is_optimization_found } = req.body
if (typeof trace_id === "undefined" || typeof is_optimization_found !== "boolean") {
return res
// Fix validation to handle null values properly
if (trace_id == null || typeof is_optimization_found !== "boolean") {
res
.status(400)
.json({ error: "Invalid input: trace_id and is_optimization_found(boolean) are required." })
return
}
try {
const result = await prisma.optimization_events.updateMany({
const result = await dependencies.prisma.optimization_events.updateMany({
where: { trace_id: trace_id },
data: { is_optimization_found: is_optimization_found },
})
if (result.count === 0) {
return res.status(404).json({ error: "Optimization event not found." })
res.status(404).json({ error: "Optimization event not found." })
return
}
return res.status(200).json({ message: "Optimization status updated." })
res.status(200).json({ message: "Optimization status updated." })
return
} catch (error) {
console.error("Error in markOptimizationSuccess:", error)
return res.status(500).json({ error: "Internal server error." })
res.status(500).json({ error: "Internal server error." })
return
}
}

View file

@ -3,14 +3,54 @@ import * as crypto from "crypto"
import { posthog } from "../analytics.js"
import { processReaction } from "../github/optimization_approval.js"
import * as Sentry from "@sentry/node"
const SLACK_SIGNING_SECRET = process.env.SLACK_SIGNING_SECRET
// Dependencies interface for easier testing
export interface HandleSlackEventsDependencies {
crypto: typeof crypto
posthog: typeof posthog
processReaction: typeof processReaction
Sentry: typeof Sentry
getSlackSigningSecret: () => string | undefined
getCurrentTime: () => number
console: typeof console
}
// Default dependencies
let dependencies: HandleSlackEventsDependencies = {
crypto,
posthog,
processReaction,
Sentry,
getSlackSigningSecret: () => process.env.SLACK_SIGNING_SECRET,
getCurrentTime: () => Math.floor(Date.now() / 1000),
console,
}
// For testing - allow dependency injection
export function setHandleSlackEventsDependencies(deps: Partial<HandleSlackEventsDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetHandleSlackEventsDependencies() {
dependencies = {
crypto,
posthog,
processReaction,
Sentry,
getSlackSigningSecret: () => process.env.SLACK_SIGNING_SECRET,
getCurrentTime: () => Math.floor(Date.now() / 1000),
console,
}
}
/**
* Verify that the request is coming from Slack
*/
function verifySlackRequest(req: Request): boolean {
export function verifySlackRequest(req: Request): boolean {
const SLACK_SIGNING_SECRET = dependencies.getSlackSigningSecret()
if (!SLACK_SIGNING_SECRET) {
console.warn("SLACK_SIGNING_SECRET not configured. Skipping request verification.")
dependencies.console.warn("SLACK_SIGNING_SECRET not configured. Skipping request verification.")
return true
}
@ -22,7 +62,7 @@ function verifySlackRequest(req: Request): boolean {
}
// Prevent replay attacks - reject requests older than 5 minutes
const currentTime = Math.floor(Date.now() / 1000)
const currentTime = dependencies.getCurrentTime()
if (Math.abs(currentTime - parseInt(slackTimestamp)) > 300) {
return false
}
@ -31,10 +71,10 @@ function verifySlackRequest(req: Request): boolean {
const baseString = `v0:${slackTimestamp}:${requestBody}`
const hmac = crypto.createHmac("sha256", SLACK_SIGNING_SECRET)
const hmac = dependencies.crypto.createHmac("sha256", SLACK_SIGNING_SECRET)
const signature = "v0=" + hmac.update(baseString).digest("hex")
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(slackSignature))
return dependencies.crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(slackSignature))
}
/**
@ -44,13 +84,13 @@ export async function handleSlackEvents(req: Request, res: Response) {
try {
// Verify the request is from Slack
if (!verifySlackRequest(req)) {
console.error("Failed to verify Slack request")
dependencies.console.error("Failed to verify Slack request")
return res.status(403).send("Invalid request")
}
// Handle URL verification (required when setting up events)
if (req.body.type === "url_verification") {
console.log("Handling Slack URL verification challenge")
dependencies.console.log("Handling Slack URL verification challenge")
return res.json({ challenge: req.body.challenge })
}
@ -59,13 +99,13 @@ export async function handleSlackEvents(req: Request, res: Response) {
const event = req.body.event
if (event) {
console.log(`Processing Slack event: ${event.type}`)
dependencies.console.log(`Processing Slack event: ${event.type}`)
if (event.type === "reaction_added") {
const processed = await processReaction(event)
const processed = await dependencies.processReaction(event)
if (processed) {
posthog.capture({
dependencies.posthog.capture({
distinctId: "system",
event: "slack-approval-reaction-processed",
properties: {
@ -77,15 +117,15 @@ export async function handleSlackEvents(req: Request, res: Response) {
}
}
} catch (error) {
console.error(`Error handling Slack event: ${error}`)
dependencies.console.error(`Error handling Slack event: ${error}`)
// If we haven't sent a response yet, send an error
if (!res.headersSent) {
res.status(500).send("Error processing event")
}
Sentry.captureException(error)
dependencies.Sentry.captureException(error)
// Log to monitoring
posthog.capture({
dependencies.posthog.capture({
distinctId: "system",
event: "slack-optimization-approval-error",
properties: {

View file

@ -18,11 +18,46 @@ interface SubscriptionData {
current_period_end: Date
}
// Dependencies interface for easier testing
export interface StripeWebhookDependencies {
stripe: typeof stripe
prisma: typeof prisma
Sentry: typeof Sentry
getWebhookSecret: () => string | undefined
}
// Default dependencies
let dependencies: StripeWebhookDependencies = {
stripe,
prisma,
Sentry,
getWebhookSecret: () => process.env.STRIPE_WEBHOOK_SECRET,
}
// For testing - allow dependency injection
export function setStripeWebhookDependencies(deps: Partial<StripeWebhookDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetStripeWebhookDependencies() {
dependencies = {
stripe,
prisma,
Sentry,
getWebhookSecret: () => process.env.STRIPE_WEBHOOK_SECRET,
}
}
export async function stripeWebhookHandler(req: Request, res: Response) {
const sig = req.headers["stripe-signature"]
try {
const event = stripe.webhooks.constructEvent(req.body, sig!, process.env.STRIPE_WEBHOOK_SECRET!)
const webhookSecret = dependencies.getWebhookSecret()
if (!webhookSecret) {
throw new Error("STRIPE_WEBHOOK_SECRET is not configured")
}
const event = dependencies.stripe.webhooks.constructEvent(req.body, sig!, webhookSecret)
console.log(`Processing Stripe webhook: ${event.type}`)
@ -58,12 +93,12 @@ export async function stripeWebhookHandler(req: Request, res: Response) {
res.json({ received: true })
} catch (err: any) {
console.error("Webhook Error:", err.message)
Sentry.captureException(err)
dependencies.Sentry.captureException(err)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
}
async function handleCheckoutCompleted(session: any) {
export async function handleCheckoutCompleted(session: any) {
// Extract userId from the session metadata
const userId = session.metadata?.userId
if (!userId) {
@ -77,35 +112,35 @@ async function handleCheckoutCompleted(session: any) {
if (session.mode === "subscription" && session.subscription) {
// Update the subscription with userId metadata if missing
try {
await stripe.subscriptions.update(session.subscription, {
await dependencies.stripe.subscriptions.update(session.subscription, {
metadata: { userId },
})
// Also update customer metadata
if (session.customer) {
await stripe.customers.update(session.customer, {
await dependencies.stripe.customers.update(session.customer, {
metadata: { userId },
})
}
// Now fetch and process the subscription
const subscription = await stripe.subscriptions.retrieve(session.subscription)
const subscription = await dependencies.stripe.subscriptions.retrieve(session.subscription)
await handleSubscriptionUpdate(subscription)
} catch (error) {
console.error("Error processing checkout session:", error)
Sentry.captureException(error)
dependencies.Sentry.captureException(error)
}
}
}
async function handleSubscriptionUpdate(subscription: any) {
export async function handleSubscriptionUpdate(subscription: any) {
// First check subscription metadata
let userId = subscription.metadata?.userId
// If not in subscription metadata, check customer metadata
if (!userId && subscription.customer) {
try {
const customer = await stripe.customers.retrieve(subscription.customer as string)
const customer = await dependencies.stripe.customers.retrieve(subscription.customer as string)
if ("metadata" in customer) {
userId = customer.metadata?.userId
}
@ -127,7 +162,7 @@ async function handleSubscriptionUpdate(subscription: any) {
try {
const priceId = subscription.items.data[0].price.id
const price = await stripe.prices.retrieve(priceId)
const price = await dependencies.stripe.prices.retrieve(priceId)
console.log(`Updating subscription for user ${userId}`)
@ -155,7 +190,8 @@ async function handleSubscriptionUpdate(subscription: any) {
updateData.cancellation_request_date = null
}
}
await prisma.subscriptions.upsert({
await dependencies.prisma.subscriptions.upsert({
where: { user_id: userId },
create: {
user_id: userId,
@ -178,23 +214,23 @@ async function handleSubscriptionUpdate(subscription: any) {
console.log(`Successfully updated subscription for user ${userId}`)
} catch (error) {
console.error("Error updating subscription:", error)
Sentry.captureException(error)
dependencies.Sentry.captureException(error)
}
}
async function handleSubscriptionCancellation(subscription: any) {
export async function handleSubscriptionCancellation(subscription: any) {
const userId = subscription.metadata.userId
if (!userId) return
try {
await prisma.subscriptions.update({
await dependencies.prisma.subscriptions.update({
where: { user_id: userId },
data: {
subscription_status: "canceled",
plan_type: "free",
optimizations_limit: 100,
stripe_subscription_id: null,
cancel_at_period_end: false, // The subscription is fully canceled, not pending cancellation
cancel_at_period_end: false,
cancellation_request_date: null,
},
})
@ -202,19 +238,19 @@ async function handleSubscriptionCancellation(subscription: any) {
console.log(`Cancelled subscription for user ${userId}`)
} catch (error) {
console.error("Error cancelling subscription:", error)
Sentry.captureException(error)
dependencies.Sentry.captureException(error)
}
}
async function handleFailedPayment(invoice: any) {
export async function handleFailedPayment(invoice: any) {
if (!invoice.subscription) return
try {
const subscription = await stripe.subscriptions.retrieve(invoice.subscription)
const subscription = await dependencies.stripe.subscriptions.retrieve(invoice.subscription)
const userId = subscription.metadata.userId
if (!userId) return
await prisma.subscriptions.update({
await dependencies.prisma.subscriptions.update({
where: { user_id: userId },
data: {
subscription_status: "past_due",
@ -224,6 +260,6 @@ async function handleFailedPayment(invoice: any) {
console.log(`Updated subscription status to past_due for user ${userId}`)
} catch (error) {
console.error("Error handling failed payment:", error)
Sentry.captureException(error)
dependencies.Sentry.captureException(error)
}
}

View file

@ -6,6 +6,42 @@ import {
} from "@codeflash-ai/common"
import * as Sentry from "@sentry/node"
// Dependencies interface for easier testing
export interface SubscriptionDependencies {
prisma: {
subscriptions: {
findUnique: (params: any) => Promise<any>
}
}
createCheckoutSession: typeof createCheckoutSession
cancelStripeSubscription: typeof cancelStripeSubscription
Sentry: {
captureException: (error: any) => void
}
}
// Default dependencies
let dependencies: SubscriptionDependencies = {
prisma,
createCheckoutSession,
cancelStripeSubscription,
Sentry,
}
// For testing - allow dependency injection
export function setSubscriptionDependencies(deps: Partial<SubscriptionDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetSubscriptionDependencies() {
dependencies = {
prisma,
createCheckoutSession,
cancelStripeSubscription,
Sentry,
}
}
// Get a user's subscription details
export async function getSubscription(req: Request, res: Response, next: NextFunction) {
const userId = req.query.userId as string
@ -16,7 +52,7 @@ export async function getSubscription(req: Request, res: Response, next: NextFun
try {
// Get subscription with usage data
const subscription = await prisma.subscriptions.findUnique({
const subscription = await dependencies.prisma.subscriptions.findUnique({
where: { user_id: userId },
})
@ -33,7 +69,7 @@ export async function getSubscription(req: Request, res: Response, next: NextFun
})
} catch (error) {
console.error("Error getting subscription:", error)
Sentry.captureException(error)
dependencies.Sentry.captureException(error)
next(error) // Pass errors to the error handler
}
}
@ -48,7 +84,7 @@ export async function createCheckout(req: Request, res: Response, next: NextFunc
try {
// Use the common function with options object
const checkoutUrl = await createCheckoutSession(userId, priceId, {
const checkoutUrl = await dependencies.createCheckoutSession(userId, priceId, {
successUrl: successUrl || `${process.env.WEBAPP_URL}/app/billing?success=true`,
cancelUrl: cancelUrl || `${process.env.WEBAPP_URL}/app/billing?canceled=true`,
period: period || (priceId === process.env.STRIPE_PRO_PRICE_YEARLY_ID ? "yearly" : "monthly"),
@ -57,7 +93,7 @@ export async function createCheckout(req: Request, res: Response, next: NextFunc
return res.json({ url: checkoutUrl })
} catch (error) {
console.error("Error creating checkout session:", error)
Sentry.captureException(error)
dependencies.Sentry.captureException(error)
next(error) // Pass errors to the error handler
}
}
@ -72,11 +108,11 @@ export async function cancelSubscription(req: Request, res: Response, next: Next
try {
// Use the common function for cancellation
await cancelStripeSubscription(userId)
await dependencies.cancelStripeSubscription(userId)
return res.json({ status: "canceled" })
} catch (error) {
console.error("Error canceling subscription:", error)
Sentry.captureException(error)
dependencies.Sentry.captureException(error)
next(error) // Pass errors to the error handler
}
}

View file

@ -9,11 +9,10 @@ import {
createNewBranchFromDiffContents,
} from "../github/create-pr-from-diffcontents.js"
import { posthog } from "../analytics.js"
import suggester from "@codeflash-ai/code-suggester"
import { type FileDiffContent } from "@codeflash-ai/code-suggester/build/src/types.js"
import { PrismaClient } from "@prisma/client"
import { sendSlackMessage } from "../github/slack_util.js"
import { Request, Response } from "express"
import { Response } from "express"
import { AnyOctokit, AuthorizedUserReq, PullRequestDB } from "../types.js"
import {
requestApproval,
@ -23,10 +22,74 @@ import {
} from "../github/optimization_approval.js"
import { registerRepositoryAndMember } from "./utils/github-repo-setup.js"
interface CustomRequest extends Request {
userId?: string
// Dependencies interface for easier testing
export interface SuggestPrChangesDependencies {
prisma: PrismaClient
userNickname: typeof userNickname
getInstallationOctokitByOwner: typeof getInstallationOctokitByOwner
isUserCollaborator: typeof isUserCollaborator
requiresApproval: typeof requiresApproval
requestApproval: typeof requestApproval
posthog: typeof posthog
githubApp: typeof githubApp
isDiffContentsWellFormed: typeof isDiffContentsWellFormed
fileDiffsToMap: typeof fileDiffsToMap
determineValidHunks: typeof determineValidHunks
buildDependentPrTitle: typeof buildDependentPrTitle
buildPrCommentBody: typeof buildPrCommentBody
createNewBranchFromDiffContents: typeof createNewBranchFromDiffContents
createDependentPullRequest: typeof createDependentPullRequest
sendSlackMessage: typeof sendSlackMessage
updateOptimizationEvent: typeof updateOptimizationEvent
}
// Default dependencies
let dependencies: SuggestPrChangesDependencies = {
prisma: new PrismaClient(),
userNickname,
getInstallationOctokitByOwner,
isUserCollaborator,
requiresApproval,
requestApproval,
posthog,
githubApp,
isDiffContentsWellFormed,
fileDiffsToMap,
determineValidHunks,
buildDependentPrTitle,
buildPrCommentBody,
createNewBranchFromDiffContents,
createDependentPullRequest,
sendSlackMessage,
updateOptimizationEvent,
}
// For testing - allow dependency injection
export function setSuggestPrChangesDependencies(deps: Partial<SuggestPrChangesDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetSuggestPrChangesDependencies() {
dependencies = {
prisma: new PrismaClient(),
userNickname,
getInstallationOctokitByOwner,
isUserCollaborator,
requiresApproval,
requestApproval,
posthog,
githubApp,
isDiffContentsWellFormed,
fileDiffsToMap,
determineValidHunks,
buildDependentPrTitle,
buildPrCommentBody,
createNewBranchFromDiffContents,
createDependentPullRequest,
sendSlackMessage,
updateOptimizationEvent,
}
}
const prisma = new PrismaClient()
const slackNotificationConfig = {
"Future-House": ["aviary", "paper-qa"],
@ -36,11 +99,12 @@ const slackNotificationConfig = {
roboflow: ["inference"],
gdsfactory: ["gdsfactory"],
}
// Utility function to update optimization event in the database
async function updateOptimizationEvent(traceId: string, prId?: string) {
export async function updateOptimizationEvent(traceId: string, prId?: string) {
if (traceId !== "") {
try {
await prisma.optimization_events.update({
await dependencies.prisma.optimization_events.update({
where: { trace_id: traceId },
data: {
...(prId ? { pr_id: String(prId) } : {}),
@ -53,6 +117,7 @@ async function updateOptimizationEvent(traceId: string, prId?: string) {
}
}
}
export async function suggestPrChanges(
req: AuthorizedUserReq,
res: Response,
@ -69,24 +134,33 @@ export async function suggestPrChanges(
coverage_message,
} = req.body
const userId = req.userId
//traceId is optional to allow for backwards compatibility, can make this required in the future
const traceId = req.body.traceId || ""
console.log(`traceId: ${traceId}`)
if (!repo || !owner || !pullNumber || !isDiffContentsWellFormed(diffContents)) {
if (!repo || !owner || !pullNumber || !dependencies.isDiffContentsWellFormed(diffContents)) {
return res.status(400).send("Missing or malformed fields")
}
const nickname = await userNickname(userId)
const nickname = await dependencies.userNickname(userId)
if (nickname == null) {
return res.status(401).json({ error: "Unauthorized" }) // Error getting user nickname
}
const installationOctokit = await getInstallationOctokitByOwner(githubApp, owner, repo)
const installationOctokit = await dependencies.getInstallationOctokitByOwner(
dependencies.githubApp,
owner,
repo,
)
if (installationOctokit instanceof Error) {
return res.status(401).send(installationOctokit.message)
}
const isCollaborator = await isUserCollaborator(installationOctokit, owner, repo, nickname)
const isCollaborator = await dependencies.isUserCollaborator(
installationOctokit,
owner,
repo,
nickname,
)
if (!isCollaborator) {
console.log(`${nickname} is not a collaborator on ${owner}/${repo}`)
return res.status(401).json({ error: "Unauthorized" }) // User is not a collaborator
@ -100,9 +174,8 @@ export async function suggestPrChanges(
Sentry.captureException(err)
})
// Check if approval is required
if (traceId && requiresApproval(owner, repo)) {
// Check existing approval status
const optimization = await prisma.optimization_features.findUnique({
if (traceId && dependencies.requiresApproval(owner, repo)) {
const optimization = await dependencies.prisma.optimization_features.findUnique({
where: { trace_id: traceId },
select: {
approval_required: true,
@ -110,7 +183,6 @@ export async function suggestPrChanges(
},
})
// Handle previously rejected requests
if (optimization?.approval_status === "rejected") {
return res.status(403).json({
status: "rejected",
@ -118,7 +190,6 @@ export async function suggestPrChanges(
})
}
// Handle approved requests
if (optimization?.approval_status === "approved") {
console.log(`Request ${traceId} was previously approved, continuing with PR suggestion`)
const result = await triggerSuggestPrChanges(
@ -137,10 +208,7 @@ export async function suggestPrChanges(
)
return res.json(result)
}
// Handle new or pending requests
else {
// Store request data for later processing
} else {
const requestData = {
type: "suggest-pr-changes",
owner,
@ -154,8 +222,7 @@ export async function suggestPrChanges(
userId,
}
// Request approval
await requestApproval(
await dependencies.requestApproval(
traceId,
owner,
repo,
@ -230,7 +297,7 @@ export async function suggestPrChanges(
console.log(`Error in /cfapi/suggest-pr-changes: ${error}`)
console.log(`Error message: ${error.message}`)
console.log(`Error stack: ${error.stack}`)
posthog.capture({
dependencies.posthog.capture({
distinctId: req.userId,
event: `cfapi-suggest-pr-changes-failed-error`,
properties: {
@ -242,7 +309,6 @@ export async function suggestPrChanges(
}
}
// New function to trigger suggest PR changes - keeps all the existing logic
export async function triggerSuggestPrChanges(
owner: string,
repo: string,
@ -259,10 +325,9 @@ export async function triggerSuggestPrChanges(
res?: Response,
): Promise<Response | number | null> {
try {
// Suggest changes - all the existing logic is preserved
const diffContentsMap: Map<string, FileDiffContent> = fileDiffsToMap(diffContents)
const diffContentsMap: Map<string, FileDiffContent> = dependencies.fileDiffsToMap(diffContents)
const { validHunks, invalidHunks } = await determineValidHunks(
const { validHunks, invalidHunks } = await dependencies.determineValidHunks(
installationOctokit.rest as AnyOctokit,
{ owner, repo },
pullNumber,
@ -270,16 +335,13 @@ export async function triggerSuggestPrChanges(
diffContentsMap,
)
// a timestamp format like 2024-01-31-12.59.48
const timestamp = new Date()
.toISOString()
.replace(/:/g, ".")
.replace(/\.\d+Z$/, "")
// If you change this, please also change the regex in github-app.ts in the pull_request.closed event
const newBranchName = `codeflash/optimize-pr${pullNumber}-${timestamp}`
// Get the head branch of the original pull request
const originalPrData = await installationOctokit.rest.pulls.get({
owner,
repo,
@ -287,15 +349,17 @@ export async function triggerSuggestPrChanges(
})
const baseBranch = originalPrData.data.head.ref
console.log(`Attempting to access ref for: ${owner}/${repo}, branch: ${baseBranch}`)
const commitMessage =
buildDependentPrTitle(
dependencies.buildDependentPrTitle(
prCommentFields.function_name,
prCommentFields.speedup_pct,
prCommentFields.speedup_x,
pullNumber,
baseBranch,
) + `\n${prCommentFields.optimization_explanation}`
const branchCreated = await createNewBranchFromDiffContents(
const branchCreated = await dependencies.createNewBranchFromDiffContents(
installationOctokit,
owner,
repo,
@ -304,12 +368,14 @@ export async function triggerSuggestPrChanges(
diffContentsMap,
commitMessage,
)
if (!branchCreated) {
throw new Error(`Failed to create branch ${newBranchName}`)
}
let hasMultipleHunksInSameFile = false
let hasMultipleFiles = validHunks.size > 1
for (const [filePath, hunks] of validHunks.entries()) {
if (hunks.length > 1) {
console.log(
@ -319,29 +385,25 @@ export async function triggerSuggestPrChanges(
break
}
}
if (hasMultipleFiles) {
console.log(
`Found ${validHunks.size} files with changes, using dependent PR instead of review comments`,
)
}
// Modified condition: Create a dependent PR if there are invalid hunks OR multiple valid hunks
// This addresses both the invalid hunks case and the code suggester library issues
if (
invalidHunks.size > 0 ||
validHunks.size > 1 ||
hasMultipleFiles ||
hasMultipleHunksInSameFile
) {
// we can't suggest all of the hunks for this PR, because some of them are invalid (out of scope for this PR).
// so instead, let's make a new branch, commit the changes,
// and make a new PR from that branch onto the PR's branch
console.log(
`Creating a dependent PR because there are ${invalidHunks.size > 0 ? "invalid hunks" : "multiple valid hunks"}.`,
)
console.log(`Making a new dependent PR...`)
const newPrData = await createDependentPullRequest(
const newPrData = await dependencies.createDependentPullRequest(
installationOctokit as AnyOctokit,
owner,
repo,
@ -354,18 +416,17 @@ export async function triggerSuggestPrChanges(
coverage_message,
)
// Respond with the new PR details
console.log(
`Created new dependent PR #${newPrData.data.number} from branch ${newPrData.data.head.ref}`,
)
if (slackNotificationConfig[owner as keyof typeof slackNotificationConfig]?.includes(repo)) {
await sendSlackMessage(
await dependencies.sendSlackMessage(
`new dependent PR created: ${newPrData.data.html_url} for ${owner}/${repo}`,
)
}
posthog.capture({
dependencies.posthog.capture({
distinctId: userId,
event: `cfapi-suggest-pr-changes-success-dependent-pr-created`,
properties: {
@ -376,8 +437,9 @@ export async function triggerSuggestPrChanges(
PRURL: newPrData.data.html_url,
},
})
if (traceId !== "") {
let pull_request_db = await prisma.optimization_features.findUnique({
let pull_request_db = await dependencies.prisma.optimization_features.findUnique({
where: {
trace_id: traceId,
},
@ -385,6 +447,7 @@ export async function triggerSuggestPrChanges(
pull_request: true,
},
})
if (pull_request_db) {
if (pull_request_db.pull_request === null || pull_request_db.pull_request === undefined) {
pull_request_db.pull_request = {}
@ -392,7 +455,7 @@ export async function triggerSuggestPrChanges(
;(pull_request_db as PullRequestDB).pull_request.dependent_pr_url =
newPrData.data.html_url
await prisma.optimization_features.update({
await dependencies.prisma.optimization_features.update({
where: {
trace_id: traceId,
},
@ -402,9 +465,9 @@ export async function triggerSuggestPrChanges(
})
}
}
await updateOptimizationEvent(traceId, newPrData.data.id)
// For backward compatibility
await dependencies.updateOptimizationEvent(traceId, newPrData.data.id)
if (res) {
res.json(newPrData.data.number)
return res
@ -412,10 +475,9 @@ export async function triggerSuggestPrChanges(
return newPrData.data.number
} else {
// Only use the code suggester library when there is exactly one valid hunk and no invalid hunks
console.log(`Creating unified review for a single valid hunk.`)
// Include all the information in a single PR comment
const prCommentBody = buildPrCommentBody(
const prCommentBody = dependencies.buildPrCommentBody(
prCommentFields,
existingTests,
generatedTests,
@ -423,19 +485,18 @@ export async function triggerSuggestPrChanges(
newBranchName,
{ isUnifiedReview: true, includeHeader: false, isCollapsed: true },
)
let reviewComments = []
let foundInvalidHunk = false
// Process each valid hunk entry in the Map
for (const [filePath, hunks] of validHunks.entries()) {
// For each hunk in this file
for (const hunk of hunks) {
if (hunk.oldStart < hunk.oldEnd) {
const newContent = hunk.newContent.join("\n")
const isLongDiff = newContent.length > 500
let commentBody
if (isLongDiff) {
// Create a collapsible section for long diffs
commentBody =
prCommentBody +
"\n\n" +
@ -446,15 +507,15 @@ export async function triggerSuggestPrChanges(
"\n```\n" +
"</details>"
} else {
// For smaller diffs, show them directly
commentBody = prCommentBody + "\n\n" + "```suggestion\n" + newContent + "\n```"
}
reviewComments.push({
path: filePath,
line: hunk.oldEnd,
start_line: hunk.oldStart,
side: "RIGHT",
body: commentBody, // Format as a suggestion
body: commentBody,
})
} else {
console.log(
@ -470,7 +531,6 @@ export async function triggerSuggestPrChanges(
}
}
// Add explanation comment
if (!foundInvalidHunk && reviewComments.length > 0) {
const review = await installationOctokit.rest.pulls.createReview({
owner,
@ -480,14 +540,18 @@ export async function triggerSuggestPrChanges(
event: "COMMENT",
comments: reviewComments,
})
console.log(`Added review comment to PR #${pullNumber}: ${review.data.html_url}`)
if (
slackNotificationConfig[owner as keyof typeof slackNotificationConfig]?.includes(repo)
) {
await sendSlackMessage(`Suggestions made for ${review.data.html_url} in ${owner}/${repo}`)
await dependencies.sendSlackMessage(
`Suggestions made for ${review.data.html_url} in ${owner}/${repo}`,
)
}
posthog.capture({
dependencies.posthog.capture({
distinctId: userId,
event: `cfapi-suggest-pr-changes-success-suggestions-made`,
properties: {
@ -499,7 +563,7 @@ export async function triggerSuggestPrChanges(
})
if (traceId !== "") {
let pull_request_db = await prisma.optimization_features.findUnique({
let pull_request_db = await dependencies.prisma.optimization_features.findUnique({
where: {
trace_id: traceId,
},
@ -507,8 +571,8 @@ export async function triggerSuggestPrChanges(
pull_request: true,
},
})
if (pull_request_db) {
// the trace_id is not in the database then ignore it, because it should already exist by this stage.
if (
pull_request_db.pull_request === null ||
pull_request_db.pull_request === undefined
@ -519,7 +583,7 @@ export async function triggerSuggestPrChanges(
;(pull_request_db as PullRequestDB).pull_request.review_suggestion_pr_url =
review.data.html_url
await prisma.optimization_features.update({
await dependencies.prisma.optimization_features.update({
where: {
trace_id: traceId,
},
@ -529,9 +593,9 @@ export async function triggerSuggestPrChanges(
})
}
}
await updateOptimizationEvent(traceId)
// For backward compatibility, handle the response object if provided
await dependencies.updateOptimizationEvent(traceId)
if (res) {
res.json(review.data.id)
return res
@ -539,16 +603,13 @@ export async function triggerSuggestPrChanges(
return review.data.id
} else {
// Don't create a dependent PR, just return an error response
const reason = foundInvalidHunk
? "invalid line ordering in hunks"
: "no valid review comments could be created"
console.log(`Cannot create review due to ${reason}`)
// Return a meaningful error response
if (res) {
// For backward compatibility
return res.status(422).json({
error: `Cannot create review comments due to ${reason}`,
message: "Please consider creating a dependent PR instead",
@ -564,7 +625,6 @@ export async function triggerSuggestPrChanges(
console.error(`Error message: ${error.message}`)
console.error(`Error stack: ${error.stack}`)
// For backward compatibility, handle the response object if provided
if (res) {
res.status(500).send(`Error creating pull request: ${error.message}`)
return res

View file

@ -0,0 +1,58 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals"
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
import { dirname } from "path"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const min_version = fs
.readFileSync(path.join(__dirname, "../min_supported_version.txt"), "utf8")
.trim()
describe("getUser (unit test)", () => {
beforeEach(() => {
jest.resetModules()
})
it("should respond with userId and min_version when cli_version is provided", async () => {
const { getUser } = await import("../cli-get-user")
const req = {
headers: { cli_version: "1.0.0" },
userId: "test-user-id",
} as any
const res = {
status: jest.fn().mockReturnThis(),
send: jest.fn(),
} as any
getUser(req, res)
expect(res.status).toHaveBeenCalledWith(200)
expect(res.send).toHaveBeenCalledWith({
userId: "test-user-id",
min_version: min_version,
})
})
it("should respond with userId when cli_version is unknown", async () => {
const { getUser } = await import("../cli-get-user")
const req = {
headers: {},
userId: "test-user-id",
} as any
const res = {
status: jest.fn().mockReturnThis(),
send: jest.fn(),
} as any
getUser(req, res)
expect(res.status).toHaveBeenCalledWith(200)
expect(res.send).toHaveBeenCalledWith("test-user-id")
})
})

View file

@ -0,0 +1,151 @@
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import { collectEmail, setNotionClient } from "../collect-email"
describe("collectEmail", () => {
let mockPagesCreate: jest.MockedFunction<any>
let mockNotionClient: any
let mockReq: any
let mockRes: any
beforeEach(() => {
// Create mock for pages.create
mockPagesCreate = jest.fn()
// Create mock notion client
mockNotionClient = {
pages: {
create: mockPagesCreate,
},
}
// Inject the mock
setNotionClient(mockNotionClient)
// Setup request and response mocks
mockReq = {
body: {
email: "test@example.com",
userId: "user123",
username: "testuser",
},
}
mockRes = {
status: jest.fn().mockReturnThis(),
send: jest.fn(),
}
})
afterEach(() => {
// Clean up
setNotionClient(null)
})
it("should successfully collect email and create notion page", async () => {
mockPagesCreate.mockResolvedValue({ id: "page123" })
await collectEmail(mockReq, mockRes)
expect(mockPagesCreate).toHaveBeenCalledWith({
parent: {
database_id: "test-notion-db-id", // Set in .env.test
},
properties: {
Email: {
title: [
{
text: {
content: "test@example.com",
},
},
],
},
Nickname: {
rich_text: [
{
text: {
content: "testuser",
},
},
],
},
github_id: {
rich_text: [
{
text: {
content: "user123",
},
},
],
},
},
})
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.send).toHaveBeenCalledWith("Email collected successfully.")
})
it("should handle errors and return 500 status", async () => {
const error = new Error("Notion API error")
error.stack = "Error stack trace"
mockPagesCreate.mockRejectedValue(error)
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await collectEmail(mockReq, mockRes)
expect(consoleSpy).toHaveBeenCalledWith("collecting waitlist emails")
expect(consoleSpy).toHaveBeenCalledWith(`Error in /cfapi/collect-email: ${error}`)
expect(consoleSpy).toHaveBeenCalledWith(`Error message: ${error.message}`)
expect(consoleSpy).toHaveBeenCalledWith(`Error stack: ${error.stack}`)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.send).toHaveBeenCalledWith(`Error collecting emails: ${error.message}`)
consoleSpy.mockRestore()
})
it("should handle empty request body", async () => {
mockReq.body = {}
mockPagesCreate.mockResolvedValue({ id: "page123" })
await collectEmail(mockReq, mockRes)
expect(mockPagesCreate).toHaveBeenCalledWith({
parent: {
database_id: "test-notion-db-id", // Set in .env.test
},
properties: {
Email: {
title: [
{
text: {
content: undefined,
},
},
],
},
Nickname: {
rich_text: [
{
text: {
content: undefined,
},
},
],
},
github_id: {
rich_text: [
{
text: {
content: undefined,
},
},
],
},
},
})
expect(mockRes.status).toHaveBeenCalledWith(200)
})
})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
import { healthcheck } from "../healthcheck"
import { jest, describe, it, expect } from "@jest/globals"
describe("healthcheck", () => {
it("should respond with status 200 and 'OK'", () => {
const req = {}
const res = {
status: jest.fn().mockReturnThis(),
send: jest.fn(),
}
healthcheck(req, res)
expect(res.status).toHaveBeenCalledWith(200)
expect(res.send).toHaveBeenCalledWith("OK")
})
})

View file

@ -0,0 +1,318 @@
import { jest, describe, it, expect, beforeEach, afterEach, beforeAll } from "@jest/globals"
import { Request, Response } from "express"
let isGitHubAppInstalled: typeof import("../is-github-app-installed").isGitHubAppInstalled
let setIsGitHubAppInstalledDependencies: typeof import("../is-github-app-installed").setIsGitHubAppInstalledDependencies
let resetIsGitHubAppInstalledDependencies: typeof import("../is-github-app-installed").resetIsGitHubAppInstalledDependencies
// Extend Request interface to include userId for testing
interface TestRequest extends Request {
userId: string
}
describe("isGitHubAppInstalled", () => {
beforeAll(async () => {
process.env.KEY_VAULT_NAME = "mocked-keyvault-name"
const mod = await import("../is-github-app-installed")
isGitHubAppInstalled = mod.isGitHubAppInstalled
setIsGitHubAppInstalledDependencies = mod.setIsGitHubAppInstalledDependencies
resetIsGitHubAppInstalledDependencies = mod.resetIsGitHubAppInstalledDependencies
})
let mockReq: Partial<TestRequest>
let mockRes: Partial<Response>
let mockDependencies: any
beforeEach(() => {
// Setup response mock with proper typing
mockRes = {
status: jest.fn().mockReturnThis() as any,
send: jest.fn() as any,
json: jest.fn() as any,
}
// Setup mock dependencies
mockDependencies = {
userNickname: jest.fn(),
getInstallationOctokitByOwner: jest.fn(),
isUserCollaborator: jest.fn(),
githubApp: {},
}
setIsGitHubAppInstalledDependencies(mockDependencies)
})
afterEach(() => {
resetIsGitHubAppInstalledDependencies()
jest.clearAllMocks()
})
describe("input validation", () => {
it("should return 400 when owner is missing", async () => {
mockReq = {
query: { repo: "test-repo" },
userId: "test-user-id",
}
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.send).toHaveBeenCalledWith("Missing owner or repo query parameters")
})
it("should return 400 when repo is missing", async () => {
mockReq = {
query: { owner: "test-owner" },
userId: "test-user-id",
}
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.send).toHaveBeenCalledWith("Missing owner or repo query parameters")
})
it("should return 400 when both owner and repo are missing", async () => {
mockReq = {
query: {},
userId: "test-user-id",
}
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.send).toHaveBeenCalledWith("Missing owner or repo query parameters")
})
it("should return 400 when owner is empty string", async () => {
mockReq = {
query: { owner: " ", repo: "test-repo" },
userId: "test-user-id",
}
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.send).toHaveBeenCalledWith("Invalid owner or repo query parameters")
})
it("should return 400 when repo is empty string", async () => {
mockReq = {
query: { owner: "test-owner", repo: " " },
userId: "test-user-id",
}
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.send).toHaveBeenCalledWith("Invalid owner or repo query parameters")
})
it("should trim whitespace from owner and repo", async () => {
mockReq = {
query: { owner: " test-owner ", repo: " test-repo " },
userId: "test-user-id",
}
mockDependencies.userNickname.mockResolvedValue("test-nickname")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
mockDependencies.isUserCollaborator.mockResolvedValue(true)
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockDependencies.getInstallationOctokitByOwner).toHaveBeenCalledWith(
mockDependencies.githubApp,
"test-owner",
"test-repo",
)
})
})
describe("authentication", () => {
beforeEach(() => {
mockReq = {
query: { owner: "test-owner", repo: "test-repo" },
userId: "test-user-id",
}
})
it("should return 401 when user nickname is null", async () => {
mockDependencies.userNickname.mockResolvedValue(null)
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockDependencies.userNickname).toHaveBeenCalledWith("test-user-id")
expect(mockRes.status).toHaveBeenCalledWith(401)
expect(mockRes.send).toHaveBeenCalledWith("Unauthorized")
})
it("should return 401 when getInstallationOctokitByOwner returns Error", async () => {
const error = new Error("Installation not found")
mockDependencies.userNickname.mockResolvedValue("test-nickname")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(error)
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(401)
expect(mockRes.send).toHaveBeenCalledWith("Installation not found")
})
})
describe("authorization", () => {
beforeEach(() => {
mockReq = {
query: { owner: "test-owner", repo: "test-repo" },
userId: "test-user-id",
}
mockDependencies.userNickname.mockResolvedValue("test-nickname")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
})
it("should return 403 when user is not a collaborator", async () => {
mockDependencies.isUserCollaborator.mockResolvedValue(false)
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockDependencies.isUserCollaborator).toHaveBeenCalledWith(
{},
"test-owner",
"test-repo",
"test-nickname",
)
expect(mockRes.status).toHaveBeenCalledWith(403)
expect(mockRes.send).toHaveBeenCalledWith(
"The authenticated user is not a collaborator on the repository test-owner/test-repo",
)
})
it("should return true when user is a collaborator", async () => {
mockDependencies.isUserCollaborator.mockResolvedValue(true)
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockDependencies.isUserCollaborator).toHaveBeenCalledWith(
{},
"test-owner",
"test-repo",
"test-nickname",
)
expect(mockRes.json).toHaveBeenCalledWith(true)
})
})
describe("error handling", () => {
beforeEach(() => {
mockReq = {
query: { owner: "test-owner", repo: "test-repo" },
userId: "test-user-id",
}
})
it("should return 404 when error status is 404", async () => {
const error = { status: 404, message: "Not found" }
mockDependencies.userNickname.mockResolvedValue("test-nickname")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
mockDependencies.isUserCollaborator.mockRejectedValue(error)
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.send).toHaveBeenCalledWith(
"GitHub App is not installed on the repository test-owner/test-repo",
)
})
it("should return 500 for other errors from isUserCollaborator", async () => {
const error = new Error("Unexpected error")
mockDependencies.userNickname.mockResolvedValue("test-nickname")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
mockDependencies.isUserCollaborator.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Error checking GitHub App installation or collaborator status:",
error,
)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.send).toHaveBeenCalledWith(
"Error checking GitHub App installation or collaborator status",
)
consoleErrorSpy.mockRestore()
})
it("should handle error from userNickname", async () => {
const error = new Error("Auth0 error")
mockDependencies.userNickname.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Error checking GitHub App installation or collaborator status:",
error,
)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.send).toHaveBeenCalledWith(
"Error checking GitHub App installation or collaborator status",
)
consoleErrorSpy.mockRestore()
})
it("should handle error from getInstallationOctokitByOwner", async () => {
const error = new Error("GitHub API error")
mockDependencies.userNickname.mockResolvedValue("test-nickname")
mockDependencies.getInstallationOctokitByOwner.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Error checking GitHub App installation or collaborator status:",
error,
)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.send).toHaveBeenCalledWith(
"Error checking GitHub App installation or collaborator status",
)
consoleErrorSpy.mockRestore()
})
})
describe("successful flow", () => {
it("should return true for valid installation and authorized user", async () => {
mockReq = {
query: { owner: "test-owner", repo: "test-repo" },
userId: "test-user-id",
}
const mockOctokit = { rest: { apps: {} } }
mockDependencies.userNickname.mockResolvedValue("test-nickname")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
mockDependencies.isUserCollaborator.mockResolvedValue(true)
await isGitHubAppInstalled(mockReq as Request, mockRes as Response)
expect(mockDependencies.userNickname).toHaveBeenCalledWith("test-user-id")
expect(mockDependencies.getInstallationOctokitByOwner).toHaveBeenCalledWith(
mockDependencies.githubApp,
"test-owner",
"test-repo",
)
expect(mockDependencies.isUserCollaborator).toHaveBeenCalledWith(
mockOctokit,
"test-owner",
"test-repo",
"test-nickname",
)
expect(mockRes.json).toHaveBeenCalledWith(true)
})
})
})

View file

@ -0,0 +1,353 @@
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import { Request, Response } from "express"
import {
optimizationSuccess,
setOptimizationSuccessDependencies,
resetOptimizationSuccessDependencies,
} from "../optimiaztion-success"
describe("optimizationSuccess", () => {
let mockReq: Partial<Request>
let mockRes: Partial<Response>
let mockDependencies: any
beforeEach(() => {
// Setup response mock
mockRes = {
status: jest.fn().mockReturnThis() as jest.MockedFunction<(code: number) => Response>,
json: jest.fn() as jest.MockedFunction<(body?: any) => Response>,
}
// Setup mock dependencies
mockDependencies = {
prisma: {
optimization_events: {
updateMany: jest.fn() as jest.MockedFunction<(params: any) => Promise<{ count: number }>>,
},
},
}
setOptimizationSuccessDependencies(mockDependencies)
})
afterEach(() => {
resetOptimizationSuccessDependencies()
jest.clearAllMocks()
})
describe("input validation", () => {
it("should return 400 when trace_id is undefined", async () => {
mockReq = {
body: {
is_optimization_found: true,
},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
it("should return 400 when trace_id is null", async () => {
mockReq = {
body: {
trace_id: null,
is_optimization_found: true,
},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
it("should return 400 when is_optimization_found is undefined", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
it("should return 400 when is_optimization_found is not a boolean", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: "true",
},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
it("should return 400 when is_optimization_found is null", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: null,
},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
it("should return 400 when both fields are missing", async () => {
mockReq = {
body: {},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
it("should accept valid input with is_optimization_found as true", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: true,
},
} as Request
mockDependencies.prisma.optimization_events.updateMany.mockResolvedValue({ count: 1 })
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith({ message: "Optimization status updated." })
})
it("should accept valid input with is_optimization_found as false", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: false,
},
} as Request
mockDependencies.prisma.optimization_events.updateMany.mockResolvedValue({ count: 1 })
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith({ message: "Optimization status updated." })
})
it("should accept empty string trace_id as valid", async () => {
mockReq = {
body: {
trace_id: "",
is_optimization_found: true,
},
} as Request
mockDependencies.prisma.optimization_events.updateMany.mockResolvedValue({ count: 1 })
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith({ message: "Optimization status updated." })
})
})
describe("database operations", () => {
beforeEach(() => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: true,
},
} as Request
})
it("should successfully update optimization event", async () => {
mockDependencies.prisma.optimization_events.updateMany.mockResolvedValue({ count: 1 })
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockDependencies.prisma.optimization_events.updateMany).toHaveBeenCalledWith({
where: { trace_id: "test-trace-id" },
data: { is_optimization_found: true },
})
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith({ message: "Optimization status updated." })
})
it("should return 404 when no optimization event is found", async () => {
mockDependencies.prisma.optimization_events.updateMany.mockResolvedValue({ count: 0 })
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockDependencies.prisma.optimization_events.updateMany).toHaveBeenCalledWith({
where: { trace_id: "test-trace-id" },
data: { is_optimization_found: true },
})
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Optimization event not found." })
})
it("should handle multiple records updated", async () => {
mockDependencies.prisma.optimization_events.updateMany.mockResolvedValue({ count: 3 })
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockDependencies.prisma.optimization_events.updateMany).toHaveBeenCalledWith({
where: { trace_id: "test-trace-id" },
data: { is_optimization_found: true },
})
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith({ message: "Optimization status updated." })
})
it("should handle database error", async () => {
const error = new Error("Database connection failed")
mockDependencies.prisma.optimization_events.updateMany.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error in markOptimizationSuccess:", error)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Internal server error." })
consoleErrorSpy.mockRestore()
})
})
describe("edge cases", () => {
it("should handle numeric trace_id", async () => {
mockReq = {
body: {
trace_id: 12345,
is_optimization_found: false,
},
} as Request
mockDependencies.prisma.optimization_events.updateMany.mockResolvedValue({ count: 1 })
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockDependencies.prisma.optimization_events.updateMany).toHaveBeenCalledWith({
where: { trace_id: 12345 },
data: { is_optimization_found: false },
})
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith({ message: "Optimization status updated." })
})
it("should handle extra fields in request body", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: true,
extra_field: "should be ignored",
another_field: 123,
},
} as Request
mockDependencies.prisma.optimization_events.updateMany.mockResolvedValue({ count: 1 })
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockDependencies.prisma.optimization_events.updateMany).toHaveBeenCalledWith({
where: { trace_id: "test-trace-id" },
data: { is_optimization_found: true },
})
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith({ message: "Optimization status updated." })
})
})
describe("type validation edge cases", () => {
it("should reject is_optimization_found as number 0", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: 0,
},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
it("should reject is_optimization_found as number 1", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: 1,
},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
it("should reject is_optimization_found as array", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: [],
},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
it("should reject is_optimization_found as object", async () => {
mockReq = {
body: {
trace_id: "test-trace-id",
is_optimization_found: {},
},
} as Request
await optimizationSuccess(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Invalid input: trace_id and is_optimization_found(boolean) are required.",
})
})
})
})

View file

@ -0,0 +1,800 @@
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import { Request, Response } from "express"
import {
handleSlackEvents,
verifySlackRequest,
setHandleSlackEventsDependencies,
resetHandleSlackEventsDependencies,
} from "../slack-events"
// Helper function to create mock requests
function createMockRequest(options: { headers?: Record<string, string>; body?: any }): Request {
return {
headers: options.headers || {},
body: options.body || {},
} as unknown as Request
}
describe("handleSlackEvents", () => {
let mockReq: Request
let mockRes: Partial<Response>
let mockDependencies: any
beforeEach(() => {
// Setup response mock
mockRes = {
status: jest.fn().mockReturnThis() as jest.MockedFunction<(code: number) => Response>,
send: jest.fn() as jest.MockedFunction<(body?: any) => Response>,
json: jest.fn() as jest.MockedFunction<(body?: any) => Response>,
headersSent: false,
}
// Setup mock dependencies
mockDependencies = {
crypto: {
createHmac: jest.fn(),
timingSafeEqual: jest.fn(),
},
posthog: {
capture: jest.fn(),
},
processReaction: jest.fn(),
Sentry: {
captureException: jest.fn(),
},
getSlackSigningSecret: jest.fn(),
getCurrentTime: jest.fn(),
console: {
log: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}
setHandleSlackEventsDependencies(mockDependencies)
})
afterEach(() => {
resetHandleSlackEventsDependencies()
jest.clearAllMocks()
})
describe("verifySlackRequest", () => {
let mockHmac: any
beforeEach(() => {
mockHmac = {
update: jest.fn().mockReturnThis(),
digest: jest.fn(),
}
;(mockDependencies.crypto.createHmac as jest.MockedFunction<any>).mockReturnValue(mockHmac)
})
it("should return true when SLACK_SIGNING_SECRET is not configured", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
undefined,
)
const req = createMockRequest({
headers: {},
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(true)
expect(mockDependencies.console.warn).toHaveBeenCalledWith(
"SLACK_SIGNING_SECRET not configured. Skipping request verification.",
)
})
it("should return false when headers are missing", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
const req = createMockRequest({
headers: {},
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(false)
})
it("should return false when signature header is missing", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
const req = createMockRequest({
headers: {
"x-slack-request-timestamp": "1234567890",
},
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(false)
})
it("should return false when timestamp header is missing", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
const req = createMockRequest({
headers: {
"x-slack-signature": "v0=test-signature",
},
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(false)
})
it("should return false when request is too old (replay attack prevention)", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(1000000000)
const oldTimestamp = 1000000000 - 400 // 400 seconds old (> 300 limit)
const req = createMockRequest({
headers: {
"x-slack-signature": "v0=test-signature",
"x-slack-request-timestamp": oldTimestamp.toString(),
},
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(false)
})
it("should verify signature correctly for valid request", () => {
const currentTime = 1000000000
const timestamp = currentTime
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(currentTime)
mockHmac.digest.mockReturnValue("valid-hash")
;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(true)
const req = createMockRequest({
headers: {
"x-slack-signature": "v0=valid-signature",
"x-slack-request-timestamp": timestamp.toString(),
},
body: { test: "data" },
})
const result = verifySlackRequest(req)
expect(result).toBe(true)
expect(mockDependencies.crypto.createHmac).toHaveBeenCalledWith("sha256", "test-secret")
expect(mockHmac.update).toHaveBeenCalledWith(`v0:${timestamp}:{"test":"data"}`)
expect(mockHmac.digest).toHaveBeenCalledWith("hex")
expect(mockDependencies.crypto.timingSafeEqual).toHaveBeenCalled()
})
it("should handle string body correctly", () => {
const currentTime = 1000000000
const timestamp = currentTime
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(currentTime)
mockHmac.digest.mockReturnValue("valid-hash")
;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(true)
const req = createMockRequest({
headers: {
"x-slack-signature": "v0=valid-signature",
"x-slack-request-timestamp": timestamp.toString(),
},
body: "string-body",
})
const result = verifySlackRequest(req)
expect(result).toBe(true)
expect(mockHmac.update).toHaveBeenCalledWith(`v0:${timestamp}:string-body`)
})
it("should return false when signature verification fails", () => {
const currentTime = 1000000000
const timestamp = currentTime
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(currentTime)
mockHmac.digest.mockReturnValue("invalid-hash")
;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(false)
const req = createMockRequest({
headers: {
"x-slack-signature": "v0=wrong-signature",
"x-slack-request-timestamp": timestamp.toString(),
},
body: { test: "data" },
})
const result = verifySlackRequest(req)
expect(result).toBe(false)
})
it("should handle empty string signing secret", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue("")
const req = createMockRequest({
headers: {},
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(true)
expect(mockDependencies.console.warn).toHaveBeenCalledWith(
"SLACK_SIGNING_SECRET not configured. Skipping request verification.",
)
})
it("should handle null signing secret", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(null)
const req = createMockRequest({
headers: {},
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(true)
expect(mockDependencies.console.warn).toHaveBeenCalledWith(
"SLACK_SIGNING_SECRET not configured. Skipping request verification.",
)
})
it("should handle invalid timestamp format", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(1000000000)
// Set up the crypto mocks properly
mockHmac.digest.mockReturnValue("test-hash")
;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(false)
const req = createMockRequest({
headers: {
"x-slack-signature": "v0=test-signature",
"x-slack-request-timestamp": "invalid-timestamp",
},
body: {},
})
const result = verifySlackRequest(req)
// Invalid timestamp should cause verification to fail
// Even though the timestamp check might pass (due to NaN behavior),
// the signature verification should fail
expect(result).toBe(false)
})
})
describe("handleSlackEvents", () => {
beforeEach(() => {
// Setup verifySlackRequest to return true by default
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(1000000000)
const mockHmac = {
update: jest.fn().mockReturnThis(),
digest: jest.fn().mockReturnValue("valid-hash"),
}
;(mockDependencies.crypto.createHmac as jest.MockedFunction<any>).mockReturnValue(mockHmac)
;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(true)
})
it("should return 403 when Slack request verification fails", async () => {
;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(false)
mockReq = createMockRequest({
headers: {
"x-slack-signature": "invalid",
"x-slack-request-timestamp": "1000000000",
},
body: {},
})
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockDependencies.console.error).toHaveBeenCalledWith("Failed to verify Slack request")
expect(mockRes.status).toHaveBeenCalledWith(403)
expect(mockRes.send).toHaveBeenCalledWith("Invalid request")
})
it("should handle URL verification challenge", async () => {
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "url_verification",
challenge: "test-challenge-string",
},
})
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockDependencies.console.log).toHaveBeenCalledWith(
"Handling Slack URL verification challenge",
)
expect(mockRes.json).toHaveBeenCalledWith({ challenge: "test-challenge-string" })
})
it("should acknowledge receipt and process reaction_added event", async () => {
const mockEvent = {
type: "reaction_added",
reaction: "white_check_mark",
user: "U123456",
item: {
type: "message",
channel: "C123456",
ts: "1234567890.123456",
},
}
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "event_callback",
event: mockEvent,
},
})
;(mockDependencies.processReaction as jest.MockedFunction<any>).mockResolvedValue(true)
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.send).toHaveBeenCalledWith("OK")
expect(mockDependencies.console.log).toHaveBeenCalledWith(
"Processing Slack event: reaction_added",
)
expect(mockDependencies.processReaction).toHaveBeenCalledWith(mockEvent)
expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
distinctId: "system",
event: "slack-approval-reaction-processed",
properties: {
reaction: "white_check_mark",
result: "success",
},
})
})
it("should not capture analytics when processReaction returns false", async () => {
const mockEvent = {
type: "reaction_added",
reaction: "x",
user: "U123456",
}
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "event_callback",
event: mockEvent,
},
})
;(mockDependencies.processReaction as jest.MockedFunction<any>).mockResolvedValue(false)
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockDependencies.processReaction).toHaveBeenCalledWith(mockEvent)
expect(mockDependencies.posthog.capture).not.toHaveBeenCalledWith(
expect.objectContaining({
event: "slack-approval-reaction-processed",
}),
)
})
it("should ignore non-reaction events", async () => {
const mockEvent = {
type: "message",
text: "Hello world",
}
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "event_callback",
event: mockEvent,
},
})
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockDependencies.console.log).toHaveBeenCalledWith("Processing Slack event: message")
expect(mockDependencies.processReaction).not.toHaveBeenCalled()
})
it("should handle requests without event property", async () => {
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "other_type",
},
})
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.send).toHaveBeenCalledWith("OK")
expect(mockDependencies.processReaction).not.toHaveBeenCalled()
})
it("should handle errors and capture them", async () => {
const error = new Error("Processing failed")
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "event_callback",
event: {
type: "reaction_added",
reaction: "test",
},
},
})
;(mockDependencies.processReaction as jest.MockedFunction<any>).mockRejectedValue(error)
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
`Error handling Slack event: ${error}`,
)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.send).toHaveBeenCalledWith("Error processing event")
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
distinctId: "system",
event: "slack-optimization-approval-error",
properties: {
error: "Error: Processing failed",
},
})
})
it("should not send error response if headers already sent", async () => {
const error = new Error("Processing failed")
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "event_callback",
event: {
type: "reaction_added",
reaction: "test",
},
},
})
// Mock processReaction to throw an error
;(mockDependencies.processReaction as jest.MockedFunction<any>).mockRejectedValue(error)
// Mock the response to simulate headers already sent after the first response
;(mockRes.status as jest.MockedFunction<any>).mockImplementationOnce(() => {
mockRes.headersSent = true
return mockRes
})
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
`Error handling Slack event: ${error}`,
)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
// The status should be called once (for the initial 200 response)
expect(mockRes.status).toHaveBeenCalledTimes(1)
expect(mockRes.status).toHaveBeenCalledWith(200)
})
it("should handle errors during verification", async () => {
const error = new Error("Verification failed")
;(mockDependencies.crypto.createHmac as jest.MockedFunction<any>).mockImplementation(() => {
throw error
})
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {},
})
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
`Error handling Slack event: ${error}`,
)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.send).toHaveBeenCalledWith("Error processing event")
})
it("should handle invalid timestamp format gracefully", async () => {
// Force verification to fail by making timingSafeEqual return false
;(mockDependencies.crypto.timingSafeEqual as jest.MockedFunction<any>).mockReturnValue(false)
mockReq = createMockRequest({
headers: {
"x-slack-signature": "v0=test-signature",
"x-slack-request-timestamp": "invalid-timestamp",
},
body: {},
})
await handleSlackEvents(mockReq, mockRes as Response)
// Should fail verification and return 403
expect(mockRes.status).toHaveBeenCalledWith(403)
expect(mockRes.send).toHaveBeenCalledWith("Invalid request")
})
it("should handle complex event data with nested objects", async () => {
const complexEvent = {
type: "reaction_added",
reaction: "heavy_check_mark",
user: "U1234567890",
item: {
type: "message",
channel: "C1234567890",
ts: "1234567890.123456",
},
item_user: "U0987654321",
event_ts: "1234567890.123457",
}
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "event_callback",
team_id: "T1234567890",
api_app_id: "A1234567890",
event: complexEvent,
event_id: "Ev1234567890",
event_time: 1234567890,
},
})
;(mockDependencies.processReaction as jest.MockedFunction<any>).mockResolvedValue(true)
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockDependencies.processReaction).toHaveBeenCalledWith(complexEvent)
expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
distinctId: "system",
event: "slack-approval-reaction-processed",
properties: {
reaction: "heavy_check_mark",
result: "success",
},
})
})
it("should handle empty request body", async () => {
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {},
})
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.send).toHaveBeenCalledWith("OK")
})
it("should handle undefined request body in verification", async () => {
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: undefined,
})
await handleSlackEvents(mockReq, mockRes as Response)
// Should handle undefined body in JSON.stringify
expect(mockRes.status).toHaveBeenCalledWith(200)
})
it("should handle malformed JSON in error conversion", async () => {
const circularError = {}
;(circularError as any).self = circularError // Create circular reference
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "event_callback",
event: {
type: "reaction_added",
reaction: "test",
},
},
})
;(mockDependencies.processReaction as jest.MockedFunction<any>).mockRejectedValue(
circularError,
)
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
distinctId: "system",
event: "slack-optimization-approval-error",
properties: {
error: "[object Object]", // String() conversion of circular object
},
})
})
it("should handle processReaction throwing synchronously", async () => {
const error = new Error("Sync error")
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {
type: "event_callback",
event: {
type: "reaction_added",
reaction: "test",
},
},
})
;(mockDependencies.processReaction as jest.MockedFunction<any>).mockImplementation(() => {
throw error
})
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
`Error handling Slack event: ${error}`,
)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
})
it("should handle errors that occur before sending response", async () => {
const error = new Error("Early error")
// Mock verification to fail with an error before any response is sent
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockImplementation(
() => {
throw error
},
)
mockReq = createMockRequest({
headers: {
"x-slack-signature": "valid",
"x-slack-request-timestamp": "1000000000",
},
body: {},
})
await handleSlackEvents(mockReq, mockRes as Response)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
`Error handling Slack event: ${error}`,
)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.send).toHaveBeenCalledWith("Error processing event")
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
})
})
describe("edge cases", () => {
it("should handle missing headers object", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
const req = createMockRequest({
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(false)
})
it("should handle very large timestamp differences", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(
Number.MAX_SAFE_INTEGER,
)
const req = createMockRequest({
headers: {
"x-slack-signature": "v0=test-signature",
"x-slack-request-timestamp": "0",
},
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(false)
})
it("should handle negative timestamps", () => {
;(mockDependencies.getSlackSigningSecret as jest.MockedFunction<any>).mockReturnValue(
"test-secret",
)
;(mockDependencies.getCurrentTime as jest.MockedFunction<any>).mockReturnValue(1000000000)
const req = createMockRequest({
headers: {
"x-slack-signature": "v0=test-signature",
"x-slack-request-timestamp": "-1000",
},
body: {},
})
const result = verifySlackRequest(req)
expect(result).toBe(false)
})
})
})

View file

@ -0,0 +1,735 @@
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import { Request, Response } from "express"
import {
stripeWebhookHandler,
handleCheckoutCompleted,
handleSubscriptionUpdate,
handleSubscriptionCancellation,
handleFailedPayment,
setStripeWebhookDependencies,
resetStripeWebhookDependencies,
} from "../stripe-webhook"
describe("Stripe Webhook Handler", () => {
let mockReq: Partial<Request>
let mockRes: Partial<Response>
let mockDependencies: any
beforeEach(() => {
// Setup request mock
mockReq = {
headers: {
"stripe-signature": "test-signature",
},
body: "test-body",
}
// Setup response mock
mockRes = {
status: jest.fn().mockReturnThis() as jest.MockedFunction<(code: number) => Response>,
send: jest.fn() as jest.MockedFunction<(body?: any) => Response>,
json: jest.fn() as jest.MockedFunction<(body?: any) => Response>,
}
// Setup mock dependencies
mockDependencies = {
stripe: {
webhooks: {
constructEvent: jest.fn(),
},
subscriptions: {
update: jest.fn(),
retrieve: jest.fn(),
},
customers: {
update: jest.fn(),
retrieve: jest.fn(),
},
prices: {
retrieve: jest.fn(),
},
},
prisma: {
subscriptions: {
upsert: jest.fn(),
update: jest.fn(),
},
},
Sentry: {
captureException: jest.fn(),
},
getWebhookSecret: jest.fn().mockReturnValue("test-webhook-secret"),
}
setStripeWebhookDependencies(mockDependencies)
})
afterEach(() => {
resetStripeWebhookDependencies()
jest.clearAllMocks()
})
describe("stripeWebhookHandler", () => {
describe("webhook event construction", () => {
it("should return 400 when webhook secret is not configured", async () => {
;(mockDependencies.getWebhookSecret as jest.MockedFunction<any>).mockReturnValue(undefined)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await stripeWebhookHandler(mockReq as Request, mockRes as Response)
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Webhook Error:",
"STRIPE_WEBHOOK_SECRET is not configured",
)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalled()
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.send).toHaveBeenCalledWith(
"Webhook Error: STRIPE_WEBHOOK_SECRET is not configured",
)
consoleErrorSpy.mockRestore()
})
it("should return 400 when webhook signature verification fails", async () => {
const error = new Error("Invalid signature")
;(
mockDependencies.stripe.webhooks.constructEvent as jest.MockedFunction<any>
).mockImplementation(() => {
throw error
})
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await stripeWebhookHandler(mockReq as Request, mockRes as Response)
expect(mockDependencies.stripe.webhooks.constructEvent).toHaveBeenCalledWith(
"test-body",
"test-signature",
"test-webhook-secret",
)
expect(consoleErrorSpy).toHaveBeenCalledWith("Webhook Error:", "Invalid signature")
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.send).toHaveBeenCalledWith("Webhook Error: Invalid signature")
consoleErrorSpy.mockRestore()
})
it("should process webhook event successfully", async () => {
const mockEvent = {
id: "evt_test123",
type: "customer.subscription.created",
data: {
object: {
id: "sub_test123",
customer: "cus_test123",
metadata: { userId: "user_test123" },
items: {
data: [
{
price: { id: "price_test123" },
},
],
},
status: "active",
current_period_start: 1640995200,
current_period_end: 1643673600,
},
},
}
;(
mockDependencies.stripe.webhooks.constructEvent as jest.MockedFunction<any>
).mockReturnValue(mockEvent)
;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: { tier: "pro", optimizations: "1000" },
})
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await stripeWebhookHandler(mockReq as Request, mockRes as Response)
expect(consoleSpy).toHaveBeenCalledWith(
"Processing Stripe webhook: customer.subscription.created",
)
expect(consoleSpy).toHaveBeenCalledWith("Event ID: evt_test123")
expect(mockRes.json).toHaveBeenCalledWith({ received: true })
consoleSpy.mockRestore()
})
it("should handle unimplemented event types", async () => {
const mockEvent = {
id: "evt_test123",
type: "unknown.event.type",
data: {
object: {},
},
}
;(
mockDependencies.stripe.webhooks.constructEvent as jest.MockedFunction<any>
).mockReturnValue(mockEvent)
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await stripeWebhookHandler(mockReq as Request, mockRes as Response)
expect(consoleSpy).toHaveBeenCalledWith(
"No handler implemented for event type: unknown.event.type",
)
expect(mockRes.json).toHaveBeenCalledWith({ received: true })
consoleSpy.mockRestore()
})
})
})
describe("handleCheckoutCompleted", () => {
it("should handle checkout session without userId metadata", async () => {
const session = {
id: "cs_test123",
metadata: {},
mode: "subscription",
}
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await handleCheckoutCompleted(session)
expect(consoleErrorSpy).toHaveBeenCalledWith(
"No userId in checkout session metadata",
"cs_test123",
)
expect(mockDependencies.stripe.subscriptions.update).not.toHaveBeenCalled()
consoleErrorSpy.mockRestore()
})
it("should handle non-subscription checkout", async () => {
const session = {
id: "cs_test123",
metadata: { userId: "user_test123" },
mode: "payment",
}
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await handleCheckoutCompleted(session)
expect(consoleSpy).toHaveBeenCalledWith(
"Processing checkout completion for user user_test123",
)
expect(mockDependencies.stripe.subscriptions.update).not.toHaveBeenCalled()
consoleSpy.mockRestore()
})
it("should update subscription and customer metadata", async () => {
const session = {
id: "cs_test123",
metadata: { userId: "user_test123" },
mode: "subscription",
subscription: "sub_test123",
customer: "cus_test123",
}
;(
mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
).mockResolvedValue({
id: "sub_test123",
customer: "cus_test123",
metadata: { userId: "user_test123" },
items: {
data: [
{
price: { id: "price_test123" },
},
],
},
status: "active",
current_period_start: 1640995200,
current_period_end: 1643673600,
})
;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: { tier: "pro", optimizations: "1000" },
})
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await handleCheckoutCompleted(session)
expect(mockDependencies.stripe.subscriptions.update).toHaveBeenCalledWith("sub_test123", {
metadata: { userId: "user_test123" },
})
expect(mockDependencies.stripe.customers.update).toHaveBeenCalledWith("cus_test123", {
metadata: { userId: "user_test123" },
})
expect(mockDependencies.stripe.subscriptions.retrieve).toHaveBeenCalledWith("sub_test123")
expect(mockDependencies.prisma.subscriptions.upsert).toHaveBeenCalled()
consoleSpy.mockRestore()
})
it("should handle errors during checkout processing", async () => {
const session = {
id: "cs_test123",
metadata: { userId: "user_test123" },
mode: "subscription",
subscription: "sub_test123",
customer: "cus_test123",
}
const error = new Error("Stripe API error")
;(mockDependencies.stripe.subscriptions.update as jest.MockedFunction<any>).mockRejectedValue(
error,
)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await handleCheckoutCompleted(session)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error processing checkout session:", error)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
consoleErrorSpy.mockRestore()
})
})
describe("handleSubscriptionUpdate", () => {
it("should handle subscription without userId in metadata", async () => {
const subscription = {
id: "sub_test123",
customer: "cus_test123",
metadata: {},
}
;(mockDependencies.stripe.customers.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: {},
})
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await handleSubscriptionUpdate(subscription)
expect(mockDependencies.stripe.customers.retrieve).toHaveBeenCalledWith("cus_test123")
expect(consoleErrorSpy).toHaveBeenCalledWith(
"No userId found in subscription or customer metadata",
JSON.stringify({
subscription_id: "sub_test123",
customer_id: "cus_test123",
}),
)
expect(mockDependencies.prisma.subscriptions.upsert).not.toHaveBeenCalled()
consoleErrorSpy.mockRestore()
})
it("should get userId from customer metadata when not in subscription", async () => {
const subscription = {
id: "sub_test123",
customer: "cus_test123",
metadata: {},
items: {
data: [
{
price: { id: "price_test123" },
},
],
},
status: "active",
current_period_start: 1640995200,
current_period_end: 1643673600,
}
;(mockDependencies.stripe.customers.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: { userId: "user_test123" },
})
;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: { tier: "enterprise", optimizations: "2000" },
})
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await handleSubscriptionUpdate(subscription)
expect(mockDependencies.stripe.customers.retrieve).toHaveBeenCalledWith("cus_test123")
expect(consoleSpy).toHaveBeenCalledWith("Updating subscription for user user_test123")
expect(mockDependencies.stripe.prices.retrieve).toHaveBeenCalledWith("price_test123")
expect(mockDependencies.prisma.subscriptions.upsert).toHaveBeenCalled()
consoleSpy.mockRestore()
})
it("should handle customer retrieval error gracefully", async () => {
const subscription = {
id: "sub_test123",
customer: "cus_test123",
metadata: {},
}
const error = new Error("Customer not found")
;(mockDependencies.stripe.customers.retrieve as jest.MockedFunction<any>).mockRejectedValue(
error,
)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await handleSubscriptionUpdate(subscription)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error retrieving customer:", error)
expect(mockDependencies.prisma.subscriptions.upsert).not.toHaveBeenCalled()
consoleErrorSpy.mockRestore()
})
it("should create new subscription record", async () => {
const subscription = {
id: "sub_test123",
customer: "cus_test123",
metadata: { userId: "user_test123" },
items: {
data: [
{
price: { id: "price_test123" },
},
],
},
status: "active",
current_period_start: 1640995200,
current_period_end: 1643673600,
cancel_at_period_end: false,
}
;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: { tier: "pro", optimizations: "1000" },
})
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await handleSubscriptionUpdate(subscription)
expect(mockDependencies.stripe.prices.retrieve).toHaveBeenCalledWith("price_test123")
expect(mockDependencies.prisma.subscriptions.upsert).toHaveBeenCalledWith({
where: { user_id: "user_test123" },
create: {
user_id: "user_test123",
stripe_customer_id: "cus_test123",
stripe_subscription_id: "sub_test123",
plan_type: "pro",
optimizations_limit: 1000,
optimizations_used: 0,
subscription_status: "active",
current_period_start: new Date(1640995200 * 1000),
current_period_end: new Date(1643673600 * 1000),
created_at: expect.any(Date),
updated_at: expect.any(Date),
cancel_at_period_end: false,
cancellation_request_date: null,
},
update: expect.any(Object),
})
expect(consoleSpy).toHaveBeenCalledWith(
"Successfully updated subscription for user user_test123",
)
consoleSpy.mockRestore()
})
it("should handle cancel_at_period_end = true", async () => {
const subscription = {
id: "sub_test123",
customer: "cus_test123",
metadata: { userId: "user_test123" },
items: {
data: [
{
price: { id: "price_test123" },
},
],
},
status: "active",
current_period_start: 1640995200,
current_period_end: 1643673600,
cancel_at_period_end: true,
}
;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: { tier: "pro", optimizations: "1000" },
})
await handleSubscriptionUpdate(subscription)
const upsertCall = (mockDependencies.prisma.subscriptions.upsert as jest.MockedFunction<any>)
.mock.calls[0][0]
expect(upsertCall.update.cancel_at_period_end).toBe(true)
expect(upsertCall.update.cancellation_request_date).toBeInstanceOf(Date)
})
it("should handle cancel_at_period_end = false (reactivation)", async () => {
const subscription = {
id: "sub_test123",
customer: "cus_test123",
metadata: { userId: "user_test123" },
items: {
data: [
{
price: { id: "price_test123" },
},
],
},
status: "active",
current_period_start: 1640995200,
current_period_end: 1643673600,
cancel_at_period_end: false,
}
;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: { tier: "pro", optimizations: "1000" },
})
await handleSubscriptionUpdate(subscription)
const upsertCall = (mockDependencies.prisma.subscriptions.upsert as jest.MockedFunction<any>)
.mock.calls[0][0]
expect(upsertCall.update.cancel_at_period_end).toBe(false)
expect(upsertCall.update.cancellation_request_date).toBe(null)
})
it("should use default values when price metadata is missing", async () => {
const subscription = {
id: "sub_test123",
customer: "cus_test123",
metadata: { userId: "user_test123" },
items: {
data: [
{
price: { id: "price_test123" },
},
],
},
status: "active",
current_period_start: 1640995200,
current_period_end: 1643673600,
}
;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: {},
})
await handleSubscriptionUpdate(subscription)
const upsertCall = (mockDependencies.prisma.subscriptions.upsert as jest.MockedFunction<any>)
.mock.calls[0][0]
expect(upsertCall.create.plan_type).toBe("pro")
expect(upsertCall.create.optimizations_limit).toBe(500)
})
it("should handle database errors", async () => {
const subscription = {
id: "sub_test123",
customer: "cus_test123",
metadata: { userId: "user_test123" },
items: {
data: [
{
price: { id: "price_test123" },
},
],
},
status: "active",
current_period_start: 1640995200,
current_period_end: 1643673600,
}
const error = new Error("Database error")
;(mockDependencies.stripe.prices.retrieve as jest.MockedFunction<any>).mockResolvedValue({
metadata: { tier: "pro", optimizations: "1000" },
})
;(mockDependencies.prisma.subscriptions.upsert as jest.MockedFunction<any>).mockRejectedValue(
error,
)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await handleSubscriptionUpdate(subscription)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error updating subscription:", error)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
consoleErrorSpy.mockRestore()
})
})
describe("handleSubscriptionCancellation", () => {
it("should handle subscription without userId", async () => {
const subscription = {
id: "sub_test123",
metadata: {},
}
await handleSubscriptionCancellation(subscription)
expect(mockDependencies.prisma.subscriptions.update).not.toHaveBeenCalled()
})
it("should cancel subscription successfully", async () => {
const subscription = {
id: "sub_test123",
metadata: { userId: "user_test123" },
}
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await handleSubscriptionCancellation(subscription)
expect(mockDependencies.prisma.subscriptions.update).toHaveBeenCalledWith({
where: { user_id: "user_test123" },
data: {
subscription_status: "canceled",
plan_type: "free",
optimizations_limit: 100,
stripe_subscription_id: null,
cancel_at_period_end: false,
cancellation_request_date: null,
},
})
expect(consoleSpy).toHaveBeenCalledWith("Cancelled subscription for user user_test123")
consoleSpy.mockRestore()
})
it("should handle database errors during cancellation", async () => {
const subscription = {
id: "sub_test123",
metadata: { userId: "user_test123" },
}
const error = new Error("Database error")
;(mockDependencies.prisma.subscriptions.update as jest.MockedFunction<any>).mockRejectedValue(
error,
)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await handleSubscriptionCancellation(subscription)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error cancelling subscription:", error)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
consoleErrorSpy.mockRestore()
})
})
describe("handleFailedPayment", () => {
it("should handle invoice without subscription", async () => {
const invoice = {
id: "in_test123",
}
await handleFailedPayment(invoice)
expect(mockDependencies.stripe.subscriptions.retrieve).not.toHaveBeenCalled()
})
it("should handle subscription without userId", async () => {
const invoice = {
id: "in_test123",
subscription: "sub_test123",
}
;(
mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
).mockResolvedValue({
id: "sub_test123",
metadata: {},
})
await handleFailedPayment(invoice)
expect(mockDependencies.stripe.subscriptions.retrieve).toHaveBeenCalledWith("sub_test123")
expect(mockDependencies.prisma.subscriptions.update).not.toHaveBeenCalled()
})
it("should update subscription to past_due", async () => {
const invoice = {
id: "in_test123",
subscription: "sub_test123",
}
;(
mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
).mockResolvedValue({
id: "sub_test123",
metadata: { userId: "user_test123" },
})
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await handleFailedPayment(invoice)
expect(mockDependencies.stripe.subscriptions.retrieve).toHaveBeenCalledWith("sub_test123")
expect(mockDependencies.prisma.subscriptions.update).toHaveBeenCalledWith({
where: { user_id: "user_test123" },
data: {
subscription_status: "past_due",
},
})
expect(consoleSpy).toHaveBeenCalledWith(
"Updated subscription status to past_due for user user_test123",
)
consoleSpy.mockRestore()
})
it("should handle errors during failed payment processing", async () => {
const invoice = {
id: "in_test123",
subscription: "sub_test123",
}
const error = new Error("Database error")
;(
mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
).mockResolvedValue({
id: "sub_test123",
metadata: { userId: "user_test123" },
})
;(mockDependencies.prisma.subscriptions.update as jest.MockedFunction<any>).mockRejectedValue(
error,
)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await handleFailedPayment(invoice)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error handling failed payment:", error)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
consoleErrorSpy.mockRestore()
})
it("should handle Stripe API errors when retrieving subscription", async () => {
const invoice = {
id: "in_test123",
subscription: "sub_test123",
}
const error = new Error("Subscription not found")
;(
mockDependencies.stripe.subscriptions.retrieve as jest.MockedFunction<any>
).mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await handleFailedPayment(invoice)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error handling failed payment:", error)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
expect(mockDependencies.prisma.subscriptions.update).not.toHaveBeenCalled()
consoleErrorSpy.mockRestore()
})
})
})

View file

@ -0,0 +1,617 @@
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import { Request, Response, NextFunction } from "express"
import {
getSubscription,
createCheckout,
cancelSubscription,
setSubscriptionDependencies,
resetSubscriptionDependencies,
} from "../subscription-management"
describe("Subscription Functions", () => {
let mockReq: Partial<Request>
let mockRes: Partial<Response>
let mockNext: NextFunction
let mockDependencies: any
beforeEach(() => {
// Setup response mock
mockRes = {
status: jest.fn().mockReturnThis() as jest.MockedFunction<(code: number) => Response>,
json: jest.fn() as jest.MockedFunction<(body?: any) => Response>,
}
// Setup next function mock
mockNext = jest.fn() as NextFunction
// Setup mock dependencies
mockDependencies = {
prisma: {
subscriptions: {
findUnique: jest.fn() as jest.MockedFunction<(params: any) => Promise<any>>,
},
},
createCheckoutSession: jest.fn() as jest.MockedFunction<
(userId: string, priceId: string, options: any) => Promise<string>
>,
cancelStripeSubscription: jest.fn() as jest.MockedFunction<(userId: string) => Promise<void>>,
Sentry: {
captureException: jest.fn() as jest.MockedFunction<(error: any) => void>,
},
}
setSubscriptionDependencies(mockDependencies)
})
afterEach(() => {
resetSubscriptionDependencies()
jest.clearAllMocks()
})
describe("getSubscription", () => {
describe("input validation", () => {
it("should return 400 when userId is missing", async () => {
mockReq = {
query: {},
}
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID is required" })
})
it("should return 400 when userId is empty string", async () => {
mockReq = {
query: { userId: "" },
}
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID is required" })
})
it("should return 400 when userId is null", async () => {
mockReq = {
query: { userId: null as any },
}
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID is required" })
})
})
describe("successful retrieval", () => {
it("should return subscription details when found", async () => {
const mockSubscription = {
plan_type: "pro",
subscription_status: "active",
optimizations_used: 5,
optimizations_limit: 100,
current_period_end: "2024-12-31T23:59:59Z",
}
mockReq = {
query: { userId: "user-123" },
}
mockDependencies.prisma.subscriptions.findUnique.mockResolvedValue(mockSubscription)
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockDependencies.prisma.subscriptions.findUnique).toHaveBeenCalledWith({
where: { user_id: "user-123" },
})
expect(mockRes.json).toHaveBeenCalledWith({
plan: "pro",
status: "active",
usageCount: 5,
usageLimit: 100,
renewalDate: "2024-12-31T23:59:59Z",
})
})
it("should return 404 when subscription not found", async () => {
mockReq = {
query: { userId: "user-123" },
}
mockDependencies.prisma.subscriptions.findUnique.mockResolvedValue(null)
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Subscription not found" })
})
})
describe("error handling", () => {
it("should handle database errors", async () => {
const error = new Error("Database error")
mockReq = {
query: { userId: "user-123" },
}
mockDependencies.prisma.subscriptions.findUnique.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting subscription:", error)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
expect(mockNext).toHaveBeenCalledWith(error)
consoleErrorSpy.mockRestore()
})
})
})
describe("createCheckout", () => {
describe("input validation", () => {
it("should return 400 when userId is missing", async () => {
mockReq = {
body: { priceId: "price-123" },
}
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID and price ID are required" })
})
it("should return 400 when priceId is missing", async () => {
mockReq = {
body: { userId: "user-123" },
}
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID and price ID are required" })
})
it("should return 400 when both userId and priceId are missing", async () => {
mockReq = {
body: {},
}
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID and price ID are required" })
})
})
describe("successful checkout creation", () => {
beforeEach(() => {
// Mock environment variables
process.env.WEBAPP_URL = "https://test.example.com"
process.env.STRIPE_PRO_PRICE_YEARLY_ID = "price-yearly-123"
})
it("should create checkout session with default options", async () => {
mockReq = {
body: {
userId: "user-123",
priceId: "price-monthly-123",
},
}
mockDependencies.createCheckoutSession.mockResolvedValue(
"https://checkout.stripe.com/session-123",
)
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockDependencies.createCheckoutSession).toHaveBeenCalledWith(
"user-123",
"price-monthly-123",
{
successUrl: "https://test.example.com/app/billing?success=true",
cancelUrl: "https://test.example.com/app/billing?canceled=true",
period: "monthly",
},
)
expect(mockRes.json).toHaveBeenCalledWith({
url: "https://checkout.stripe.com/session-123",
})
})
it("should use yearly period for yearly price ID", async () => {
mockReq = {
body: {
userId: "user-123",
priceId: "price-yearly-123",
},
}
mockDependencies.createCheckoutSession.mockResolvedValue(
"https://checkout.stripe.com/session-123",
)
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockDependencies.createCheckoutSession).toHaveBeenCalledWith(
"user-123",
"price-yearly-123",
{
successUrl: "https://test.example.com/app/billing?success=true",
cancelUrl: "https://test.example.com/app/billing?canceled=true",
period: "yearly",
},
)
})
it("should use custom URLs when provided", async () => {
mockReq = {
body: {
userId: "user-123",
priceId: "price-monthly-123",
successUrl: "https://custom.com/success",
cancelUrl: "https://custom.com/cancel",
},
}
mockDependencies.createCheckoutSession.mockResolvedValue(
"https://checkout.stripe.com/session-123",
)
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockDependencies.createCheckoutSession).toHaveBeenCalledWith(
"user-123",
"price-monthly-123",
{
successUrl: "https://custom.com/success",
cancelUrl: "https://custom.com/cancel",
period: "monthly",
},
)
})
it("should use custom period when provided", async () => {
mockReq = {
body: {
userId: "user-123",
priceId: "price-monthly-123",
period: "custom",
},
}
mockDependencies.createCheckoutSession.mockResolvedValue(
"https://checkout.stripe.com/session-123",
)
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockDependencies.createCheckoutSession).toHaveBeenCalledWith(
"user-123",
"price-monthly-123",
{
successUrl: "https://test.example.com/app/billing?success=true",
cancelUrl: "https://test.example.com/app/billing?canceled=true",
period: "custom",
},
)
})
})
describe("error handling", () => {
it("should handle checkout session creation errors", async () => {
const error = new Error("Stripe error")
mockReq = {
body: {
userId: "user-123",
priceId: "price-123",
},
}
mockDependencies.createCheckoutSession.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error creating checkout session:", error)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
expect(mockNext).toHaveBeenCalledWith(error)
consoleErrorSpy.mockRestore()
})
})
})
describe("cancelSubscription", () => {
describe("input validation", () => {
it("should return 400 when userId is missing", async () => {
mockReq = {
body: {},
}
await cancelSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID is required" })
})
it("should return 400 when userId is empty string", async () => {
mockReq = {
body: { userId: "" },
}
await cancelSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID is required" })
})
it("should return 400 when userId is null", async () => {
mockReq = {
body: { userId: null },
}
await cancelSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID is required" })
})
})
describe("successful cancellation", () => {
it("should cancel subscription successfully", async () => {
mockReq = {
body: { userId: "user-123" },
}
mockDependencies.cancelStripeSubscription.mockResolvedValue(undefined)
await cancelSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockDependencies.cancelStripeSubscription).toHaveBeenCalledWith("user-123")
expect(mockRes.json).toHaveBeenCalledWith({ status: "canceled" })
})
})
describe("error handling", () => {
it("should handle cancellation errors", async () => {
const error = new Error("Cancellation failed")
mockReq = {
body: { userId: "user-123" },
}
mockDependencies.cancelStripeSubscription.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await cancelSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error canceling subscription:", error)
expect(mockDependencies.Sentry.captureException).toHaveBeenCalledWith(error)
expect(mockNext).toHaveBeenCalledWith(error)
consoleErrorSpy.mockRestore()
})
})
})
describe("edge cases", () => {
it("should handle array userId in query", async () => {
mockReq = {
query: { userId: ["user-123", "user-456"] as any },
}
// Mock the function to return null since arrays won't match any real user_id
mockDependencies.prisma.subscriptions.findUnique.mockResolvedValue(null)
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
// The function will receive the array as-is and pass it to the database query
expect(mockDependencies.prisma.subscriptions.findUnique).toHaveBeenCalledWith({
where: { user_id: ["user-123", "user-456"] },
})
// Since no subscription will be found with an array userId, it should return 404
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Subscription not found" })
})
it("should handle numeric userId in query", async () => {
mockReq = {
query: { userId: 12345 as any },
}
// Numeric user IDs likely won't match string user_ids in database
mockDependencies.prisma.subscriptions.findUnique.mockResolvedValue(null)
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
// Numeric values are passed as-is, not converted to string by the type assertion
expect(mockDependencies.prisma.subscriptions.findUnique).toHaveBeenCalledWith({
where: { user_id: 12345 },
})
// Should return 404 since numeric ID likely won't match any records
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Subscription not found" })
})
it("should handle missing environment variables in createCheckout", async () => {
const originalWebAppUrl = process.env.WEBAPP_URL
const originalYearlyPriceId = process.env.STRIPE_PRO_PRICE_YEARLY_ID
delete process.env.WEBAPP_URL
delete process.env.STRIPE_PRO_PRICE_YEARLY_ID
mockReq = {
body: {
userId: "user-123",
priceId: "price-monthly-123",
},
}
mockDependencies.createCheckoutSession.mockResolvedValue(
"https://checkout.stripe.com/session-123",
)
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockDependencies.createCheckoutSession).toHaveBeenCalledWith(
"user-123",
"price-monthly-123",
{
successUrl: "undefined/app/billing?success=true",
cancelUrl: "undefined/app/billing?canceled=true",
period: "monthly",
},
)
// Restore environment variables
if (originalWebAppUrl) process.env.WEBAPP_URL = originalWebAppUrl
if (originalYearlyPriceId) process.env.STRIPE_PRO_PRICE_YEARLY_ID = originalYearlyPriceId
})
it("should handle boolean userId in query", async () => {
mockReq = {
query: { userId: true as any },
}
// Boolean values likely won't match any real user_id
mockDependencies.prisma.subscriptions.findUnique.mockResolvedValue(null)
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
// Boolean values are passed as-is
expect(mockDependencies.prisma.subscriptions.findUnique).toHaveBeenCalledWith({
where: { user_id: true },
})
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Subscription not found" })
})
it("should handle object userId in query", async () => {
const objectUserId = { id: "user-123" }
mockReq = {
query: { userId: objectUserId as any },
}
// Object values likely won't match any real user_id
mockDependencies.prisma.subscriptions.findUnique.mockResolvedValue(null)
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
// Object values are passed as-is
expect(mockDependencies.prisma.subscriptions.findUnique).toHaveBeenCalledWith({
where: { user_id: objectUserId },
})
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Subscription not found" })
})
it("should handle empty object in request body for createCheckout", async () => {
mockReq = {
body: {
userId: "",
priceId: "",
},
}
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "User ID and price ID are required" })
})
it("should handle subscription with missing fields", async () => {
const mockIncompleteSubscription = {
plan_type: "pro",
// Missing some fields
}
mockReq = {
query: { userId: "user-123" },
}
mockDependencies.prisma.subscriptions.findUnique.mockResolvedValue(mockIncompleteSubscription)
await getSubscription(mockReq as Request, mockRes as Response, mockNext)
expect(mockRes.json).toHaveBeenCalledWith({
plan: "pro",
status: undefined,
usageCount: undefined,
usageLimit: undefined,
renewalDate: undefined,
})
})
})
describe("createCheckout environment variable handling", () => {
afterEach(() => {
// Ensure environment variables are restored
process.env.WEBAPP_URL = "https://test.example.com"
process.env.STRIPE_PRO_PRICE_YEARLY_ID = "price-yearly-123"
})
it("should handle undefined WEBAPP_URL gracefully", async () => {
delete process.env.WEBAPP_URL
process.env.STRIPE_PRO_PRICE_YEARLY_ID = "price-yearly-123"
mockReq = {
body: {
userId: "user-123",
priceId: "price-monthly-123",
},
}
mockDependencies.createCheckoutSession.mockResolvedValue(
"https://checkout.stripe.com/session-123",
)
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockDependencies.createCheckoutSession).toHaveBeenCalledWith(
"user-123",
"price-monthly-123",
{
successUrl: "undefined/app/billing?success=true",
cancelUrl: "undefined/app/billing?canceled=true",
period: "monthly",
},
)
})
it("should handle undefined STRIPE_PRO_PRICE_YEARLY_ID gracefully", async () => {
process.env.WEBAPP_URL = "https://test.example.com"
delete process.env.STRIPE_PRO_PRICE_YEARLY_ID
mockReq = {
body: {
userId: "user-123",
priceId: "some-unknown-price-id",
},
}
mockDependencies.createCheckoutSession.mockResolvedValue(
"https://checkout.stripe.com/session-123",
)
await createCheckout(mockReq as Request, mockRes as Response, mockNext)
expect(mockDependencies.createCheckoutSession).toHaveBeenCalledWith(
"user-123",
"some-unknown-price-id",
{
successUrl: "https://test.example.com/app/billing?success=true",
cancelUrl: "https://test.example.com/app/billing?canceled=true",
period: "monthly", // Should default to monthly since yearly price ID is undefined
},
)
})
})
})

View file

@ -0,0 +1,687 @@
import { jest, describe, it, expect, beforeEach, afterEach, beforeAll } from "@jest/globals"
import { Request, Response } from "express"
let suggestPrChanges: typeof import("../suggest-pr-changes").suggestPrChanges
let triggerSuggestPrChanges: typeof import("../suggest-pr-changes").triggerSuggestPrChanges
let setSuggestPrChangesDependencies: typeof import("../suggest-pr-changes").setSuggestPrChangesDependencies
let resetSuggestPrChangesDependencies: typeof import("../suggest-pr-changes").resetSuggestPrChangesDependencies
// Mock types for testing
interface AuthorizedUserReq extends Request {
userId: string
}
// Note: Global mocks are set up in jest.setup.ts
describe("Suggest PR Changes", () => {
beforeAll(async () => {
process.env.KEY_VAULT_NAME = "mocked-keyvault-name"
const mod = await import("../suggest-pr-changes")
suggestPrChanges = mod.suggestPrChanges
triggerSuggestPrChanges = mod.triggerSuggestPrChanges
setSuggestPrChangesDependencies = mod.setSuggestPrChangesDependencies
resetSuggestPrChangesDependencies = mod.resetSuggestPrChangesDependencies
})
let mockReq: Partial<AuthorizedUserReq>
let mockRes: Partial<Response>
let mockDependencies: any
beforeEach(() => {
// Setup response mock with proper typing
mockRes = {
status: jest.fn().mockReturnThis() as any,
send: jest.fn() as any,
json: jest.fn() as any,
}
// Setup comprehensive mock dependencies
mockDependencies = {
prisma: {
optimization_features: {
findUnique: jest.fn(),
update: jest.fn(),
},
optimization_events: {
update: jest.fn(),
},
},
userNickname: jest.fn(),
getInstallationOctokitByOwner: jest.fn(),
isUserCollaborator: jest.fn(),
requiresApproval: jest.fn(),
requestApproval: jest.fn(),
posthog: {
capture: jest.fn(),
},
githubApp: {},
isDiffContentsWellFormed: jest.fn(),
fileDiffsToMap: jest.fn(),
determineValidHunks: jest.fn(),
buildDependentPrTitle: jest.fn(),
buildPrCommentBody: jest.fn(),
createNewBranchFromDiffContents: jest.fn(),
createDependentPullRequest: jest.fn(),
sendSlackMessage: jest.fn(),
updateOptimizationEvent: jest.fn(),
}
setSuggestPrChangesDependencies(mockDependencies)
})
afterEach(() => {
resetSuggestPrChangesDependencies()
jest.clearAllMocks()
})
describe("suggestPrChanges", () => {
describe("input validation", () => {
it("should return 400 for missing required fields", async () => {
mockReq = {
body: {
owner: "test-owner",
// missing repo, pullNumber, diffContents
},
userId: "test-user-id",
} as AuthorizedUserReq
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.send).toHaveBeenCalledWith("Missing or malformed fields")
})
it("should return 400 for malformed diff contents", async () => {
mockReq = {
body: {
owner: "test-owner",
repo: "test-repo",
pullNumber: 1,
diffContents: "invalid-diff",
},
userId: "test-user-id",
} as AuthorizedUserReq
mockDependencies.isDiffContentsWellFormed.mockReturnValue(false)
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.send).toHaveBeenCalledWith("Missing or malformed fields")
})
})
describe("authentication and authorization", () => {
beforeEach(() => {
mockReq = {
body: {
owner: "test-owner",
repo: "test-repo",
pullNumber: 1,
diffContents: [{ file: "test.js", content: "test" }],
prCommentFields: { function_name: "testFunc" },
},
userId: "test-user-id",
} as AuthorizedUserReq
mockDependencies.isDiffContentsWellFormed.mockReturnValue(true)
})
it("should return 401 when user nickname is null", async () => {
mockDependencies.userNickname.mockResolvedValue(null)
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(401)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Unauthorized" })
})
it("should return 401 when installation octokit fails", async () => {
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(
new Error("Installation error"),
)
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(401)
expect(mockRes.send).toHaveBeenCalledWith("Installation error")
})
it("should return 401 when user is not a collaborator", async () => {
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
mockDependencies.isUserCollaborator.mockResolvedValue(false)
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(consoleSpy).toHaveBeenCalledWith(
"test-user is not a collaborator on test-owner/test-repo",
)
expect(mockRes.status).toHaveBeenCalledWith(401)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Unauthorized" })
consoleSpy.mockRestore()
})
it("should return 401 for roboflow repositories", async () => {
mockReq.body.owner = "roboflow"
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue({})
mockDependencies.isUserCollaborator.mockResolvedValue(true)
mockDependencies.requiresApproval.mockReturnValue(false)
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(consoleSpy).toHaveBeenCalledWith("Rejecting request for roboflow repository")
expect(mockRes.status).toHaveBeenCalledWith(401)
expect(mockRes.send).toHaveBeenCalledWith("Unauthorized for roboflow repositories")
consoleSpy.mockRestore()
})
})
describe("approval workflow", () => {
beforeEach(() => {
mockReq = {
body: {
owner: "test-owner",
repo: "test-repo",
pullNumber: 1,
diffContents: [{ file: "test.js", content: "test" }],
prCommentFields: { function_name: "testFunc" },
traceId: "test-trace-id",
},
userId: "test-user-id",
} as AuthorizedUserReq
mockDependencies.isDiffContentsWellFormed.mockReturnValue(true)
mockDependencies.userNickname.mockResolvedValue("test-user")
// Create a properly typed mock installation octokit
const mockInstallationOctokit = {
rest: {
pulls: {
get: jest.fn() as any,
},
},
}
// Fix: Use any to bypass TypeScript issues
mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
data: { head: { ref: "feature-branch" } },
})
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockInstallationOctokit)
mockDependencies.isUserCollaborator.mockResolvedValue(true)
})
it("should return 403 for rejected requests", async () => {
mockDependencies.requiresApproval.mockReturnValue(true)
mockDependencies.prisma.optimization_features.findUnique.mockResolvedValue({
approval_status: "rejected",
})
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(403)
expect(mockRes.json).toHaveBeenCalledWith({
status: "rejected",
message: "This optimization request was rejected.",
})
})
it("should proceed with approved requests", async () => {
mockDependencies.requiresApproval.mockReturnValue(true)
mockDependencies.prisma.optimization_features.findUnique.mockResolvedValue({
approval_status: "approved",
})
// Mock triggerSuggestPrChanges dependencies
mockDependencies.fileDiffsToMap.mockReturnValue(new Map())
mockDependencies.determineValidHunks.mockResolvedValue({
validHunks: new Map(),
invalidHunks: new Map(),
})
mockDependencies.buildDependentPrTitle.mockReturnValue("Test PR Title")
mockDependencies.createNewBranchFromDiffContents.mockResolvedValue(true)
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(consoleSpy).toHaveBeenCalledWith(
"Request test-trace-id was previously approved, continuing with PR suggestion",
)
expect(mockRes.json).toHaveBeenCalled()
consoleSpy.mockRestore()
})
it("should request approval for new requests", async () => {
mockDependencies.requiresApproval.mockReturnValue(true)
mockDependencies.prisma.optimization_features.findUnique.mockResolvedValue({
approval_status: "pending",
})
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(mockDependencies.requestApproval).toHaveBeenCalledWith(
"test-trace-id",
"test-owner",
"test-repo",
"testFunc",
"test-user-id",
expect.any(Object),
)
expect(mockRes.status).toHaveBeenCalledWith(202)
expect(mockRes.json).toHaveBeenCalledWith({
status: "pending_approval",
message:
"This PR suggestion requires approval. You will be notified when it is processed.",
})
})
})
describe("error handling", () => {
it("should handle errors and track them", async () => {
mockReq = {
body: {
owner: "test-owner",
repo: "test-repo",
pullNumber: 1,
diffContents: [{ file: "test.js", content: "test" }],
},
userId: "test-user-id",
} as AuthorizedUserReq
const error = new Error("Test error")
error.stack = "Test stack trace"
mockDependencies.isDiffContentsWellFormed.mockReturnValue(true)
mockDependencies.userNickname.mockRejectedValue(error)
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await suggestPrChanges(mockReq as AuthorizedUserReq, mockRes as Response)
expect(consoleSpy).toHaveBeenCalledWith(`Error in /cfapi/suggest-pr-changes: ${error}`)
expect(consoleSpy).toHaveBeenCalledWith(`Error message: ${error.message}`)
expect(consoleSpy).toHaveBeenCalledWith(`Error stack: ${error.stack}`)
expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
distinctId: "test-user-id",
event: "cfapi-suggest-pr-changes-failed-error",
properties: {
error: error.message,
stack: error.stack,
},
})
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.send).toHaveBeenCalledWith(`Error creating pull request: ${error.message}`)
consoleSpy.mockRestore()
})
})
})
describe("triggerSuggestPrChanges", () => {
let mockInstallationOctokit: any
beforeEach(() => {
mockInstallationOctokit = {
rest: {
pulls: {
get: jest.fn() as any,
createReview: jest.fn() as any,
},
},
}
// Setup default mocks
mockDependencies.fileDiffsToMap.mockReturnValue(new Map())
mockDependencies.determineValidHunks.mockResolvedValue({
validHunks: new Map(),
invalidHunks: new Map(),
})
mockDependencies.buildDependentPrTitle.mockReturnValue("Test PR Title")
mockDependencies.createNewBranchFromDiffContents.mockResolvedValue(true)
})
describe("dependent PR creation", () => {
it("should create dependent PR when there are invalid hunks", async () => {
const validHunks = new Map([
["file1.js", [{ oldStart: 1, oldEnd: 5, newContent: ["new code"] }]],
])
const invalidHunks = new Map([
["file2.js", [{ oldStart: 10, oldEnd: 15, newContent: ["invalid code"] }]],
])
mockDependencies.determineValidHunks.mockResolvedValue({
validHunks,
invalidHunks,
})
// Fix: Use any to bypass TypeScript issues
mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
data: { head: { ref: "feature-branch" } },
})
mockDependencies.createDependentPullRequest.mockResolvedValue({
data: {
id: 123,
number: 456,
html_url: "https://github.com/test/test/pull/456",
head: { ref: "codeflash/optimize-pr1-2024-01-01" },
},
})
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
const result = await triggerSuggestPrChanges(
"test-owner",
"test-repo",
1,
[],
{
function_name: "testFunc",
speedup_pct: 50,
speedup_x: 2,
optimization_explanation: "test",
},
"existing tests",
"generated tests",
"coverage message",
"user123",
"testuser",
mockInstallationOctokit,
"trace123",
)
expect(consoleSpy).toHaveBeenCalledWith(
"Creating a dependent PR because there are invalid hunks.",
)
expect(consoleSpy).toHaveBeenCalledWith("Making a new dependent PR...")
expect(consoleSpy).toHaveBeenCalledWith(
"Created new dependent PR #456 from branch codeflash/optimize-pr1-2024-01-01",
)
expect(mockDependencies.createDependentPullRequest).toHaveBeenCalled()
expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
distinctId: "user123",
event: "cfapi-suggest-pr-changes-success-dependent-pr-created",
properties: {
owner: "test-owner",
repo: "test-repo",
newPrNumber: 456,
newPrBranch: "codeflash/optimize-pr1-2024-01-01",
PRURL: "https://github.com/test/test/pull/456",
},
})
expect(result).toBe(456)
consoleSpy.mockRestore()
})
it("should create dependent PR when there are multiple files", async () => {
const validHunks = new Map([
["file1.js", [{ oldStart: 1, oldEnd: 5, newContent: ["new code"] }]],
["file2.js", [{ oldStart: 10, oldEnd: 15, newContent: ["more code"] }]],
])
const invalidHunks = new Map()
mockDependencies.determineValidHunks.mockResolvedValue({
validHunks,
invalidHunks,
})
mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
data: { head: { ref: "feature-branch" } },
})
mockDependencies.createDependentPullRequest.mockResolvedValue({
data: {
id: 123,
number: 456,
html_url: "https://github.com/test/test/pull/456",
head: { ref: "codeflash/optimize-pr1-2024-01-01" },
},
})
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
const result = await triggerSuggestPrChanges(
"test-owner",
"test-repo",
1,
[],
{
function_name: "testFunc",
speedup_pct: 50,
speedup_x: 2,
optimization_explanation: "test",
},
"existing tests",
"generated tests",
"coverage message",
"user123",
"testuser",
mockInstallationOctokit,
"trace123",
)
expect(consoleSpy).toHaveBeenCalledWith(
"Found 2 files with changes, using dependent PR instead of review comments",
)
expect(consoleSpy).toHaveBeenCalledWith(
"Creating a dependent PR because there are multiple valid hunks.",
)
expect(result).toBe(456)
consoleSpy.mockRestore()
})
it("should send Slack notification for configured repositories", async () => {
const validHunks = new Map([
["file1.js", [{ oldStart: 1, oldEnd: 5, newContent: ["new code"] }]],
])
const invalidHunks = new Map([
["file2.js", [{ oldStart: 10, oldEnd: 15, newContent: ["invalid code"] }]],
])
mockDependencies.determineValidHunks.mockResolvedValue({
validHunks,
invalidHunks,
})
mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
data: { head: { ref: "feature-branch" } },
})
mockDependencies.createDependentPullRequest.mockResolvedValue({
data: {
id: 123,
number: 456,
html_url: "https://github.com/test/test/pull/456",
head: { ref: "codeflash/optimize-pr1-2024-01-01" },
},
})
await triggerSuggestPrChanges(
"Future-House", // This is in slackNotificationConfig
"aviary", // This repo is configured for notifications
1,
[],
{
function_name: "testFunc",
speedup_pct: 50,
speedup_x: 2,
optimization_explanation: "test",
},
"existing tests",
"generated tests",
"coverage message",
"user123",
"testuser",
mockInstallationOctokit,
"trace123",
)
expect(mockDependencies.sendSlackMessage).toHaveBeenCalledWith(
"new dependent PR created: https://github.com/test/test/pull/456 for Future-House/aviary",
)
})
})
describe("review comment creation", () => {
it("should create review comments for single valid hunk", async () => {
const validHunks = new Map([
[
"file1.js",
[{ oldStart: 1, oldEnd: 5, newContent: ["new code line 1", "new code line 2"] }],
],
])
const invalidHunks = new Map()
mockDependencies.determineValidHunks.mockResolvedValue({
validHunks,
invalidHunks,
})
mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
data: {
head: { ref: "feature-branch", sha: "abc123" },
},
})
mockInstallationOctokit.rest.pulls.createReview.mockResolvedValue({
data: {
id: 789,
html_url: "https://github.com/test/test/pull/1#pullrequestreview-789",
},
})
mockDependencies.buildPrCommentBody.mockReturnValue("Test PR comment body")
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
const result = await triggerSuggestPrChanges(
"test-owner",
"test-repo",
1,
[],
{
function_name: "testFunc",
speedup_pct: 50,
speedup_x: 2,
optimization_explanation: "test",
},
"existing tests",
"generated tests",
"coverage message",
"user123",
"testuser",
mockInstallationOctokit,
"trace123",
)
expect(consoleSpy).toHaveBeenCalledWith("Creating unified review for a single valid hunk.")
expect(consoleSpy).toHaveBeenCalledWith(
"Added review comment to PR #1: https://github.com/test/test/pull/1#pullrequestreview-789",
)
expect(mockInstallationOctokit.rest.pulls.createReview).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
pull_number: 1,
commit_id: "abc123",
event: "COMMENT",
comments: [
{
path: "file1.js",
line: 5,
start_line: 1,
side: "RIGHT",
body: "Test PR comment body\n\n```suggestion\nnew code line 1\nnew code line 2\n```",
},
],
})
expect(result).toBe(789)
consoleSpy.mockRestore()
})
it("should handle review creation failure gracefully", async () => {
const validHunks = new Map([
[
"file1.js",
[{ oldStart: 5, oldEnd: 1, newContent: ["new code"] }], // Invalid range (start > end)
],
])
const invalidHunks = new Map()
mockDependencies.determineValidHunks.mockResolvedValue({
validHunks,
invalidHunks,
})
mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
data: { head: { ref: "feature-branch", sha: "abc123" } },
})
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
const result = await triggerSuggestPrChanges(
"test-owner",
"test-repo",
1,
[],
{
function_name: "testFunc",
speedup_pct: 50,
speedup_x: 2,
optimization_explanation: "test",
},
"existing tests",
"generated tests",
"coverage message",
"user123",
"testuser",
mockInstallationOctokit,
"trace123",
)
expect(consoleSpy).toHaveBeenCalledWith(
"Found invalid review range for file1.js: start_line (5) must be less than line (1) with content new code",
)
expect(result).toBe(null)
consoleSpy.mockRestore()
})
})
describe("error handling", () => {
it("should handle branch creation failure", async () => {
mockDependencies.createNewBranchFromDiffContents.mockResolvedValue(false)
mockInstallationOctokit.rest.pulls.get.mockResolvedValue({
data: { head: { ref: "feature-branch" } },
})
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
const result = await triggerSuggestPrChanges(
"test-owner",
"test-repo",
1,
[],
{
function_name: "testFunc",
speedup_pct: 50,
speedup_x: 2,
optimization_explanation: "test",
},
"existing tests",
"generated tests",
"coverage message",
"user123",
"testuser",
mockInstallationOctokit,
"trace123",
mockRes as Response,
)
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining("Error in triggerSuggestPrChanges:"),
)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.send).toHaveBeenCalledWith(
expect.stringContaining("Error creating pull request:"),
)
consoleErrorSpy.mockRestore()
})
})
})
})

View file

@ -0,0 +1,634 @@
import { jest, describe, it, expect, beforeEach, afterEach, beforeAll } from "@jest/globals"
import { Request, Response } from "express"
let verifyExistingOptimizations: typeof import("../verify-existing-optimizations").verifyExistingOptimizations
let setVerifyExistingOptimizationsDependencies: typeof import("../verify-existing-optimizations").setVerifyExistingOptimizationsDependencies
let resetVerifyExistingOptimizationsDependencies: typeof import("../verify-existing-optimizations").resetVerifyExistingOptimizationsDependencies
// Extend Request interface to include userId for testing
interface TestRequest extends Request {
userId: string
}
describe("verifyExistingOptimizations", () => {
beforeAll(async () => {
process.env.KEY_VAULT_NAME = "mocked-keyvault-name"
const mod = await import("../verify-existing-optimizations")
verifyExistingOptimizations = mod.verifyExistingOptimizations
setVerifyExistingOptimizationsDependencies = mod.setVerifyExistingOptimizationsDependencies
resetVerifyExistingOptimizationsDependencies = mod.resetVerifyExistingOptimizationsDependencies
})
let mockReq: Partial<TestRequest>
let mockRes: Partial<Response>
let mockDependencies: any
let mockOctokit: any
beforeEach(() => {
// Setup request mock
mockReq = {
body: {
repo_owner: "test-owner",
repo_name: "test-repo",
pr_number: 123,
},
userId: "test-user-id", // Now properly typed
}
// Setup response mock
mockRes = {
status: jest.fn().mockReturnThis() as any,
json: jest.fn() as any,
send: jest.fn() as any,
}
// Setup mock octokit
mockOctokit = {
rest: {
pulls: {
get: jest.fn(),
listReviews: jest.fn(),
listCommentsForReview: jest.fn(),
listReviewComments: jest.fn(),
},
issues: {
listComments: jest.fn(),
},
},
}
// Setup mock dependencies
mockDependencies = {
getInstallationOctokitByOwner: jest.fn(),
githubApp: {},
parseAndCreateOptimizationsDict: jest.fn(),
posthog: {
capture: jest.fn(),
},
userNickname: jest.fn(),
isUserCollaborator: jest.fn(),
}
setVerifyExistingOptimizationsDependencies(mockDependencies)
})
afterEach(() => {
resetVerifyExistingOptimizationsDependencies()
jest.clearAllMocks()
})
describe("input validation", () => {
it("should return 400 for missing required fields", async () => {
mockReq.body = { repo_owner: "test-owner" } // Missing repo_name and pr_number
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Missing or malformed fields" })
})
it("should return 400 for zero PR number", async () => {
mockReq.body = {
repo_owner: "test-owner",
repo_name: "test-repo",
pr_number: 0, // 0 is falsy and should fail validation
}
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Missing or malformed fields" })
})
it("should return 401 when user nickname is null", async () => {
mockDependencies.userNickname.mockResolvedValue(null)
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(401)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Unauthorized" })
})
it("should return 500 when installation octokit fails", async () => {
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(
new Error("Installation error"),
)
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Installation error" })
})
})
describe("collaborator verification", () => {
beforeEach(() => {
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
})
it("should return 401 when user is not a collaborator", async () => {
mockDependencies.isUserCollaborator.mockResolvedValue(false)
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(401)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Unauthorized - User is not a collaborator",
})
expect(consoleSpy).toHaveBeenCalledWith(
"test-user is not a collaborator on test-owner/test-repo",
)
consoleSpy.mockRestore()
})
it("should return 500 when collaborator check fails", async () => {
const error = new Error("API error")
mockDependencies.isUserCollaborator.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({ error: "Failed to verify collaborator status" })
// Fix: console.error concatenates the error as a string
expect(consoleErrorSpy).toHaveBeenCalledWith(`Error checking collaborator status: ${error}`)
consoleErrorSpy.mockRestore()
})
})
describe("PR retrieval", () => {
beforeEach(() => {
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
mockDependencies.isUserCollaborator.mockResolvedValue(true)
})
it("should return 404 when PR is not found", async () => {
const error = new Error("Not found")
;(error as any).status = 404
mockOctokit.rest.pulls.get.mockRejectedValue(error)
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({
error: "PR #123 not found for test-owner/test-repo",
})
})
it("should handle non-404 PR retrieval errors", async () => {
const error = new Error("API error")
;(error as any).status = 500
mockOctokit.rest.pulls.get.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
// Fix: console.error concatenates the error as a string
expect(consoleErrorSpy).toHaveBeenCalledWith(
`Error in /cfapi/verify-existing-optimizations: ${error}`,
)
expect(mockRes.status).toHaveBeenCalledWith(500)
consoleErrorSpy.mockRestore()
})
it("should successfully retrieve PR data", async () => {
mockOctokit.rest.pulls.get.mockResolvedValue({
data: { body: "test body" },
})
mockOctokit.rest.issues.listComments.mockResolvedValue({ data: [] })
mockOctokit.rest.pulls.listReviews.mockResolvedValue({ data: [] })
mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({ data: [] })
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockOctokit.rest.pulls.get).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
pull_number: 123,
})
})
})
describe("comments and reviews retrieval", () => {
beforeEach(() => {
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
mockDependencies.isUserCollaborator.mockResolvedValue(true)
mockOctokit.rest.pulls.get.mockResolvedValue({
data: { body: "test body" },
})
})
it("should handle PR comments retrieval failure", async () => {
const error = new Error("Comments API error")
mockOctokit.rest.issues.listComments.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting PR messages:", error)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Failed to retrieve PR comments for test-owner/test-repo",
})
consoleErrorSpy.mockRestore()
})
it("should handle PR reviews retrieval failure", async () => {
mockOctokit.rest.issues.listComments.mockResolvedValue({ data: [] })
const error = new Error("Reviews API error")
mockOctokit.rest.pulls.listReviews.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting PR reviews:", error)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Failed to retrieve PR reviews for test-owner/test-repo",
})
consoleErrorSpy.mockRestore()
})
it("should successfully process reviews and comments", async () => {
mockOctokit.rest.issues.listComments.mockResolvedValue({
data: [{ body: "Regular comment" }],
})
mockOctokit.rest.pulls.listReviews.mockResolvedValue({
data: [
{ id: 1, body: "Review body 1" },
{ id: 2, body: "Review body 2" },
],
})
mockOctokit.rest.pulls.listCommentsForReview.mockResolvedValue({
data: [{ body: "Review comment" }],
})
mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({
data: [{ body: "Additional review comment" }],
})
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
function1: new Set(["optimization1"]),
})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
// Verify that all comments types are collected
expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
"test body",
expect.arrayContaining([
{ body: "Regular comment" },
{ body: "Review body 1" },
{ body: "Review body 2" },
{ body: "Review comment" },
{ body: "Additional review comment" },
]),
)
})
it("should filter out comments with undefined body", async () => {
mockOctokit.rest.issues.listComments.mockResolvedValue({
data: [{ body: "Valid comment" }, { body: undefined }, { body: "Another valid comment" }],
})
mockOctokit.rest.pulls.listReviews.mockResolvedValue({ data: [] })
mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({ data: [] })
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
function1: new Set(["optimization1"]),
})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith("test body", [
{ body: "Valid comment" },
{ body: "Another valid comment" },
])
})
})
describe("optimizations processing", () => {
beforeEach(() => {
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
mockDependencies.isUserCollaborator.mockResolvedValue(true)
mockOctokit.rest.pulls.get.mockResolvedValue({
data: { body: "test body" },
})
mockOctokit.rest.issues.listComments.mockResolvedValue({
data: [{ body: "test comment" }],
})
mockOctokit.rest.pulls.listReviews.mockResolvedValue({ data: [] })
mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({ data: [] })
})
it("should return 404 when no optimizations are found", async () => {
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({
error: "No optimizations found for this PR",
})
})
it("should handle null PR body", async () => {
mockOctokit.rest.pulls.get.mockResolvedValue({
data: { body: null },
})
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
"", // null body should become empty string
[{ body: "test comment" }],
)
})
it("should handle undefined PR body", async () => {
mockOctokit.rest.pulls.get.mockResolvedValue({
data: { body: undefined },
})
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
"", // undefined body should become empty string
[{ body: "test comment" }],
)
})
it("should convert Set to array format and return optimizations", async () => {
const mockOptimizationsDict = {
function1: new Set(["optimization1", "optimization2"]),
function2: new Set(["optimization3"]),
}
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue(mockOptimizationsDict)
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
const expectedResponse = {
function1: ["optimization1", "optimization2"],
function2: ["optimization3"],
}
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
})
it("should track analytics event", async () => {
const mockOptimizationsDict = {
function1: new Set(["optimization1"]),
}
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue(mockOptimizationsDict)
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockDependencies.posthog.capture).toHaveBeenCalledWith({
distinctId: "test-user-id",
event: "cfapi-github-pr-optimization",
properties: {
repo_owner: "test-owner",
repo_name: "test-repo",
pr_number: 123,
},
})
})
it("should handle empty sets in optimizations dict", async () => {
const mockOptimizationsDict = {
function1: new Set([]),
function2: new Set(["optimization1"]),
}
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue(mockOptimizationsDict)
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
const expectedResponse = {
function1: [],
function2: ["optimization1"],
}
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
})
})
describe("error handling", () => {
beforeEach(() => {
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
mockDependencies.isUserCollaborator.mockResolvedValue(true)
})
it("should handle general errors with Error objects", async () => {
const error = new Error("Unexpected error")
mockOctokit.rest.pulls.get.mockRejectedValue(error)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
// Fix: console.error concatenates the error as a string
expect(consoleErrorSpy).toHaveBeenCalledWith(
`Error in /cfapi/verify-existing-optimizations: ${error}`,
)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Error verifying existing optimizations: Unexpected error",
})
consoleErrorSpy.mockRestore()
})
it("should handle non-Error exceptions", async () => {
mockOctokit.rest.pulls.get.mockRejectedValue("String error")
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Error verifying existing optimizations",
})
consoleErrorSpy.mockRestore()
})
})
describe("edge cases", () => {
beforeEach(() => {
mockDependencies.userNickname.mockResolvedValue("test-user")
mockDependencies.getInstallationOctokitByOwner.mockResolvedValue(mockOctokit)
mockDependencies.isUserCollaborator.mockResolvedValue(true)
mockOctokit.rest.pulls.get.mockResolvedValue({
data: { body: "test body" },
})
mockOctokit.rest.issues.listComments.mockResolvedValue({ data: [] })
mockOctokit.rest.pulls.listReviews.mockResolvedValue({ data: [] })
mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({ data: [] })
})
it("should handle review comment retrieval failures gracefully", async () => {
mockOctokit.rest.pulls.listReviews.mockResolvedValue({
data: [{ id: 1, body: "Review body" }],
})
mockOctokit.rest.pulls.listCommentsForReview.mockRejectedValue(new Error("Review API error"))
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
function1: new Set(["optimization1"]),
})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Error getting review comments for review 1:",
expect.any(Error),
)
// Should still continue and succeed
expect(mockRes.status).toHaveBeenCalledWith(200)
consoleErrorSpy.mockRestore()
})
it("should handle all review comments retrieval failure gracefully", async () => {
mockOctokit.rest.pulls.listReviewComments.mockRejectedValue(
new Error("All review comments API error"),
)
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
function1: new Set(["optimization1"]),
})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Error getting all review comments:",
expect.any(Error),
)
// Should still continue and succeed
expect(mockRes.status).toHaveBeenCalledWith(200)
consoleErrorSpy.mockRestore()
})
it("should handle valid non-zero PR number", async () => {
mockReq.body.pr_number = 999
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockOctokit.rest.pulls.get).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
pull_number: 999,
})
})
it("should handle very large optimizations dict", async () => {
const largeOptimizationsDict: { [key: string]: Set<string> } = {}
// Create 100 functions with 50 optimizations each
for (let i = 0; i < 100; i++) {
const functionName = `function${i}`
const optimizations = new Set<string>()
for (let j = 0; j < 50; j++) {
optimizations.add(`optimization${i}_${j}`)
}
largeOptimizationsDict[functionName] = optimizations
}
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue(largeOptimizationsDict)
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockRes.status).toHaveBeenCalledWith(200)
// Verify the response structure is correct
const jsonCall = (mockRes.json as jest.MockedFunction<any>).mock.calls[0][0]
expect(Object.keys(jsonCall)).toHaveLength(100)
expect(jsonCall.function0).toHaveLength(50)
expect(jsonCall.function99).toHaveLength(50)
})
it("should handle reviews with no body", async () => {
mockOctokit.rest.pulls.listReviews.mockResolvedValue({
data: [
{ id: 1, body: "Review with body" },
{ id: 2, body: null },
{ id: 3, body: "" },
{ id: 4 }, // no body property
],
})
mockOctokit.rest.pulls.listCommentsForReview.mockResolvedValue({ data: [] })
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
function1: new Set(["optimization1"]),
})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
// Should only include reviews with truthy body values
expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
"test body",
expect.arrayContaining([{ body: "Review with body" }]),
)
})
it("should handle complex review structure", async () => {
mockOctokit.rest.pulls.listReviews.mockResolvedValue({
data: [{ id: 1, body: "Main review body" }],
})
mockOctokit.rest.pulls.listCommentsForReview.mockResolvedValue({
data: [
{ body: "Review comment 1" },
{ body: "Review comment 2" },
{ body: null }, // Should be filtered out
],
})
mockOctokit.rest.pulls.listReviewComments.mockResolvedValue({
data: [{ body: "General review comment 1" }, { body: "General review comment 2" }],
})
mockDependencies.parseAndCreateOptimizationsDict.mockReturnValue({
function1: new Set(["optimization1"]),
})
await verifyExistingOptimizations(mockReq as Request, mockRes as Response)
expect(mockDependencies.parseAndCreateOptimizationsDict).toHaveBeenCalledWith(
"test body",
expect.arrayContaining([
{ body: "Main review body" },
{ body: "Review comment 1" },
{ body: "Review comment 2" },
{ body: "General review comment 1" },
{ body: "General review comment 2" },
]),
)
})
})
})

View file

@ -5,6 +5,44 @@ import { parseAndCreateOptimizationsDict } from "../github/pr-changes-utils.js"
import { posthog } from "../analytics.js"
import { userNickname } from "../auth0-mgmt.js"
// Dependencies interface for easier testing
export interface VerifyExistingOptimizationsDependencies {
getInstallationOctokitByOwner: typeof getInstallationOctokitByOwner
githubApp: typeof githubApp
parseAndCreateOptimizationsDict: typeof parseAndCreateOptimizationsDict
posthog: typeof posthog
userNickname: typeof userNickname
isUserCollaborator: typeof isUserCollaborator
}
// Default dependencies
let dependencies: VerifyExistingOptimizationsDependencies = {
getInstallationOctokitByOwner,
githubApp,
parseAndCreateOptimizationsDict,
posthog,
userNickname,
isUserCollaborator,
}
// For testing - allow dependency injection
export function setVerifyExistingOptimizationsDependencies(
deps: Partial<VerifyExistingOptimizationsDependencies>,
) {
dependencies = { ...dependencies, ...deps }
}
export function resetVerifyExistingOptimizationsDependencies() {
dependencies = {
getInstallationOctokitByOwner,
githubApp,
parseAndCreateOptimizationsDict,
posthog,
userNickname,
isUserCollaborator,
}
}
export async function verifyExistingOptimizations(req: Request, res: Response) {
try {
const { repo_owner, repo_name, pr_number } = req.body
@ -14,19 +52,30 @@ export async function verifyExistingOptimizations(req: Request, res: Response) {
return res.status(400).json({ error: "Missing or malformed fields" })
}
const nickname: string | null = await userNickname(userId)
const nickname: string | null = await dependencies.userNickname(userId)
if (nickname == null) {
return res.status(401).json({ error: "Unauthorized" })
}
const octokit = await getInstallationOctokitByOwner(githubApp, repo_owner, repo_name)
const octokit = await dependencies.getInstallationOctokitByOwner(
dependencies.githubApp,
repo_owner,
repo_name,
)
if (octokit instanceof Error) {
return res.status(500).json({ error: octokit.message })
}
console.log(`Got installation Octokit for ${repo_owner}/${repo_name}`)
// Check collaborator status with error handling
try {
const isCollaborator = await isUserCollaborator(octokit, repo_owner, repo_name, nickname)
const isCollaborator = await dependencies.isUserCollaborator(
octokit,
repo_owner,
repo_name,
nickname,
)
if (!isCollaborator) {
console.log(`${nickname} is not a collaborator on ${repo_owner}/${repo_name}`)
return res.status(401).json({ error: "Unauthorized - User is not a collaborator" })
@ -82,7 +131,7 @@ export async function verifyExistingOptimizations(req: Request, res: Response) {
error: `Failed to retrieve PR reviews for ${repo_owner}/${repo_name}`,
})
}
//console.log("pr reviews:", pr_reviews)
const reviewBodies: { body: string }[] = []
for (const review of pr_reviews.data) {
// Add the main review body if it exists
@ -98,7 +147,6 @@ export async function verifyExistingOptimizations(req: Request, res: Response) {
pull_number: pr_number,
review_id: review.id,
})
// console.log(`Review comments for review ${review.id}:`, reviewComments)
// Add each review comment body
for (const comment of reviewComments.data) {
if (comment.body) {
@ -118,7 +166,6 @@ export async function verifyExistingOptimizations(req: Request, res: Response) {
repo: repo_name,
pull_number: pr_number,
})
// console.log("All review comments:", allReviewComments)
for (const comment of allReviewComments.data) {
if (comment.body) {
reviewBodies.push({ body: comment.body })
@ -134,10 +181,10 @@ export async function verifyExistingOptimizations(req: Request, res: Response) {
(comment: { body?: string }) => comment.body !== undefined,
) as { body: string }[]
const allComments = [...validComments, ...reviewBodies]
const optimizations_dict = parseAndCreateOptimizationsDict(prBody, allComments)
const optimizations_dict = dependencies.parseAndCreateOptimizationsDict(prBody, allComments)
if (Object.keys(optimizations_dict).length === 0) {
return res.status(200).json({ error: "No optimizations found for this PR" })
return res.status(404).json({ error: "No optimizations found for this PR" })
}
const response_dict: { [key: string]: string[] } = {}
@ -145,7 +192,7 @@ export async function verifyExistingOptimizations(req: Request, res: Response) {
response_dict[key] = Array.from(optimizations_dict[key])
}
posthog.capture({
dependencies.posthog.capture({
distinctId: userId,
event: `cfapi-github-pr-optimization`,
properties: {

View file

@ -12,13 +12,9 @@ import type { RestEndpointMethodTypes } from "@octokit/rest"
import { buildResultHeader, buildResultDetails, buildResultTestReport } from "./pr-changes-utils.js"
import { AnyOctokit, PullRequestCreationResponse } from "../types.js"
type PullsCreateResponse = RestEndpointMethodTypes["pulls"]["create"]["response"]
type GitCreateTreeParamsTree =
RestEndpointMethodTypes["git"]["createTree"]["parameters"]["tree"][number]
type Response<T> = {
status: number
data: T
}
interface BenchmarkDetail {
benchmark_name: string
test_function: string
@ -40,6 +36,53 @@ export interface PrCommentFields {
benchmark_details?: BenchmarkDetail[]
}
// Dependencies interface for easier testing
export interface CreatePrFromDiffContentsDependencies {
addLabelToPullRequest: typeof addLabelToPullRequest
buildBenchmarkInfo: typeof buildBenchmarkInfo
buildDependentPrTitle: typeof buildDependentPrTitle
buildPrTitle: typeof buildPrTitle
buildResultFooter: typeof buildResultFooter
buildResultHeader: typeof buildResultHeader
buildResultDetails: typeof buildResultDetails
buildResultTestReport: typeof buildResultTestReport
originalPRComment: typeof originalPRComment
}
// Default dependencies
let dependencies: CreatePrFromDiffContentsDependencies = {
addLabelToPullRequest,
buildBenchmarkInfo,
buildDependentPrTitle,
buildPrTitle,
buildResultFooter,
buildResultHeader,
buildResultDetails,
buildResultTestReport,
originalPRComment,
}
// For testing - allow dependency injection
export function setCreatePrFromDiffContentsDependencies(
deps: Partial<CreatePrFromDiffContentsDependencies>,
) {
dependencies = { ...dependencies, ...deps }
}
export function resetCreatePrFromDiffContentsDependencies() {
dependencies = {
addLabelToPullRequest,
buildBenchmarkInfo,
buildDependentPrTitle,
buildPrTitle,
buildResultFooter,
buildResultHeader,
buildResultDetails,
buildResultTestReport,
originalPRComment,
}
}
export async function createNewBranchFromDiffContents(
installationOctokit: Octokit,
owner: string,
@ -49,80 +92,85 @@ export async function createNewBranchFromDiffContents(
diffContentsMap: Map<string, FileDiffContent>,
commitMessage: string,
): Promise<boolean> {
// Create a new branch off of the specified base branch
await installationOctokit.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${newBranchName}`,
sha: (
await installationOctokit.rest.git.getRef({
owner,
repo,
ref: `heads/${baseBranch}`,
})
).data.object.sha,
})
const treeItems: GitCreateTreeParamsTree[] = []
for (const [filePath, fileDiffContent] of diffContentsMap) {
// Create blobs for the changed files with newContent
const blobData = await installationOctokit.rest.git.createBlob({
try {
// Create a new branch off of the specified base branch
await installationOctokit.rest.git.createRef({
owner,
repo,
content: fileDiffContent.newContent,
encoding: "utf-8",
ref: `refs/heads/${newBranchName}`,
sha: (
await installationOctokit.rest.git.getRef({
owner,
repo,
ref: `heads/${baseBranch}`,
})
).data.object.sha,
})
treeItems.push({
path: filePath,
mode: "100644", // blob
type: "blob",
sha: blobData.data.sha,
const treeItems: GitCreateTreeParamsTree[] = []
for (const [filePath, fileDiffContent] of diffContentsMap) {
// Create blobs for the changed files with newContent
const blobData = await installationOctokit.rest.git.createBlob({
owner,
repo,
content: fileDiffContent.newContent,
encoding: "utf-8",
})
treeItems.push({
path: filePath,
mode: "100644", // blob
type: "blob",
sha: blobData.data.sha,
})
}
// Get the current commit SHA of the new branch
const refData = await installationOctokit.rest.git.getRef({
owner,
repo,
ref: `heads/${newBranchName}`,
})
const currentCommitSha = refData.data.object.sha
// Get the tree SHA of the current commit
const commitData = await installationOctokit.rest.git.getCommit({
owner,
repo,
commit_sha: currentCommitSha,
})
const currentTreeSha = commitData.data.tree.sha
// Create a new tree with the changes
const newTreeData = await installationOctokit.rest.git.createTree({
owner,
repo,
base_tree: currentTreeSha,
tree: treeItems,
})
// Create a new commit with the new tree
const newCommitData = await installationOctokit.rest.git.createCommit({
owner,
repo,
message: commitMessage,
tree: newTreeData.data.sha,
parents: [currentCommitSha],
})
// Update the new branch reference to point to the new commit
const result = await installationOctokit.rest.git.updateRef({
owner,
repo,
ref: `heads/${newBranchName}`,
sha: newCommitData.data.sha,
})
return result.status === 200
} catch (error) {
console.error("Error creating branch from diff contents:", error)
return false
}
// Get the current commit SHA of the new branch
const refData = await installationOctokit.rest.git.getRef({
owner,
repo,
ref: `heads/${newBranchName}`,
})
const currentCommitSha = refData.data.object.sha
// Get the tree SHA of the current commit
const commitData = await installationOctokit.rest.git.getCommit({
owner,
repo,
commit_sha: currentCommitSha,
})
const currentTreeSha = commitData.data.tree.sha
// Create a new tree with the changes
const newTreeData = await installationOctokit.rest.git.createTree({
owner,
repo,
base_tree: currentTreeSha,
tree: treeItems,
})
// Create a new commit with the new tree
const newCommitData = await installationOctokit.rest.git.createCommit({
owner,
repo,
message: commitMessage,
tree: newTreeData.data.sha,
parents: [currentCommitSha],
})
// Update the new branch reference to point to the new commit
const result = await installationOctokit.rest.git.updateRef({
owner,
repo,
ref: `heads/${newBranchName}`,
sha: newCommitData.data.sha,
})
return result.status === 200
}
export async function createNewPullRequest(
@ -133,7 +181,6 @@ export async function createNewPullRequest(
body: string,
newBranchName: string,
baseBranch: string,
// ): Promise<Octokit.Response<Octokit.PullsCreateResponse>> {
): Promise<PullRequestCreationResponse> {
return await installationOctokit.rest.pulls.create({
owner,
@ -160,6 +207,53 @@ export async function assignReviewer(
})
}
export async function createStandalonePullRequest(
installationOctokit: AnyOctokit,
owner: string,
repo: string,
newBranchName: string,
baseBranch: string,
prCommentFields: PrCommentFields,
existingTests: string,
generatedTests: string,
coverage_message: string,
): Promise<PullRequestCreationResponse> {
// Open a new standalone Codeflash PR (that doesn't reference an original PR, likely just to main)
const prCommentHeader = dependencies.buildResultHeader(prCommentFields)
// Build benchmark info if available
const benchmarkInfo =
prCommentFields.benchmark_details && prCommentFields.benchmark_details.length > 0
? dependencies.buildBenchmarkInfo(prCommentFields)
: ""
// Open a new PR from the new branch onto the original PR's head branch
const prCommentBody = dependencies.buildResultDetails(prCommentFields)
const prCommentTestReport = dependencies.buildResultTestReport(
prCommentFields,
existingTests,
generatedTests,
coverage_message,
)
const prCommentFooter = dependencies.buildResultFooter(newBranchName)
const title: string = dependencies.buildPrTitle(
prCommentFields.function_name,
prCommentFields.speedup_pct,
prCommentFields.speedup_x,
)
const body: string = benchmarkInfo
? `${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
: `${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
return await createNewPullRequest(
installationOctokit,
owner,
repo,
title,
body,
newBranchName,
baseBranch,
)
}
export async function createDependentPullRequest(
installationOctokit: AnyOctokit,
owner: string,
@ -172,7 +266,7 @@ export async function createDependentPullRequest(
generatedTests: string,
coverage_message: string,
): Promise<PullRequestCreationResponse> {
let { title, body } = createDependentPRTitleAndBody(
const { title, body } = createDependentPRTitleAndBody(
prCommentFields,
existingTests,
generatedTests,
@ -181,6 +275,7 @@ export async function createDependentPullRequest(
newBranchName,
baseBranch,
)
const newPrData = await createNewPullRequest(
installationOctokit,
owner,
@ -190,11 +285,16 @@ export async function createDependentPullRequest(
newBranchName,
baseBranch,
)
await addLabelToPullRequest(installationOctokit, owner, repo, newPrData.data.number)
await dependencies.addLabelToPullRequest(installationOctokit, owner, repo, newPrData.data.number)
// Make a comment on the original PR with a link to the new PR
const commentBody: string = originalPRComment(prCommentFields, newPrData.data.number, baseBranch)
const result = await installationOctokit.rest.issues.createComment({
const commentBody: string = dependencies.originalPRComment(
prCommentFields,
newPrData.data.number,
baseBranch,
)
await installationOctokit.rest.issues.createComment({
owner,
repo,
issue_number: origPrNumber,
@ -213,22 +313,22 @@ function createDependentPRTitleAndBody(
newBranchName: string,
baseBranch: string,
): { title: string; body: string } {
const prCommentHeader = buildResultHeader(prCommentFields)
const prCommentHeader = dependencies.buildResultHeader(prCommentFields)
// Build benchmark info if available
const benchmarkInfo =
prCommentFields.benchmark_details && prCommentFields.benchmark_details.length > 0
? buildBenchmarkInfo(prCommentFields)
? dependencies.buildBenchmarkInfo(prCommentFields)
: ""
// Open a new PR from the new branch onto the original PR's head branch
const prCommentBody = buildResultDetails(prCommentFields)
const prCommentTestReport = buildResultTestReport(
const prCommentBody = dependencies.buildResultDetails(prCommentFields)
const prCommentTestReport = dependencies.buildResultTestReport(
prCommentFields,
existingTests,
generatedTests,
coverage_message,
)
const prCommentFooter = buildResultFooter(newBranchName)
const title = buildDependentPrTitle(
const prCommentFooter = dependencies.buildResultFooter(newBranchName)
const title = dependencies.buildDependentPrTitle(
prCommentFields.function_name,
prCommentFields.speedup_pct,
prCommentFields.speedup_x,
@ -245,5 +345,5 @@ If you approve this dependent PR, these changes will be merged into the original
? `${introSection}${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
: `${introSection}${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
return { title: title, body: body }
return { title, body }
}

View file

@ -18,406 +18,452 @@ import {
} from "@codeflash-ai/common"
import { getUserRole } from "./github-utils.js"
const APP_ID: string = process.env.GH_APP_ID ?? "" // GitHub App ID
const APP_USER_ID: number = parseInt(process.env.GH_APP_USER_ID ?? "0") // GitHub App User ID
const PRIVATE_KEY: string =
process.env.NODE_ENV === "production"
? await getGithubAppPrivateKey()
: fs.readFileSync("github/Codeflash AI Dev GitHub App Private Key.pem", "utf8")
const WEBHOOK_SECRET: string =
process.env.NODE_ENV === "production"
? await getGithubAppWebhookSecret()
: (process.env.GH_APP_WEBHOOK_SECRET ?? "default-secret")
// Create a function to get config without initializing the app
async function getGithubAppConfig() {
const APP_ID: string = process.env.GH_APP_ID ?? ""
export const githubApp = new App({
appId: APP_ID,
privateKey: PRIVATE_KEY,
webhooks: {
secret: WEBHOOK_SECRET,
},
oauth: {
// OAuth details are currently unused by the app
clientId: "", // process.env.GH_APP_CLIENT_ID ?? "",
clientSecret: "", // process.env.GH_APP_CLIENT_SECRET ?? "",
},
})
console.log(`Github App Initialized`)
const PRIVATE_KEY: string =
process.env.NODE_ENV === "production"
? await getGithubAppPrivateKey()
: process.env.NODE_ENV === "test"
? "test-private-key"
: fs.readFileSync("github/Codeflash AI Dev GitHub App Private Key.pem", "utf8")
const { data } = await githubApp.octokit.request("/app")
// Read more about custom logging: https://github.com/octokit/core.js#logging
githubApp.octokit.log.debug(`Authenticated as '${data.name}'`)
const WEBHOOK_SECRET: string =
process.env.NODE_ENV === "production"
? await getGithubAppWebhookSecret()
: (process.env.GH_APP_WEBHOOK_SECRET ?? "default-secret")
githubApp.webhooks.onAny(async ({ id, name, payload }) => {
console.log(`Github App: Received webhook event ${name} (${id})`)
console.log(`Payload: ${JSON.stringify(payload)}`)
posthog.capture({
distinctId: `github|${payload.sender?.id}`,
event: `cfapi-github-webhook-received`,
properties: {
event: name,
id,
return { APP_ID, PRIVATE_KEY, WEBHOOK_SECRET }
}
// Initialize the app conditionally
const initializeApp = async () => {
const { APP_ID, PRIVATE_KEY, WEBHOOK_SECRET } = await getGithubAppConfig()
return new App({
appId: APP_ID,
privateKey: PRIVATE_KEY,
webhooks: {
secret: WEBHOOK_SECRET,
},
oauth: {
// OAuth details are currently unused by the app
clientId: "", // process.env.GH_APP_CLIENT_ID ?? "",
clientSecret: "", // process.env.GH_APP_CLIENT_SECRET ?? "",
},
})
})
}
console.log(`Github App Authenticated as '${data.name}'`)
githubApp.webhooks.on("installation", async ({ octokit, payload }) => {
console.log(`Received a new installation event: ${JSON.stringify(payload)}`)
// Create an installation access token
const installationAccessToken = await octokit.rest.apps.createInstallationAccessToken({
installation_id: payload.installation.id,
})
console.log(`Installation access token: ${installationAccessToken.data.token}`)
})
githubApp.webhooks.on("pull_request.opened", async ({ octokit, payload }) => {
console.log(`Received a pull request opened event: ${JSON.stringify(payload)}`)
})
githubApp.webhooks.on("pull_request.edited", async ({ octokit, payload }) => {
console.log(`Received a pull request edited event: ${JSON.stringify(payload)}`)
})
githubApp.webhooks.on("pull_request.closed", async ({ octokit, payload }) => {
if (payload.pull_request) {
const prId = String(payload.pull_request.id)
try {
await prisma.optimization_events.updateMany({
where: { pr_id: prId },
data: {
event_type: payload.pull_request.merged ? "pr_merged" : "pr_closed",
// Export the actual App instance, initialized based on environment
export const githubApp = await (async () => {
if (process.env.NODE_ENV === "test") {
// In test environment, return a minimal mock that won't fail
return {
octokit: {
request: async () => ({ data: { name: "Test App" } }),
log: {
debug: () => {},
},
})
console.log(
`Updated optimization_event for PR ID ${prId} to ${payload.pull_request.merged ? "pr_merged" : "pr_closed"}`,
)
} catch (err) {
console.error(`Failed to update optimization_event for PR ID ${prId}:`, err)
}
console.log(
`Received a pull request closed event. PR #${payload.pull_request.number} ` +
`by ${payload.pull_request.user.login} was closed.`,
)
},
webhooks: {
on: () => {},
onAny: () => {},
onError: () => {},
},
getInstallationOctokit: async () => ({}),
} as any as App
}
// Check if the PR was merged and is a PR created by Codeflash
const is_user_code_flash = payload.pull_request.user.id === APP_USER_ID
if (payload.pull_request.merged && is_user_code_flash) {
// Extract the original PR number from the branch name
const dependentBranchNamePattern = /codeflash.optimize-pr(\d+)-\d{4}-\d{2}-\d{2}T.+$/
const standaloneBranchNamePattern = /codeflash.optimize-(.+)-\d{4}-\d{2}-\d{2}T.+$/
const dependentPrMatch = dependentBranchNamePattern.exec(payload.pull_request.head.ref)
const standalonePrMatch = standaloneBranchNamePattern.exec(payload.pull_request.head.ref)
if (dependentPrMatch != null) {
const originalPrNumber = parseInt(dependentPrMatch[1])
let username = "You"
if (payload.pull_request.merged_by != null) {
// should not be null, but check anyway
username = `@${payload.pull_request.merged_by.login}`
}
// Comment on the original PR
await octokit.rest.issues.createComment({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: originalPrNumber,
body: `This PR is now faster! 🚀 ${username} accepted my optimizations from:
- #${payload.pull_request.number}`,
})
// In other environments, initialize normally
const app = await initializeApp()
posthog.capture({
distinctId: `github|${payload.sender.id}`, // this is the user who merged the PR
event: `cfapi-github-dependent-pr-merged`,
properties: {
owner: payload.repository.owner.login,
repo: payload.repository.name,
originalPrNumber,
dependentPrNumber: payload.pull_request.number,
mergedBy: payload.pull_request.merged_by?.login,
console.log(`Github App Initialized`)
const { data } = await app.octokit.request("/app")
// Read more about custom logging: https://github.com/octokit/core.js#logging
app.octokit.log.debug(`Authenticated as '${data.name}'`)
app.webhooks.onAny(async ({ id, name, payload }) => {
console.log(`Github App: Received webhook event ${name} (${id})`)
console.log(`Payload: ${JSON.stringify(payload)}`)
posthog.capture({
distinctId: `github|${payload.sender?.id}`,
event: `cfapi-github-webhook-received`,
properties: {
event: name,
id,
},
})
})
console.log(`Github App Authenticated as '${data.name}'`)
app.webhooks.on("installation", async ({ octokit, payload }) => {
console.log(`Received a new installation event: ${JSON.stringify(payload)}`)
// Create an installation access token
const installationAccessToken = await octokit.rest.apps.createInstallationAccessToken({
installation_id: payload.installation.id,
})
console.log(`Installation access token: ${installationAccessToken.data.token}`)
})
app.webhooks.on("pull_request.opened", async ({ octokit, payload }) => {
console.log(`Received a pull request opened event: ${JSON.stringify(payload)}`)
})
app.webhooks.on("pull_request.edited", async ({ octokit, payload }) => {
console.log(`Received a pull request edited event: ${JSON.stringify(payload)}`)
})
app.webhooks.on("pull_request.closed", async ({ octokit, payload }) => {
if (payload.pull_request) {
const prId = String(payload.pull_request.id)
try {
await prisma.optimization_events.updateMany({
where: { pr_id: prId },
data: {
event_type: payload.pull_request.merged ? "pr_merged" : "pr_closed",
},
})
console.log(
`Commented on original PR #${originalPrNumber} and logged the event to Posthog.`,
`Updated optimization_event for PR ID ${prId} to ${payload.pull_request.merged ? "pr_merged" : "pr_closed"}`,
)
} else if (standalonePrMatch != null) {
posthog.capture({
distinctId: `github|${payload.sender.id}`,
event: `cfapi-github-standalone-pr-merged`,
properties: {
owner: payload.repository.owner.login,
repo: payload.repository.name,
functionName: standalonePrMatch[1],
prNumber: payload.pull_request.number,
mergedBy: payload.pull_request.merged_by?.login,
},
})
console.log(`Logged standalone PR #${payload.pull_request.number} merge event to Posthog.`)
} catch (err) {
console.error(`Failed to update optimization_event for PR ID ${prId}:`, err)
}
}
// Close any open optimization PRs targeting the branch of the closed PR
// Ensure we only close PRs that are targeting the branch of the PR that was just closed
const closedPrBranch = payload.pull_request.head.ref
// Logic to close any open optimization PRs targeting this branch
console.log(`Closing optimization PRs targeting branch ${closedPrBranch}...`)
if (payload.installation === undefined) {
console.error(
`Error! Installation ID is missing from payload. Cannot close PRs for this installation!`,
console.log(
`Received a pull request closed event. PR #${payload.pull_request.number} ` +
`by ${payload.pull_request.user.login} was closed.`,
)
return
}
try {
const installationOctokit = await githubApp.getInstallationOctokit(payload.installation.id)
const openPrs = await installationOctokit.rest.pulls.list({
owner: payload.repository.owner.login,
repo: payload.repository.name,
state: "open",
base: closedPrBranch,
})
for (const pr of openPrs.data) {
// Check if the PR is opened by the Codeflash GitHub App and targets the same base branch as the closed PR
if (
pr.user?.type === "Bot" &&
pr.user?.id === APP_USER_ID &&
pr.base.ref === closedPrBranch
) {
await installationOctokit.rest.pulls.update({
owner: payload.repository.owner.login,
repo: payload.repository.name,
pull_number: pr.number,
state: "closed",
})
console.log(
`Closed optimization PR #${pr.number} targeting branch '${closedPrBranch}' ` +
`because original PR #${payload.pull_request.number} by ${payload.pull_request.user.login} was closed.`,
)
console.log(`Posting pull request comment...`)
// Check if the PR was merged and is a PR created by Codeflash
const is_user_code_flash = payload.pull_request.user.id === APP_USER_ID
if (payload.pull_request.merged && is_user_code_flash) {
// Extract the original PR number from the branch name
const dependentBranchNamePattern = /codeflash.optimize-pr(\d+)-\d{4}-\d{2}-\d{2}T.+$/
const standaloneBranchNamePattern = /codeflash.optimize-(.+)-\d{4}-\d{2}-\d{2}T.+$/
const dependentPrMatch = dependentBranchNamePattern.exec(payload.pull_request.head.ref)
const standalonePrMatch = standaloneBranchNamePattern.exec(payload.pull_request.head.ref)
if (dependentPrMatch != null) {
const originalPrNumber = parseInt(dependentPrMatch[1])
let username = "You"
if (payload.pull_request.merged_by != null) {
// should not be null, but check anyway
username = `@${payload.pull_request.merged_by.login}`
}
// Comment on the original PR
await octokit.rest.issues.createComment({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: pr.number,
body:
`>This PR has been automatically closed because the original PR #${payload.pull_request.number} ` +
`by ${payload.pull_request.user.login} was closed.`,
issue_number: originalPrNumber,
body: `This PR is now faster! 🚀 ${username} accepted my optimizations from:
- #${payload.pull_request.number}`,
})
// Proceed to delete the branch
if (is_user_code_flash) {
await deleteBranchIfExists(installationOctokit, payload, `heads/${pr.head.ref}`)
}
posthog.capture({
distinctId: `github|${payload.sender.id}`, // this is the user who merged the PR
event: `cfapi-github-dependent-pr-merged`,
properties: {
owner: payload.repository.owner.login,
repo: payload.repository.name,
originalPrNumber,
dependentPrNumber: payload.pull_request.number,
mergedBy: payload.pull_request.merged_by?.login,
},
})
console.log(
`Commented on original PR #${originalPrNumber} and logged the event to Posthog.`,
)
} else if (standalonePrMatch != null) {
posthog.capture({
distinctId: `github|${payload.sender.id}`,
event: `cfapi-github-standalone-pr-merged`,
properties: {
owner: payload.repository.owner.login,
repo: payload.repository.name,
functionName: standalonePrMatch[1],
prNumber: payload.pull_request.number,
mergedBy: payload.pull_request.merged_by?.login,
},
})
console.log(
`Logged standalone PR #${payload.pull_request.number} merge event to Posthog.`,
)
}
}
// If there was no open PR's, still delete the branch in case of inline comment
if (is_user_code_flash) {
await deleteBranchIfExists(installationOctokit, payload, closedPrBranch)
// Close any open optimization PRs targeting the branch of the closed PR
// Ensure we only close PRs that are targeting the branch of the PR that was just closed
const closedPrBranch = payload.pull_request.head.ref
// Logic to close any open optimization PRs targeting this branch
console.log(`Closing optimization PRs targeting branch ${closedPrBranch}...`)
if (payload.installation === undefined) {
console.error(
`Error! Installation ID is missing from payload. Cannot close PRs for this installation!`,
)
return
}
try {
const installationOctokit = await app.getInstallationOctokit(payload.installation.id)
const openPrs = await installationOctokit.rest.pulls.list({
owner: payload.repository.owner.login,
repo: payload.repository.name,
state: "open",
base: closedPrBranch,
})
for (const pr of openPrs.data) {
// Check if the PR is opened by the Codeflash GitHub App and targets the same base branch as the closed PR
if (
pr.user?.type === "Bot" &&
pr.user?.id === APP_USER_ID &&
pr.base.ref === closedPrBranch
) {
await installationOctokit.rest.pulls.update({
owner: payload.repository.owner.login,
repo: payload.repository.name,
pull_number: pr.number,
state: "closed",
})
console.log(
`Closed optimization PR #${pr.number} targeting branch '${closedPrBranch}' ` +
`because original PR #${payload.pull_request.number} by ${payload.pull_request.user.login} was closed.`,
)
console.log(`Posting pull request comment...`)
await octokit.rest.issues.createComment({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: pr.number,
body:
`>This PR has been automatically closed because the original PR #${payload.pull_request.number} ` +
`by ${payload.pull_request.user.login} was closed.`,
})
// Proceed to delete the branch
if (is_user_code_flash) {
await deleteBranchIfExists(installationOctokit, payload, `heads/${pr.head.ref}`)
}
}
}
// If there was no open PR's, still delete the branch in case of inline comment
if (is_user_code_flash) {
await deleteBranchIfExists(installationOctokit, payload, closedPrBranch)
}
} catch (error) {
console.error(
`Failed to close optimization PRs targeting branch ${closedPrBranch}: ${error}`,
)
Sentry.captureException(error)
}
} catch (error) {
console.error(`Failed to close optimization PRs targeting branch ${closedPrBranch}: ${error}`)
Sentry.captureException(error)
}
}
})
githubApp.webhooks.on("installation.created", async ({ octokit, payload }) => {
console.log(`Received a installation.created event: ${JSON.stringify(payload)}`)
})
githubApp.webhooks.on("installation_repositories.added", async ({ octokit, payload }) => {
console.log(`Received a installation_repositories.added event: ${JSON.stringify(payload)}`)
})
githubApp.webhooks.on("marketplace_purchase", async ({ id, name, payload }) => {
console.log(`Received a marketplace purchase event: ${name} (${id})`)
console.log(`Payload: ${JSON.stringify(payload)}`)
posthog.capture({
distinctId: `github|${payload.sender.id}`,
event: `cfapi-github-marketplace-purchase`,
properties: {
event: name,
id,
},
})
})
githubApp.webhooks.on("pull_request.synchronize", async ({ octokit, payload }) => {
if (payload.pull_request) {
console.log(
`Received a pull request synchronize event. PR #${payload.pull_request.number} ` +
`by ${payload.pull_request?.user?.login} was updated with new commits.`,
)
// Retrieve the list of commits for the pull request
const commits = await octokit.rest.pulls.listCommits({
owner: payload.repository.owner.login,
repo: payload.repository.name,
pull_number: payload.pull_request.number,
app.webhooks.on("installation.created", async ({ octokit, payload }) => {
console.log(`Received a installation.created event: ${JSON.stringify(payload)}`)
})
app.webhooks.on("installation_repositories.added", async ({ octokit, payload }) => {
console.log(`Received a installation_repositories.added event: ${JSON.stringify(payload)}`)
})
app.webhooks.on("marketplace_purchase", async ({ id, name, payload }) => {
console.log(`Received a marketplace purchase event: ${name} (${id})`)
console.log(`Payload: ${JSON.stringify(payload)}`)
posthog.capture({
distinctId: `github|${payload.sender.id}`,
event: `cfapi-github-marketplace-purchase`,
properties: {
event: name,
id,
},
})
})
// Check the latest commit for the co-authored-by line
const latestCommit = commits.data[commits.data.length - 1]
if (
latestCommit.commit.message.includes(
"Co-authored-by: codeflash-ai[bot] <148906541+codeflash-ai[bot]@users.noreply.github.com>",
app.webhooks.on("pull_request.synchronize", async ({ octokit, payload }) => {
if (payload.pull_request) {
console.log(
`Received a pull request synchronize event. PR #${payload.pull_request.number} ` +
`by ${payload.pull_request?.user?.login} was updated with new commits.`,
)
) {
// Log the event to Posthog
posthog.capture({
distinctId: `github|${payload.sender.id}`,
event: `cfapi-github-commit-coauthored-by-codeflash`,
properties: {
prNumber: payload.pull_request.number,
commitId: latestCommit.sha,
repository: payload.repository.full_name,
author: latestCommit.commit.author?.name,
},
})
console.log(`Logged co-authored commit to Posthog: ${latestCommit.sha}`)
// should not be null, but check anyway
const authorname = latestCommit.commit.author?.name ?? "You"
// Comment on the PR
await octokit.rest.issues.createComment({
// Retrieve the list of commits for the pull request
const commits = await octokit.rest.pulls.listCommits({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: payload.pull_request.number,
body: `This PR is now faster! 🚀 ${authorname} accepted my code suggestion above.`,
pull_number: payload.pull_request.number,
})
console.log(
`Commented on PR #${payload.pull_request.number} about the accepted review comment.`,
)
}
}
})
// Optional: Handle errors
githubApp.webhooks.onError(error => {
console.error(`Error occurred in Github App's onError handler: ${error}`)
if (error instanceof Error) {
// Check if it's an AggregateError, common for signature issues
if (error.name === "AggregateError" && Array.isArray((error as any).errors)) {
console.error("AggregateError details (possible secret mismatch or multiple issues):")
;(error as any).errors.forEach((subError: Error, i: number) => {
console.error(` Sub-error ${i + 1}: ${subError.message}`)
})
} else if (error.message.includes("content length")) {
console.error(
"Content length mismatch detected by Octokit. Payload may be truncated or header incorrect.",
)
const eventRequest = (error as any).event?.request
if (eventRequest && eventRequest.headers) {
// Check the latest commit for the co-authored-by line
const latestCommit = commits.data[commits.data.length - 1]
if (
latestCommit.commit.message.includes(
"Co-authored-by: codeflash-ai[bot] <148906541+codeflash-ai[bot]@users.noreply.github.com>",
)
) {
// Log the event to Posthog
posthog.capture({
distinctId: `github|${payload.sender.id}`,
event: `cfapi-github-commit-coauthored-by-codeflash`,
properties: {
prNumber: payload.pull_request.number,
commitId: latestCommit.sha,
repository: payload.repository.full_name,
author: latestCommit.commit.author?.name,
},
})
console.log(`Logged co-authored commit to Posthog: ${latestCommit.sha}`)
// should not be null, but check anyway
const authorname = latestCommit.commit.author?.name ?? "You"
// Comment on the PR
await octokit.rest.issues.createComment({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: payload.pull_request.number,
body: `This PR is now faster! 🚀 ${authorname} accepted my code suggestion above.`,
})
console.log(
`Commented on PR #${payload.pull_request.number} about the accepted review comment.`,
)
}
}
})
// Optional: Handle errors
app.webhooks.onError(error => {
console.error(`Error occurred in Github App's onError handler: ${error}`)
if (error instanceof Error) {
// Check if it's an AggregateError, common for signature issues
if (error.name === "AggregateError" && Array.isArray((error as any).errors)) {
console.error("AggregateError details (possible secret mismatch or multiple issues):")
;(error as any).errors.forEach((subError: Error, i: number) => {
console.error(` Sub-error ${i + 1}: ${subError.message}`)
})
} else if (error.message.includes("content length")) {
console.error(
"Request headers from error.event:",
JSON.stringify(eventRequest.headers, null, 2),
"Content length mismatch detected by Octokit. Payload may be truncated or header incorrect.",
)
const eventRequest = (error as any).event?.request
if (eventRequest && eventRequest.headers) {
console.error(
"Request headers from error.event:",
JSON.stringify(eventRequest.headers, null, 2),
)
}
}
// Log the full error structure for better debugging
console.error(
"Full error object (onError):",
JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
)
} else {
console.error("Full error (onError, non-Error instance):", error)
}
// Log the full error structure for better debugging
console.error(
"Full error object (onError):",
JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
)
} else {
console.error("Full error (onError, non-Error instance):", error)
}
Sentry.captureException(error)
})
Sentry.captureException(error)
})
githubApp.webhooks.on("installation_repositories", async ({ payload }) => {
console.log(`Received a installation_repositories event: ${JSON.stringify(payload)}`)
const { repositories_added, installation, sender } = payload
// Check if required fields are missing
if (!repositories_added || !installation?.id) {
console.log(
`Missing repositories_added or installation.id. Full payload: ${JSON.stringify(payload, null, 2)}`,
)
return
}
const account = installation.account
let accountLogin: string | undefined
app.webhooks.on("installation_repositories", async ({ payload }) => {
console.log(`Received a installation_repositories event: ${JSON.stringify(payload)}`)
const { repositories_added, installation, sender } = payload
// Check if required fields are missing
if (!repositories_added || !installation?.id) {
console.log(
`Missing repositories_added or installation.id. Full payload: ${JSON.stringify(payload, null, 2)}`,
)
return
}
const account = installation.account
let accountLogin: string | undefined
// Check if the account is a user or an organization
if ("login" in account) {
// It's a user account, use `login`
accountLogin = account.login
} else if ("slug" in account) {
// It's an organization account, use `slug`
accountLogin = account.slug
}
// Check if the account is a user or an organization
if ("login" in account) {
// It's a user account, use `login`
accountLogin = account.login
} else if ("slug" in account) {
// It's an organization account, use `slug`
accountLogin = account.slug
}
let accountType: string | undefined
if ("type" in account) {
accountType = account.type
} else {
accountType = "Organization" // fallback assumption
}
let accountType: string | undefined
if ("type" in account) {
accountType = account.type
} else {
accountType = "Organization" // fallback assumption
}
if (!accountLogin) {
console.error("Error: Account login or slug not found")
return
}
if (!accountLogin) {
console.error("Error: Account login or slug not found")
return
}
// Check if the installation exists, if not, create it
const installationExists = await getAppInstallationByInstalltionId(installation.id)
// Check if the installation exists, if not, create it
const installationExists = await getAppInstallationByInstalltionId(installation.id)
if (!installationExists) {
// Create the installation if it doesn't exist
await createAppInstallation({
installation_id: installation.id,
account_id: installation.account.id,
account_login: accountLogin,
account_type: accountType,
})
console.log(`Installation created for ID: ${installation.id}`)
}
// Process each repository in the list of added repositories
for (const repo of repositories_added) {
try {
// Upsert logic for repository creation or update
const savedRepo = await upsertRepository({
github_repo_id: String(repo.id),
if (!installationExists) {
// Create the installation if it doesn't exist
await createAppInstallation({
installation_id: installation.id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.private,
account_id: installation.account.id,
account_login: accountLogin,
account_type: accountType,
})
// Get the GitHub user ID of the sender
const githubUserId = sender?.id
if (githubUserId) {
console.log(`GitHub User ID: ${githubUserId} triggered the event`)
// Fetch the user's role using the helper
// Use octokit from getInstallationOctokit for this installation
const installationOctokit = await githubApp.getInstallationOctokit(installation.id)
const userRole = await getUserRole({
octokit: installationOctokit,
owner: accountLogin,
repo: repo.name,
username: sender.login,
isOrg: accountType === "Organization",
})
console.log(`Fetched user role: ${userRole}`)
const user = await createOrUpdateUser(
`github|${githubUserId}`,
sender.login,
sender.email ?? null,
sender.name ?? null,
)
await createRepositoryMember({
repository_id: savedRepo.id,
user_id: user.user_id,
role: userRole,
})
console.log(`Repository upserted: ${repo.full_name}`)
} else {
console.error("GitHub User ID not found in sender.")
}
} catch (error) {
console.error(`Failed to add/reactivate repository ${repo.full_name}:`, error)
Sentry.captureException(error)
console.log(`Installation created for ID: ${installation.id}`)
}
}
})
// Process each repository in the list of added repositories
for (const repo of repositories_added) {
try {
// Upsert logic for repository creation or update
const savedRepo = await upsertRepository({
github_repo_id: String(repo.id),
installation_id: installation.id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.private,
})
// Get the GitHub user ID of the sender
const githubUserId = sender?.id
if (githubUserId) {
console.log(`GitHub User ID: ${githubUserId} triggered the event`)
// Fetch the user's role using the helper
// Use octokit from getInstallationOctokit for this installation
const installationOctokit = await app.getInstallationOctokit(installation.id)
const userRole = await getUserRole({
octokit: installationOctokit,
owner: accountLogin,
repo: repo.name,
username: sender.login,
isOrg: accountType === "Organization",
})
console.log(`Fetched user role: ${userRole}`)
const user = await createOrUpdateUser(
`github|${githubUserId}`,
sender.login,
sender.email ?? null,
sender.name ?? null,
)
await createRepositoryMember({
repository_id: savedRepo.id,
user_id: user.user_id,
role: userRole,
})
console.log(`Repository upserted: ${repo.full_name}`)
} else {
console.error("GitHub User ID not found in sender.")
}
} catch (error) {
console.error(`Failed to add/reactivate repository ${repo.full_name}:`, error)
Sentry.captureException(error)
}
}
})
return app
})()
export const ghAppPathPrefix: string = "/cfapi/github"
@ -426,7 +472,7 @@ export const ghAppMiddleware = createNodeMiddleware(githubApp.webhooks, {
path: "/cfapi/github/webhooks",
})
const deleteBranchIfExists = async (installationOctokit, payload, branchName) => {
const deleteBranchIfExists = async (installationOctokit: any, payload: any, branchName: string) => {
try {
console.log(`Deleting the branch associated with the closed PR...`)
// Check if the branch exists by querying the reference
@ -446,7 +492,7 @@ const deleteBranchIfExists = async (installationOctokit, payload, branchName) =>
ref: `heads/${branchName}`,
})
console.log(`Branch '${branchName}' has been deleted.`)
} catch (error) {
} catch (error: any) {
// If the branch doesn't exist or other errors occur, catch the error
if (error.status === 404) {
console.log("Branch does not exist!")

View file

@ -1,6 +1,33 @@
import { type App, type Octokit } from "octokit"
import * as Sentry from "@sentry/node"
// Dependencies interface for easier testing
export interface GithubUtilsDependencies {
console: {
error: typeof console.error
}
}
// Default dependencies
let dependencies: GithubUtilsDependencies = {
console: {
error: console.error,
},
}
// For testing - allow dependency injection
export function setGithubUtilsDependencies(deps: Partial<GithubUtilsDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetGithubUtilsDependencies() {
dependencies = {
console: {
error: console.error,
},
}
}
// Checks if the user is a collaborator on the repository
export async function isUserCollaborator(
installationOctokit: Octokit,
@ -38,7 +65,7 @@ export async function getInstallationOctokitByOwner(
const installationId = repoInstallation.data.id
return await githubApp.getInstallationOctokit(installationId)
} catch (error: any) {
console.error(`Error getting installation Octokit for ${owner}/${repo}:`, error)
dependencies.console.error(`Error getting installation Octokit for ${owner}/${repo}:`, error)
if (error.status === 404) {
return Error(`GitHub App is not installed on the repository ${owner}/${repo}`)
} else {
@ -48,7 +75,7 @@ export async function getInstallationOctokitByOwner(
}
// Ensures that a label exists on a repository, creating it if it doesn't
async function ensureLabelExists(
export async function ensureLabelExists(
installationOctokit: Octokit,
owner: string,
repo: string,

View file

@ -1,17 +1,60 @@
import { WebClient, ChatPostMessageArguments } from "@slack/web-api"
const options = {}
const web = new WebClient(process.env.SLACK_TOKEN, options)
const SLACK_TOKEN = process.env.SLACK_TOKEN
const SLACK_CHANNEL_ID = process.env.SLACK_CHANNEL_ID
if (!SLACK_TOKEN) {
throw new Error("Missing SLACK_TOKEN")
// Dependencies interface for easier testing
export interface SendSlackMessageDependencies {
WebClient: typeof WebClient
getSlackToken: () => string | undefined
getSlackChannelId: () => string | undefined
console: typeof console
}
if (!SLACK_CHANNEL_ID) {
throw new Error("Missing SLACK_CHANNEL_ID")
// Default dependencies
let dependencies: SendSlackMessageDependencies = {
WebClient,
getSlackToken: () => process.env.SLACK_TOKEN,
getSlackChannelId: () => process.env.SLACK_CHANNEL_ID,
console,
}
// For testing - allow dependency injection
export function setSendSlackMessageDependencies(deps: Partial<SendSlackMessageDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetSendSlackMessageDependencies() {
dependencies = {
WebClient,
getSlackToken: () => process.env.SLACK_TOKEN,
getSlackChannelId: () => process.env.SLACK_CHANNEL_ID,
console,
}
}
// Initialize web client
let web: WebClient | null = null
export function initializeWebClient() {
const SLACK_TOKEN = dependencies.getSlackToken()
const SLACK_CHANNEL_ID = dependencies.getSlackChannelId()
if (!SLACK_TOKEN) {
throw new Error("Missing SLACK_TOKEN")
}
if (!SLACK_CHANNEL_ID) {
throw new Error("Missing SLACK_CHANNEL_ID")
}
if (!web) {
web = new dependencies.WebClient(SLACK_TOKEN, {})
}
return web
}
// For testing - allow resetting the web client
export function resetWebClient() {
web = null
}
/**
@ -22,11 +65,17 @@ if (!SLACK_CHANNEL_ID) {
* @param {boolean} returnData - Whether to return the full Slack API response
* @returns {Promise<boolean|object>} - True or API response
*/
export const sendSlackMessage = async (message, channel = null, returnData = false) => {
export const sendSlackMessage = async (
message: any,
channel: string | null = null,
returnData: boolean = false,
): Promise<boolean | object> => {
return new Promise(async (resolve, reject) => {
const channelId = channel || SLACK_CHANNEL_ID
try {
const webClient = initializeWebClient()
const SLACK_CHANNEL_ID = dependencies.getSlackChannelId()
const channelId = channel || SLACK_CHANNEL_ID
// Configure the message payload depending on the input type
let payload: ChatPostMessageArguments
@ -43,14 +92,14 @@ export const sendSlackMessage = async (message, channel = null, returnData = fal
blocks: message.blocks,
}
} else {
console.warn("Object passed to sendSlackMessage without blocks property")
dependencies.console.warn("Object passed to sendSlackMessage without blocks property")
payload = {
channel: channelId,
text: JSON.stringify(message),
}
}
} else {
console.error("Invalid message type", typeof message)
dependencies.console.error("Invalid message type", typeof message)
payload = {
channel: channelId,
text: "Invalid message",
@ -59,10 +108,10 @@ export const sendSlackMessage = async (message, channel = null, returnData = fal
// console.log("Sending payload to Slack:", JSON.stringify(payload, null, 2));
const resp = await web.chat.postMessage(payload)
const resp = await webClient.chat.postMessage(payload)
return resolve(returnData ? resp : true)
} catch (error) {
console.error("Error sending Slack message:", error)
dependencies.console.error("Error sending Slack message:", error)
return resolve(returnData ? { error } : true)
}
})

View file

@ -0,0 +1,572 @@
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import {
createNewBranchFromDiffContents,
createNewPullRequest,
assignReviewer,
createStandalonePullRequest,
createDependentPullRequest,
setCreatePrFromDiffContentsDependencies,
resetCreatePrFromDiffContentsDependencies,
type PrCommentFields,
} from "../create-pr-from-diffcontents"
import { type FileDiffContent } from "@codeflash-ai/code-suggester/build/src/types.js"
describe("Create PR from Diff Contents", () => {
let mockOctokit: any
let mockDependencies: any
beforeEach(() => {
// Setup mock octokit
mockOctokit = {
rest: {
git: {
createRef: jest.fn(),
getRef: jest.fn(),
createBlob: jest.fn(),
getCommit: jest.fn(),
createTree: jest.fn(),
createCommit: jest.fn(),
updateRef: jest.fn(),
},
pulls: {
create: jest.fn(),
requestReviewers: jest.fn(),
},
issues: {
createComment: jest.fn(),
},
},
}
// Setup mock dependencies
mockDependencies = {
addLabelToPullRequest: jest.fn(),
buildBenchmarkInfo: jest.fn(),
buildDependentPrTitle: jest.fn(),
buildPrTitle: jest.fn(),
buildResultFooter: jest.fn(),
buildResultHeader: jest.fn(),
buildResultDetails: jest.fn(),
buildResultTestReport: jest.fn(),
originalPRComment: jest.fn(),
}
setCreatePrFromDiffContentsDependencies(mockDependencies)
})
afterEach(() => {
resetCreatePrFromDiffContentsDependencies()
jest.clearAllMocks()
})
describe("createNewBranchFromDiffContents", () => {
const diffContentsMap = new Map<string, FileDiffContent>([
[
"file1.js",
{
oldContent: "console.log('old code')",
newContent: "console.log('optimized code')",
},
],
[
"file2.js",
{
oldContent: "function old() { return false; }",
newContent: "function optimized() { return true; }",
},
],
])
// Helper function to setup successful API responses
const setupSuccessfulMocks = () => {
mockOctokit.rest.git.getRef
.mockResolvedValueOnce({ data: { object: { sha: "base-sha" } } })
.mockResolvedValueOnce({ data: { object: { sha: "branch-sha" } } })
mockOctokit.rest.git.createRef.mockResolvedValue({})
mockOctokit.rest.git.createBlob
.mockResolvedValueOnce({ data: { sha: "blob1-sha" } })
.mockResolvedValueOnce({ data: { sha: "blob2-sha" } })
mockOctokit.rest.git.getCommit.mockResolvedValue({
data: { tree: { sha: "tree-sha" } },
})
mockOctokit.rest.git.createTree.mockResolvedValue({
data: { sha: "new-tree-sha" },
})
mockOctokit.rest.git.createCommit.mockResolvedValue({
data: { sha: "new-commit-sha" },
})
mockOctokit.rest.git.updateRef.mockResolvedValue({
status: 200,
})
}
it("should successfully create a new branch with diff contents", async () => {
setupSuccessfulMocks()
const result = await createNewBranchFromDiffContents(
mockOctokit,
"test-owner",
"test-repo",
"feature-branch",
"main",
diffContentsMap,
"Optimize performance",
)
expect(result).toBe(true)
// Verify branch creation
expect(mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
ref: "refs/heads/feature-branch",
sha: "base-sha",
})
// Verify blob creation for each file
expect(mockOctokit.rest.git.createBlob).toHaveBeenCalledTimes(2)
expect(mockOctokit.rest.git.createBlob).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
content: "console.log('optimized code')",
encoding: "utf-8",
})
expect(mockOctokit.rest.git.createBlob).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
content: "function optimized() { return true; }",
encoding: "utf-8",
})
// Verify tree creation
expect(mockOctokit.rest.git.createTree).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
base_tree: "tree-sha",
tree: [
{
path: "file1.js",
mode: "100644",
type: "blob",
sha: "blob1-sha",
},
{
path: "file2.js",
mode: "100644",
type: "blob",
sha: "blob2-sha",
},
],
})
// Verify commit creation
expect(mockOctokit.rest.git.createCommit).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
message: "Optimize performance",
tree: "new-tree-sha",
parents: ["branch-sha"],
})
// Verify branch update
expect(mockOctokit.rest.git.updateRef).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
ref: "heads/feature-branch",
sha: "new-commit-sha",
})
})
it("should handle empty diff contents map", async () => {
setupSuccessfulMocks()
const emptyMap = new Map<string, FileDiffContent>()
const result = await createNewBranchFromDiffContents(
mockOctokit,
"test-owner",
"test-repo",
"feature-branch",
"main",
emptyMap,
"Empty commit",
)
expect(result).toBe(true)
expect(mockOctokit.rest.git.createBlob).not.toHaveBeenCalled()
expect(mockOctokit.rest.git.createTree).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
base_tree: "tree-sha",
tree: [],
})
})
it("should return false when GitHub API fails", async () => {
mockOctokit.rest.git.createRef.mockRejectedValue(new Error("API Error"))
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
const result = await createNewBranchFromDiffContents(
mockOctokit,
"test-owner",
"test-repo",
"feature-branch",
"main",
diffContentsMap,
"Optimize performance",
)
expect(result).toBe(false)
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Error creating branch from diff contents:",
expect.any(Error),
)
consoleErrorSpy.mockRestore()
})
it("should return false when updateRef returns non-200 status", async () => {
// Setup most mocks to succeed
mockOctokit.rest.git.getRef
.mockResolvedValueOnce({ data: { object: { sha: "base-sha" } } })
.mockResolvedValueOnce({ data: { object: { sha: "branch-sha" } } })
mockOctokit.rest.git.createRef.mockResolvedValue({})
mockOctokit.rest.git.createBlob
.mockResolvedValueOnce({ data: { sha: "blob1-sha" } })
.mockResolvedValueOnce({ data: { sha: "blob2-sha" } })
mockOctokit.rest.git.getCommit.mockResolvedValue({
data: { tree: { sha: "tree-sha" } },
})
mockOctokit.rest.git.createTree.mockResolvedValue({
data: { sha: "new-tree-sha" },
})
mockOctokit.rest.git.createCommit.mockResolvedValue({
data: { sha: "new-commit-sha" },
})
// Only updateRef fails with non-200 status
mockOctokit.rest.git.updateRef.mockResolvedValue({
status: 400,
})
const result = await createNewBranchFromDiffContents(
mockOctokit,
"test-owner",
"test-repo",
"feature-branch",
"main",
diffContentsMap,
"Optimize performance",
)
expect(result).toBe(false)
})
})
describe("createNewPullRequest", () => {
it("should create a new pull request successfully", async () => {
const mockPrResponse = {
data: {
id: 123,
number: 456,
html_url: "https://github.com/test-owner/test-repo/pull/456",
title: "Test PR",
},
}
mockOctokit.rest.pulls.create.mockResolvedValue(mockPrResponse)
const result = await createNewPullRequest(
mockOctokit,
"test-owner",
"test-repo",
"Test PR Title",
"Test PR Body",
"feature-branch",
"main",
)
expect(result).toEqual(mockPrResponse)
expect(mockOctokit.rest.pulls.create).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
title: "Test PR Title",
head: "feature-branch",
base: "main",
body: "Test PR Body",
})
})
it("should propagate GitHub API errors", async () => {
const error = new Error("PR creation failed")
mockOctokit.rest.pulls.create.mockRejectedValue(error)
await expect(
createNewPullRequest(
mockOctokit,
"test-owner",
"test-repo",
"Test PR Title",
"Test PR Body",
"feature-branch",
"main",
),
).rejects.toThrow("PR creation failed")
})
})
describe("assignReviewer", () => {
it("should assign a reviewer to a pull request", async () => {
await assignReviewer(mockOctokit, "test-owner", "test-repo", 456, "reviewer-username")
expect(mockOctokit.rest.pulls.requestReviewers).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
pull_number: 456,
reviewers: ["reviewer-username"],
})
})
it("should handle GitHub API errors when assigning reviewer", async () => {
const error = new Error("Review request failed")
mockOctokit.rest.pulls.requestReviewers.mockRejectedValue(error)
await expect(
assignReviewer(mockOctokit, "test-owner", "test-repo", 456, "reviewer-username"),
).rejects.toThrow("Review request failed")
})
})
describe("createStandalonePullRequest", () => {
let mockPrCommentFields: PrCommentFields
beforeEach(() => {
mockPrCommentFields = {
best_runtime: "10ms",
original_runtime: "20ms",
function_name: "testFunction",
file_path: "test.js",
speedup_x: "2.0",
speedup_pct: "50",
loop_count: "1000",
optimization_explanation: "Optimized loop",
report_table: {},
benchmark_details: [],
}
// Setup mock dependencies
mockDependencies.buildResultHeader.mockReturnValue("## Header")
mockDependencies.buildResultDetails.mockReturnValue("Details section")
mockDependencies.buildResultTestReport.mockReturnValue("Test report")
mockDependencies.buildResultFooter.mockReturnValue("Footer")
mockDependencies.buildPrTitle.mockReturnValue("Optimize testFunction")
mockOctokit.rest.pulls.create.mockResolvedValue({
data: { id: 123, number: 456 },
})
})
it("should create standalone PR without benchmark info", async () => {
const result = await createStandalonePullRequest(
mockOctokit,
"test-owner",
"test-repo",
"feature-branch",
"main",
mockPrCommentFields,
"existing tests",
"generated tests",
"coverage message",
)
expect(mockDependencies.buildResultHeader).toHaveBeenCalledWith(mockPrCommentFields)
expect(mockDependencies.buildResultDetails).toHaveBeenCalledWith(mockPrCommentFields)
expect(mockDependencies.buildResultTestReport).toHaveBeenCalledWith(
mockPrCommentFields,
"existing tests",
"generated tests",
"coverage message",
)
expect(mockDependencies.buildResultFooter).toHaveBeenCalledWith("feature-branch")
expect(mockDependencies.buildPrTitle).toHaveBeenCalledWith("testFunction", "50", "2.0")
expect(mockOctokit.rest.pulls.create).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
title: "Optimize testFunction",
head: "feature-branch",
base: "main",
body: "## Header\nDetails section\nTest report\nFooter",
})
expect(result.data.id).toBe(123)
})
it("should create standalone PR with benchmark info", async () => {
mockPrCommentFields.benchmark_details = [
{
benchmark_name: "test_benchmark",
test_function: "testFunction",
original_timing: 20,
expected_new_timing: 10,
speedup_percent: 50,
},
]
mockDependencies.buildBenchmarkInfo.mockReturnValue("Benchmark info")
await createStandalonePullRequest(
mockOctokit,
"test-owner",
"test-repo",
"feature-branch",
"main",
mockPrCommentFields,
"existing tests",
"generated tests",
"coverage message",
)
expect(mockDependencies.buildBenchmarkInfo).toHaveBeenCalledWith(mockPrCommentFields)
expect(mockOctokit.rest.pulls.create).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
title: "Optimize testFunction",
head: "feature-branch",
base: "main",
body: "## Header\nBenchmark info\nDetails section\nTest report\nFooter",
})
})
})
describe("createDependentPullRequest", () => {
let mockPrCommentFields: PrCommentFields
beforeEach(() => {
mockPrCommentFields = {
best_runtime: "10ms",
original_runtime: "20ms",
function_name: "testFunction",
file_path: "test.js",
speedup_x: "2.0",
speedup_pct: "50",
loop_count: "1000",
optimization_explanation: "Optimized loop",
report_table: {},
benchmark_details: [],
}
// Setup mock dependencies
mockDependencies.buildResultHeader.mockReturnValue("## Header")
mockDependencies.buildResultDetails.mockReturnValue("Details section")
mockDependencies.buildResultTestReport.mockReturnValue("Test report")
mockDependencies.buildResultFooter.mockReturnValue("Footer")
mockDependencies.buildDependentPrTitle.mockReturnValue("Dependent PR: Optimize testFunction")
mockDependencies.originalPRComment.mockReturnValue("Original PR comment")
mockOctokit.rest.pulls.create.mockResolvedValue({
data: { id: 123, number: 456 },
})
})
it("should create dependent PR and comment on original PR", async () => {
const result = await createDependentPullRequest(
mockOctokit,
"test-owner",
"test-repo",
789,
"feature-branch",
"pr-789-branch",
mockPrCommentFields,
"existing tests",
"generated tests",
"coverage message",
)
expect(mockDependencies.buildDependentPrTitle).toHaveBeenCalledWith(
"testFunction",
"50",
"2.0",
789,
"pr-789-branch",
)
// Check PR creation
expect(mockOctokit.rest.pulls.create).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
title: "Dependent PR: Optimize testFunction",
head: "feature-branch",
base: "pr-789-branch",
body: expect.stringContaining(
"## ⚡️ This pull request contains optimizations for PR #789",
),
})
// Check label addition
expect(mockDependencies.addLabelToPullRequest).toHaveBeenCalledWith(
mockOctokit,
"test-owner",
"test-repo",
456,
)
// Check comment on original PR
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
issue_number: 789,
body: "Original PR comment",
})
// Verify originalPRComment was called with correct parameters
expect(mockDependencies.originalPRComment).toHaveBeenCalledWith(
mockPrCommentFields,
456,
"pr-789-branch",
)
expect(result.data.id).toBe(123)
})
it("should handle GitHub API errors during PR creation", async () => {
const error = new Error("PR creation failed")
mockOctokit.rest.pulls.create.mockRejectedValue(error)
await expect(
createDependentPullRequest(
mockOctokit,
"test-owner",
"test-repo",
789,
"feature-branch",
"pr-789-branch",
mockPrCommentFields,
"existing tests",
"generated tests",
"coverage message",
),
).rejects.toThrow("PR creation failed")
})
it("should handle GitHub API errors during comment creation", async () => {
mockOctokit.rest.pulls.create.mockResolvedValue({
data: { id: 123, number: 456 },
})
mockOctokit.rest.issues.createComment.mockRejectedValue(new Error("Comment creation failed"))
await expect(
createDependentPullRequest(
mockOctokit,
"test-owner",
"test-repo",
789,
"feature-branch",
"pr-789-branch",
mockPrCommentFields,
"existing tests",
"generated tests",
"coverage message",
),
).rejects.toThrow("Comment creation failed")
})
})
})

View file

@ -0,0 +1,601 @@
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import {
isUserCollaborator,
getInstallationOctokitByOwner,
ensureLabelExists,
addLabelToPullRequest,
setGithubUtilsDependencies,
resetGithubUtilsDependencies,
} from "../github-utils"
describe("GitHub Utils", () => {
let mockOctokit: any
let mockGithubApp: any
let mockDependencies: any
beforeEach(() => {
// Setup mock Octokit
mockOctokit = {
rest: {
repos: {
checkCollaborator: jest.fn(),
},
issues: {
getLabel: jest.fn(),
createLabel: jest.fn(),
addLabels: jest.fn(),
},
},
}
// Setup mock GitHub App
mockGithubApp = {
octokit: {
rest: {
apps: {
getRepoInstallation: jest.fn(),
},
},
},
getInstallationOctokit: jest.fn(),
}
// Setup mock dependencies
mockDependencies = {
console: {
error: jest.fn(),
},
}
setGithubUtilsDependencies(mockDependencies)
})
afterEach(() => {
resetGithubUtilsDependencies()
jest.clearAllMocks()
})
describe("isUserCollaborator", () => {
it("should return true when user is a collaborator (status 204)", async () => {
;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockResolvedValue({
status: 204,
})
const result = await isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user")
expect(result).toBe(true)
expect(mockOctokit.rest.repos.checkCollaborator).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
username: "test-user",
})
})
it("should return false when user is a collaborator but status is not 204", async () => {
;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockResolvedValue({
status: 200,
})
const result = await isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user")
expect(result).toBe(false)
})
it("should return false when user is not a collaborator (404 error)", async () => {
const error = new Error("Not Found")
;(error as any).status = 404
;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockRejectedValue(
error,
)
const result = await isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user")
expect(result).toBe(false)
})
it("should throw error for non-404 API errors", async () => {
const error = new Error("Internal Server Error")
;(error as any).status = 500
;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockRejectedValue(
error,
)
await expect(
isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user"),
).rejects.toThrow("Internal Server Error")
})
it("should throw error for API errors without status", async () => {
const error = new Error("Network Error")
;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockRejectedValue(
error,
)
await expect(
isUserCollaborator(mockOctokit, "test-owner", "test-repo", "test-user"),
).rejects.toThrow("Network Error")
})
it("should handle empty username", async () => {
;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockResolvedValue({
status: 204,
})
const result = await isUserCollaborator(mockOctokit, "test-owner", "test-repo", "")
expect(result).toBe(true)
expect(mockOctokit.rest.repos.checkCollaborator).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
username: "",
})
})
it("should handle special characters in username", async () => {
;(mockOctokit.rest.repos.checkCollaborator as jest.MockedFunction<any>).mockResolvedValue({
status: 204,
})
const result = await isUserCollaborator(
mockOctokit,
"test-owner",
"test-repo",
"user-with-dashes_and_underscores",
)
expect(result).toBe(true)
expect(mockOctokit.rest.repos.checkCollaborator).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
username: "user-with-dashes_and_underscores",
})
})
})
describe("getInstallationOctokitByOwner", () => {
it("should return Octokit instance when installation exists", async () => {
const mockInstallationOctokit = {
rest: {
/* mock octokit */
},
}
;(
mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
).mockResolvedValue({ data: { id: 12345 } })
;(mockGithubApp.getInstallationOctokit as jest.MockedFunction<any>).mockResolvedValue(
mockInstallationOctokit,
)
const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")
expect(result).toBe(mockInstallationOctokit)
expect(mockGithubApp.octokit.rest.apps.getRepoInstallation).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
})
expect(mockGithubApp.getInstallationOctokit).toHaveBeenCalledWith(12345)
})
it("should return Error when GitHub App is not installed (404)", async () => {
const error = new Error("Not Found")
;(error as any).status = 404
;(
mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
).mockRejectedValue(error)
const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")
expect(result).toBeInstanceOf(Error)
expect((result as Error).message).toBe(
"GitHub App is not installed on the repository test-owner/test-repo",
)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error getting installation Octokit for test-owner/test-repo:",
error,
)
})
it("should return Error for other API errors", async () => {
const error = new Error("Internal Server Error")
;(error as any).status = 500
;(
mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
).mockRejectedValue(error)
const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")
expect(result).toBeInstanceOf(Error)
expect((result as Error).message).toBe(
"Error checking GitHub App installation status for repo test-owner/test-repo",
)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error getting installation Octokit for test-owner/test-repo:",
error,
)
})
it("should return Error when getInstallationOctokit fails", async () => {
;(
mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
).mockResolvedValue({ data: { id: 12345 } })
const installationError = new Error("Installation Octokit failed")
;(mockGithubApp.getInstallationOctokit as jest.MockedFunction<any>).mockRejectedValue(
installationError,
)
const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")
expect(result).toBeInstanceOf(Error)
expect((result as Error).message).toBe(
"Error checking GitHub App installation status for repo test-owner/test-repo",
)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error getting installation Octokit for test-owner/test-repo:",
installationError,
)
})
it("should handle missing installation ID", async () => {
;(
mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
).mockResolvedValue({ data: {} }) // Missing id
const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")
expect(mockGithubApp.getInstallationOctokit).toHaveBeenCalledWith(undefined)
// The behavior depends on what getInstallationOctokit does with undefined
})
it("should handle network errors without status", async () => {
const error = new Error("Network timeout")
;(
mockGithubApp.octokit.rest.apps.getRepoInstallation as jest.MockedFunction<any>
).mockRejectedValue(error)
const result = await getInstallationOctokitByOwner(mockGithubApp, "test-owner", "test-repo")
expect(result).toBeInstanceOf(Error)
expect((result as Error).message).toBe(
"Error checking GitHub App installation status for repo test-owner/test-repo",
)
})
})
describe("ensureLabelExists", () => {
it("should not create label when it already exists", async () => {
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
data: { name: "test-label" },
})
await ensureLabelExists(
mockOctokit,
"test-owner",
"test-repo",
"test-label",
"FF0000",
"Test label description",
)
expect(mockOctokit.rest.issues.getLabel).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
name: "test-label",
})
expect(mockOctokit.rest.issues.createLabel).not.toHaveBeenCalled()
})
it("should create label when it doesn't exist (404 error)", async () => {
const error = new Error("Not Found")
;(error as any).status = 404
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)
await ensureLabelExists(
mockOctokit,
"test-owner",
"test-repo",
"test-label",
"FF0000",
"Test label description",
)
expect(mockOctokit.rest.issues.getLabel).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
name: "test-label",
})
expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
name: "test-label",
color: "FF0000",
description: "Test label description",
})
})
it("should throw error for non-404 API errors", async () => {
const error = new Error("Internal Server Error")
;(error as any).status = 500
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)
await expect(
ensureLabelExists(
mockOctokit,
"test-owner",
"test-repo",
"test-label",
"FF0000",
"Test label description",
),
).rejects.toThrow("Internal Server Error")
expect(mockOctokit.rest.issues.createLabel).not.toHaveBeenCalled()
})
it("should handle label creation failure", async () => {
const getError = new Error("Not Found")
;(getError as any).status = 404
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(getError)
const createError = new Error("Label creation failed")
;(mockOctokit.rest.issues.createLabel as jest.MockedFunction<any>).mockRejectedValue(
createError,
)
await expect(
ensureLabelExists(
mockOctokit,
"test-owner",
"test-repo",
"test-label",
"FF0000",
"Test label description",
),
).rejects.toThrow("Label creation failed")
})
it("should handle special characters in label name", async () => {
const error = new Error("Not Found")
;(error as any).status = 404
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)
await ensureLabelExists(
mockOctokit,
"test-owner",
"test-repo",
"⚡️ special-label",
"FFC043",
"Label with emoji and special chars",
)
expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
name: "⚡️ special-label",
color: "FFC043",
description: "Label with emoji and special chars",
})
})
it("should handle empty color and description", async () => {
const error = new Error("Not Found")
;(error as any).status = 404
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)
await ensureLabelExists(mockOctokit, "test-owner", "test-repo", "test-label", "", "")
expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
name: "test-label",
color: "",
description: "",
})
})
})
describe("addLabelToPullRequest", () => {
beforeEach(() => {
// Mock ensureLabelExists to always succeed
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
data: { name: "⚡️ codeflash" },
})
})
it("should add label to pull request with default parameters", async () => {
await addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 123)
expect(mockOctokit.rest.issues.getLabel).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
name: "⚡️ codeflash",
})
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
issue_number: 123,
labels: ["⚡️ codeflash"],
})
})
it("should add custom label to pull request", async () => {
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
data: { name: "custom-label" },
})
await addLabelToPullRequest(
mockOctokit,
"test-owner",
"test-repo",
456,
"custom-label",
"00FF00",
"Custom label description",
)
expect(mockOctokit.rest.issues.getLabel).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
name: "custom-label",
})
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
issue_number: 456,
labels: ["custom-label"],
})
})
it("should create label if it doesn't exist and then add it", async () => {
const error = new Error("Not Found")
;(error as any).status = 404
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)
await addLabelToPullRequest(
mockOctokit,
"test-owner",
"test-repo",
789,
"new-label",
"0000FF",
"New label description",
)
expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
name: "new-label",
color: "0000FF",
description: "New label description",
})
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
issue_number: 789,
labels: ["new-label"],
})
})
it("should handle addLabels API failure", async () => {
const error = new Error("Failed to add labels")
;(mockOctokit.rest.issues.addLabels as jest.MockedFunction<any>).mockRejectedValue(error)
await expect(
addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 123),
).rejects.toThrow("Failed to add labels")
})
it("should handle ensureLabelExists failure", async () => {
const error = new Error("Label operation failed")
;(error as any).status = 500
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(error)
await expect(
addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 123),
).rejects.toThrow("Label operation failed")
expect(mockOctokit.rest.issues.addLabels).not.toHaveBeenCalled()
})
it("should handle zero pull request number", async () => {
await addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 0)
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
issue_number: 0,
labels: ["⚡️ codeflash"],
})
})
it("should handle negative pull request number", async () => {
await addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", -1)
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
issue_number: -1,
labels: ["⚡️ codeflash"],
})
})
it("should handle very long label name", async () => {
const longLabelName = "a".repeat(100) // Very long label name
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
data: { name: longLabelName },
})
await addLabelToPullRequest(mockOctokit, "test-owner", "test-repo", 123, longLabelName)
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "test-owner",
repo: "test-repo",
issue_number: 123,
labels: [longLabelName],
})
})
})
describe("integration scenarios", () => {
it("should handle complete workflow for new repository", async () => {
// Simulate adding label to a PR in a new repository where the label doesn't exist
const getError = new Error("Not Found")
;(getError as any).status = 404
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockRejectedValue(getError)
await addLabelToPullRequest(
mockOctokit,
"new-owner",
"new-repo",
1,
"🚀 optimization",
"FF6B6B",
"Performance optimization",
)
// Verify label creation
expect(mockOctokit.rest.issues.createLabel).toHaveBeenCalledWith({
owner: "new-owner",
repo: "new-repo",
name: "🚀 optimization",
color: "FF6B6B",
description: "Performance optimization",
})
// Verify label addition
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "new-owner",
repo: "new-repo",
issue_number: 1,
labels: ["🚀 optimization"],
})
})
it("should handle repository with existing labels", async () => {
// Simulate adding label to a PR where the label already exists
;(mockOctokit.rest.issues.getLabel as jest.MockedFunction<any>).mockResolvedValue({
data: { name: "existing-label" },
})
await addLabelToPullRequest(
mockOctokit,
"existing-owner",
"existing-repo",
42,
"existing-label",
)
// Verify no label creation
expect(mockOctokit.rest.issues.createLabel).not.toHaveBeenCalled()
// Verify label addition
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
owner: "existing-owner",
repo: "existing-repo",
issue_number: 42,
labels: ["existing-label"],
})
})
})
})

View file

@ -0,0 +1,592 @@
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals"
import {
sendSlackMessage,
setSendSlackMessageDependencies,
resetSendSlackMessageDependencies,
initializeWebClient,
resetWebClient,
} from "../slack_util"
describe("slack-message", () => {
let mockDependencies: any
let mockWebClient: any
beforeEach(() => {
// Reset web client before each test
resetWebClient()
// Setup mock web client
mockWebClient = {
chat: {
postMessage: jest.fn(),
},
}
// Setup mock dependencies
mockDependencies = {
WebClient: jest.fn().mockReturnValue(mockWebClient),
getSlackToken: jest.fn(),
getSlackChannelId: jest.fn(),
console: {
log: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}
setSendSlackMessageDependencies(mockDependencies)
})
afterEach(() => {
resetSendSlackMessageDependencies()
resetWebClient()
jest.clearAllMocks()
})
describe("initializeWebClient", () => {
it("should throw error when SLACK_TOKEN is missing", () => {
mockDependencies.getSlackToken.mockReturnValue(undefined)
mockDependencies.getSlackChannelId.mockReturnValue("channel-id")
expect(() => initializeWebClient()).toThrow("Missing SLACK_TOKEN")
})
it("should throw error when SLACK_CHANNEL_ID is missing", () => {
mockDependencies.getSlackToken.mockReturnValue("token")
mockDependencies.getSlackChannelId.mockReturnValue(undefined)
expect(() => initializeWebClient()).toThrow("Missing SLACK_CHANNEL_ID")
})
it("should create WebClient with token when both env vars are present", () => {
mockDependencies.getSlackToken.mockReturnValue("test-token")
mockDependencies.getSlackChannelId.mockReturnValue("test-channel")
const client = initializeWebClient()
expect(mockDependencies.WebClient).toHaveBeenCalledWith("test-token", {})
expect(client).toBe(mockWebClient)
})
it("should reuse existing WebClient on subsequent calls", () => {
mockDependencies.getSlackToken.mockReturnValue("test-token")
mockDependencies.getSlackChannelId.mockReturnValue("test-channel")
const client1 = initializeWebClient()
const client2 = initializeWebClient()
expect(mockDependencies.WebClient).toHaveBeenCalledTimes(1)
expect(client1).toBe(client2)
})
it("should throw error when SLACK_TOKEN is empty string", () => {
mockDependencies.getSlackToken.mockReturnValue("")
mockDependencies.getSlackChannelId.mockReturnValue("channel-id")
expect(() => initializeWebClient()).toThrow("Missing SLACK_TOKEN")
})
it("should throw error when SLACK_CHANNEL_ID is empty string", () => {
mockDependencies.getSlackToken.mockReturnValue("token")
mockDependencies.getSlackChannelId.mockReturnValue("")
expect(() => initializeWebClient()).toThrow("Missing SLACK_CHANNEL_ID")
})
it("should throw error when SLACK_TOKEN is null", () => {
mockDependencies.getSlackToken.mockReturnValue(null)
mockDependencies.getSlackChannelId.mockReturnValue("channel-id")
expect(() => initializeWebClient()).toThrow("Missing SLACK_TOKEN")
})
it("should throw error when SLACK_CHANNEL_ID is null", () => {
mockDependencies.getSlackToken.mockReturnValue("token")
mockDependencies.getSlackChannelId.mockReturnValue(null)
expect(() => initializeWebClient()).toThrow("Missing SLACK_CHANNEL_ID")
})
})
describe("sendSlackMessage", () => {
beforeEach(() => {
// Setup default successful environment
mockDependencies.getSlackToken.mockReturnValue("test-token")
mockDependencies.getSlackChannelId.mockReturnValue("test-channel")
})
it("should send string message successfully", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const result = await sendSlackMessage("Hello World")
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Hello World",
})
expect(result).toBe(true)
})
it("should send string message with custom channel", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const result = await sendSlackMessage("Hello World", "custom-channel")
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "custom-channel",
text: "Hello World",
})
expect(result).toBe(true)
})
it("should send object with blocks successfully", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const message = {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "Hello World",
},
},
],
}
const result = await sendSlackMessage(message)
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Notification from CodeFlash",
blocks: message.blocks,
})
expect(result).toBe(true)
})
it("should use custom text when provided with blocks", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const message = {
text: "Custom fallback text",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "Hello World",
},
},
],
}
const result = await sendSlackMessage(message)
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Custom fallback text",
blocks: message.blocks,
})
expect(result).toBe(true)
})
it("should handle object without blocks property", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const message = { key: "value", nested: { prop: "test" } }
const result = await sendSlackMessage(message)
expect(mockDependencies.console.warn).toHaveBeenCalledWith(
"Object passed to sendSlackMessage without blocks property",
)
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: JSON.stringify(message),
})
expect(result).toBe(true)
})
it("should handle invalid message types", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const result = await sendSlackMessage(123)
expect(mockDependencies.console.error).toHaveBeenCalledWith("Invalid message type", "number")
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Invalid message",
})
expect(result).toBe(true)
})
it("should handle null message", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const result = await sendSlackMessage(null)
expect(mockDependencies.console.error).toHaveBeenCalledWith("Invalid message type", "object")
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Invalid message",
})
expect(result).toBe(true)
})
it("should handle undefined message", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const result = await sendSlackMessage(undefined)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Invalid message type",
"undefined",
)
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Invalid message",
})
expect(result).toBe(true)
})
it("should return full response when returnData is true", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456", channel: "C123" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const result = await sendSlackMessage("Hello World", null, true)
expect(result).toEqual(mockResponse)
})
it("should handle errors and still resolve", async () => {
const error = new Error("Slack API error")
mockWebClient.chat.postMessage.mockRejectedValue(error)
const result = await sendSlackMessage("Hello World")
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error sending Slack message:",
error,
)
expect(result).toBe(true)
})
it("should return error object when returnData is true and error occurs", async () => {
const error = new Error("Slack API error")
mockWebClient.chat.postMessage.mockRejectedValue(error)
const result = await sendSlackMessage("Hello World", null, true)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error sending Slack message:",
error,
)
expect(result).toEqual({ error })
})
it("should handle empty string message", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const result = await sendSlackMessage("")
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "",
})
expect(result).toBe(true)
})
it("should handle boolean message type", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const result = await sendSlackMessage(true)
expect(mockDependencies.console.error).toHaveBeenCalledWith("Invalid message type", "boolean")
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Invalid message",
})
expect(result).toBe(true)
})
it("should handle array message type", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const result = await sendSlackMessage([1, 2, 3])
// Arrays are objects, so they go through the object path and trigger a warning
expect(mockDependencies.console.warn).toHaveBeenCalledWith(
"Object passed to sendSlackMessage without blocks property",
)
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "[1,2,3]",
})
expect(result).toBe(true)
})
it("should handle complex nested blocks", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const message = {
text: "Fallback",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "Hello World",
},
accessory: {
type: "button",
text: {
type: "plain_text",
text: "Click Me",
},
action_id: "button_1",
},
},
{
type: "divider",
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: "Context text",
},
],
},
],
}
const result = await sendSlackMessage(message)
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Fallback",
blocks: message.blocks,
})
expect(result).toBe(true)
})
it("should handle object with empty blocks array", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const message = {
blocks: [],
}
const result = await sendSlackMessage(message)
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Notification from CodeFlash",
blocks: [],
})
expect(result).toBe(true)
})
it("should handle circular reference in object without blocks", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const message: any = { key: "value" }
message.circular = message // Create circular reference
// Save the original JSON.stringify
const originalStringify = JSON.stringify
// Replace JSON.stringify with a properly typed mock
global.JSON.stringify = ((obj: any): string => {
if (obj === message) {
throw new TypeError("Converting circular structure to JSON")
}
return originalStringify(obj)
}) as any
try {
await sendSlackMessage(message)
expect(mockDependencies.console.warn).toHaveBeenCalledWith(
"Object passed to sendSlackMessage without blocks property",
)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error sending Slack message:",
expect.any(TypeError),
)
} finally {
// Always restore JSON.stringify
global.JSON.stringify = originalStringify
}
})
it("should handle initialization error", async () => {
mockDependencies.getSlackToken.mockReturnValue(undefined)
const result = await sendSlackMessage("Hello World")
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error sending Slack message:",
expect.any(Error),
)
expect(result).toBe(true)
})
it("should handle WebClient constructor error", async () => {
const error = new Error("WebClient initialization failed")
mockDependencies.WebClient.mockImplementation(() => {
throw error
})
const result = await sendSlackMessage("Hello World", null, true)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error sending Slack message:",
error,
)
expect(result).toEqual({ error })
})
it("should use channel parameter over environment variable", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
await sendSlackMessage("Test", "override-channel")
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "override-channel",
text: "Test",
})
})
it("should handle empty channel parameter by using default", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
await sendSlackMessage("Test", "")
// Empty string is falsy, so it should fall back to the default channel
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: "Test",
})
})
it("should handle very long messages", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const longMessage = "x".repeat(10000)
const result = await sendSlackMessage(longMessage)
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: longMessage,
})
expect(result).toBe(true)
})
it("should handle special characters in message", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
const specialMessage = "Hello <@U123> & team! Check this: https://example.com"
const result = await sendSlackMessage(specialMessage)
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
channel: "test-channel",
text: specialMessage,
})
expect(result).toBe(true)
})
it("should handle promise rejection from WebClient", async () => {
mockWebClient.chat.postMessage.mockRejectedValue("Network error")
const result = await sendSlackMessage("Test", null, true)
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error sending Slack message:",
"Network error",
)
expect(result).toEqual({ error: "Network error" })
})
it("should handle synchronous error in postMessage", async () => {
mockWebClient.chat.postMessage.mockImplementation(() => {
throw new Error("Sync error")
})
const result = await sendSlackMessage("Test")
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error sending Slack message:",
expect.any(Error),
)
expect(result).toBe(true)
})
})
describe("edge cases", () => {
it("should handle getSlackToken throwing error", async () => {
mockDependencies.getSlackToken.mockImplementation(() => {
throw new Error("Env error")
})
mockDependencies.getSlackChannelId.mockReturnValue("channel")
const result = await sendSlackMessage("Test")
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error sending Slack message:",
expect.any(Error),
)
expect(result).toBe(true)
})
it("should handle getSlackChannelId throwing error", async () => {
mockDependencies.getSlackToken.mockReturnValue("token")
mockDependencies.getSlackChannelId.mockImplementation(() => {
throw new Error("Env error")
})
const result = await sendSlackMessage("Test")
expect(mockDependencies.console.error).toHaveBeenCalledWith(
"Error sending Slack message:",
expect.any(Error),
)
expect(result).toBe(true)
})
it("should handle object with null blocks property", async () => {
const mockResponse = { ok: true, ts: "1234567890.123456" }
mockWebClient.chat.postMessage.mockResolvedValue(mockResponse)
mockDependencies.getSlackToken.mockReturnValue("token")
mockDependencies.getSlackChannelId.mockReturnValue("channel")
const message = { blocks: null }
const result = await sendSlackMessage(message)
expect(mockDependencies.console.warn).toHaveBeenCalledWith(
"Object passed to sendSlackMessage without blocks property",
)
expect(result).toBe(true)
})
})
})

View file

@ -3,6 +3,10 @@ module.exports = {
preset: "ts-jest/presets/default-esm",
roots: ["."],
testEnvironment: "node",
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
testEnvironmentOptions: {
NODE_ENV: "test"
},
transform: {
"^.+\\.tsx?$": [
"ts-jest",
@ -15,7 +19,15 @@ module.exports = {
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
transformIgnorePatterns: [
'node_modules/(?!(@codeflash-ai)/)'
],
extensionsToTreatAsEsm: [".ts", ".tsx"],
testRegex: ".*\\.test\\.ts$",
testPathIgnorePatterns: process.env.CI ? ["e2e.test.ts"] : [],
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
testTimeout: 30000,
forceExit: true,
detectOpenHandles: true,
clearMocks: true,
}

122
js/cf-api/jest.setup.ts Normal file
View file

@ -0,0 +1,122 @@
import { jest, afterEach } from "@jest/globals"
import * as dotenv from "dotenv"
// Load test environment variables
dotenv.config({ path: ".env.test" })
// Ensure critical test environment variables are set
process.env.NODE_ENV = "test"
// Mock github-repo-setup module early to prevent background Prisma calls
// Note: Jest moduleNameMapper strips .js extensions, so this should match the import
// @ts-ignore
jest.mock("./endpoints/utils/github-repo-setup", () => ({
registerRepositoryAndMember: jest.fn().mockImplementation(() => Promise.resolve(12345)),
getInstallationId: jest.fn().mockImplementation(() => Promise.resolve(12345)),
}))
// Also mock the direct import paths that might be used
jest.mock("./endpoints/utils/github-repo-setup.js", () => ({
registerRepositoryAndMember: jest.fn().mockImplementation(() => Promise.resolve(12345)),
getInstallationId: jest.fn().mockImplementation(() => Promise.resolve(12345)),
}))
// Mock Prisma globally
jest.mock(
"@codeflash-ai/common",
() => {
const mockPrisma = {
repositories: {
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
upsert: jest.fn(),
delete: jest.fn(),
},
users: {
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
upsert: jest.fn(),
delete: jest.fn(),
},
repository_members: {
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
upsert: jest.fn(),
delete: jest.fn(),
},
app_installations: {
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
upsert: jest.fn(),
delete: jest.fn(),
},
optimization_features: {
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
upsert: jest.fn(),
delete: jest.fn(),
},
optimization_events: {
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
upsert: jest.fn(),
delete: jest.fn(),
},
$disconnect: jest.fn(),
}
return {
prisma: mockPrisma,
getAppInstallationByInstalltionId: jest.fn(),
createAppInstallation: jest.fn(),
upsertRepository: jest.fn(),
createRepositoryMember: jest.fn(),
createOrUpdateUser: jest.fn(),
stripeClient: {
subscriptions: {
list: jest.fn(),
retrieve: jest.fn(),
},
customers: {
list: jest.fn(),
retrieve: jest.fn(),
},
},
getActiveSubscriptionByUserId: jest.fn(),
getUserByAuth0Id: jest.fn(),
createUser: jest.fn(),
updateUser: jest.fn(),
}
},
{ virtual: true },
)
// Mock Sentry globally
jest.mock("@sentry/node", () => ({
captureException: jest.fn(),
captureMessage: jest.fn(),
addBreadcrumb: jest.fn(),
}))
// Mock is handled at the top of the file
// Increase test timeout for longer running tests
jest.setTimeout(30000)
// Clean up after each test
afterEach(async () => {
jest.clearAllMocks()
// Allow any pending promises to resolve
await new Promise(resolve => setImmediate(resolve))
})

View file

@ -48,7 +48,8 @@
"@types/body-parser": "^1.19.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.11",
"@types/jest": "^29.5.14",
"@types/supertest": "^6.0.3",
"copyfiles": "^2.4.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
@ -59,7 +60,8 @@
"lint-staged": "^15.4.3",
"prettier": "^3.4.2",
"prisma": "^6.2.1",
"ts-jest": "^29.1.1",
"supertest": "^7.1.1",
"ts-jest": "^29.3.4",
"ts-node": "^10.9.2"
}
},
@ -2158,6 +2160,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"dev": true,
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -4228,6 +4242,15 @@
"node": ">=14"
}
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
"dev": true,
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@prisma/client": {
"version": "6.8.2",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.8.2.tgz",
@ -4751,6 +4774,12 @@
"@types/node": "*"
}
},
"node_modules/@types/cookiejar": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
"dev": true
},
"node_modules/@types/cors": {
"version": "2.8.18",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.18.tgz",
@ -4833,7 +4862,6 @@
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
"integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
@ -4854,6 +4882,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
"integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
"dev": true
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -4968,6 +5002,28 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/superagent": {
"version": "8.1.9",
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
"integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==",
"dev": true,
"dependencies": {
"@types/cookiejar": "^2.1.5",
"@types/methods": "^1.1.4",
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/supertest": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz",
"integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==",
"dev": true,
"dependencies": {
"@types/methods": "^1.1.4",
"@types/superagent": "^8.1.0"
}
},
"node_modules/@types/tedious": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
@ -5530,6 +5586,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@ -6232,6 +6294,15 @@
"node": ">=18"
}
},
"node_modules/component-emitter": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -6281,6 +6352,12 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookiejar": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
"dev": true
},
"node_modules/copyfiles": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz",
@ -6630,6 +6707,16 @@
"node": ">=8"
}
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"dev": true,
"dependencies": {
"asap": "^2.0.0",
"wrappy": "1"
}
},
"node_modules/diff": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
@ -7714,6 +7801,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"dev": true
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@ -7941,6 +8034,23 @@
"node": ">=12.20.0"
}
},
"node_modules/formidable": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"dev": true,
"dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4",
"once": "^1.4.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -9193,7 +9303,6 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@ -12526,6 +12635,51 @@
"node": ">=12.*"
}
},
"node_modules/superagent": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz",
"integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==",
"dev": true,
"dependencies": {
"component-emitter": "^1.3.0",
"cookiejar": "^2.1.4",
"debug": "^4.3.4",
"fast-safe-stringify": "^2.1.1",
"form-data": "^4.0.0",
"formidable": "^3.5.4",
"methods": "^1.1.2",
"mime": "2.6.0",
"qs": "^6.11.0"
},
"engines": {
"node": ">=14.18.0"
}
},
"node_modules/superagent/node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/supertest": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz",
"integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==",
"dev": true,
"dependencies": {
"methods": "^1.1.2",
"superagent": "^10.2.1"
},
"engines": {
"node": ">=14.18.0"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -12710,7 +12864,6 @@
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
"integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bs-logger": "^0.2.6",
"ejs": "^3.1.10",

View file

@ -63,7 +63,8 @@
"@types/body-parser": "^1.19.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.11",
"@types/jest": "^29.5.14",
"@types/supertest": "^6.0.3",
"copyfiles": "^2.4.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
@ -74,7 +75,8 @@
"lint-staged": "^15.4.3",
"prettier": "^3.4.2",
"prisma": "^6.2.1",
"ts-jest": "^29.1.1",
"supertest": "^7.1.1",
"ts-jest": "^29.3.4",
"ts-node": "^10.9.2"
},
"prisma": {

View file

@ -9,7 +9,7 @@
"strictNullChecks": false,
"sourceMap": true,
"target": "es2022",
"types": ["node", "express", "jest"],
"types": ["node", "express", "jest", "@types/jest"],
"outDir": "dist",
"rootDir": ".",
"baseUrl": ".",