Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions apps/backend/src/app/api/deployments/[id]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 });
Expand Down
39 changes: 39 additions & 0 deletions apps/backend/src/app/api/deployments/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 });
}

});