diff --git a/apps/backend/src/app/api/deployments/[id]/route.test.ts b/apps/backend/src/app/api/deployments/[id]/route.test.ts index 20ee59d..4c91ca6 100644 --- a/apps/backend/src/app/api/deployments/[id]/route.test.ts +++ b/apps/backend/src/app/api/deployments/[id]/route.test.ts @@ -25,6 +25,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { NextRequest } from 'next/server'; +import { DELETE } from './route'; +import { DeploymentService } from '@/services/deployment.service'; +import { RepositoryCleanupService } from '@/services/github/repository-cleanup.service'; + +// Mock the external service +jest.mock('@/services/github/repository-cleanup.service'); // --------------------------------------------------------------------------- // Supabase mock @@ -315,6 +321,62 @@ describe('GET /api/deployments/[id]', () => { // --------------------------------------------------------------------------- describe('DELETE /api/deployments/[id]', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call cleanup(github_repo_id) after DB row is soft-deleted', async () => { + // Arrange + const mockDeployment = { id: 'dep-123', github_repo_id: 'repo-456' }; + jest.spyOn(DeploymentService, 'findById').mockResolvedValue(mockDeployment as any); + const softDeleteSpy = jest.spyOn(DeploymentService, 'softDelete').mockResolvedValue(true); + + // Act + const response = await DELETE({} as Request, { params: { id: 'dep-123' } }); + + // Assert + expect(softDeleteSpy).toHaveBeenCalledWith('dep-123'); + expect(RepositoryCleanupService.cleanup).toHaveBeenCalledWith('repo-456'); + expect(response.status).toBe(200); + }); + + it('should treat GitHub cleanup failures as non-fatal and still return 200', async () => { + // Arrange + const mockDeployment = { id: 'dep-123', github_repo_id: 'repo-456' }; + jest.spyOn(DeploymentService, 'findById').mockResolvedValue(mockDeployment as any); + jest.spyOn(DeploymentService, 'softDelete').mockResolvedValue(true); + + // Simulate GitHub API failure + (RepositoryCleanupService.cleanup as jest.Mock).mockRejectedValue(new Error('GitHub API Rate Limit')); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + // Act + const response = await DELETE({} as Request, { params: { id: 'dep-123' } }); + + // Assert + expect(RepositoryCleanupService.cleanup).toHaveBeenCalledWith('repo-456'); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('[Non-Fatal] Failed to cleanup GitHub repo'), + expect.any(Error) + ); + expect(response.status).toBe(200); // Verify DB record delete completes despite external error + }); + + it('should skip cleanup if github_repo_id is null', async () => { + // Arrange + const mockDeployment = { id: 'dep-123', github_repo_id: null }; + jest.spyOn(DeploymentService, 'findById').mockResolvedValue(mockDeployment as any); + jest.spyOn(DeploymentService, 'softDelete').mockResolvedValue(true); + + // Act + const response = await DELETE({} as Request, { params: { id: 'dep-123' } }); + + // Assert + expect(RepositoryCleanupService.cleanup).not.toHaveBeenCalled(); + expect(response.status).toBe(200); + }); + + beforeEach(() => { vi.clearAllMocks(); mockGetUser.mockResolvedValue({ data: { user: fakeUser }, error: null }); diff --git a/apps/backend/src/app/api/deployments/[id]/route.ts b/apps/backend/src/app/api/deployments/[id]/route.ts index c64aacc..a4710df 100644 --- a/apps/backend/src/app/api/deployments/[id]/route.ts +++ b/apps/backend/src/app/api/deployments/[id]/route.ts @@ -58,6 +58,10 @@ import { NextRequest, NextResponse } from 'next/server'; import { withAuth } from '@/lib/api/with-auth'; import { githubService } from '@/services/github.service'; import { vercelService } from '@/services/vercel.service'; +import { DeploymentService } from '@/services/deployment.service'; +import { RepositoryCleanupService } from '@/services/github/repository-cleanup.service'; // Adjust import path as needed + + export const GET = withAuth(async (req: NextRequest, { params, user, supabase }) => { const deploymentId = (params as { id: string }).id; @@ -166,4 +170,39 @@ export const DELETE = withAuth(async (req: NextRequest, { params, user, supabase success: true, deploymentId, }); + + export async function DELETE( + request: Request, + { params }: { params: { id: string } }) + { + try { + const deploymentId = params.id; + + // 1. Fetch the deployment to get the github_repo_id before deleting + const deployment = await DeploymentService.findById(deploymentId); + + if (!deployment) { + return NextResponse.json({ error: 'Deployment not found' }, { status: 404 }); + } + + // 2. Soft-delete the DB row + await DeploymentService.softDelete(deploymentId); + + // 3. Trigger GitHub Repo Cleanup (Non-fatal) + if (deployment.github_repo_id) { + try { + // Run cleanup asynchronously or await it, but catch errors locally + await RepositoryCleanupService.cleanup(deployment.github_repo_id); + } catch (githubError) { + // Acceptance Criteria: Treat GitHub API errors as non-fatal — log and continue + console.error(`[Non-Fatal] Failed to cleanup GitHub repo ${deployment.github_repo_id} for deployment ${deploymentId}:`, githubError); + } + } + + return NextResponse.json({ success: true, message: 'Deployment deleted' }); + } catch (error) { + console.error('Failed to delete deployment:', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } + });