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
105 changes: 105 additions & 0 deletions apps/api/src/browserbase/browserbase.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// apps/api/src/browserbase/browserbase.controller.spec.ts
jest.mock('@db', () => ({
db: {},
Prisma: {
PrismaClientKnownRequestError: class PrismaClientKnownRequestError extends Error {
code: string;
constructor(message: string, { code }: { code: string }) {
super(message);
this.code = code;
}
},
},
}));

jest.mock('../auth/auth.server', () => ({
auth: {
api: {
getSession: jest.fn(),
},
},
}));

jest.mock('@trycompai/auth', () => ({
statement: {},
BUILT_IN_ROLE_PERMISSIONS: {},
}));

import { Test } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import type { Response } from 'express';
import { BrowserbaseController } from './browserbase.controller';
import { BrowserbaseService } from './browserbase.service';
import { HybridAuthGuard } from '../auth/hybrid-auth.guard';
import { PermissionGuard } from '../auth/permission.guard';

describe('BrowserbaseController.redirectToScreenshot', () => {
let controller: BrowserbaseController;
let service: jest.Mocked<Pick<BrowserbaseService, 'getScreenshotRedirectUrl'>>;

beforeEach(async () => {
service = {
getScreenshotRedirectUrl: jest.fn(),
} as jest.Mocked<Pick<BrowserbaseService, 'getScreenshotRedirectUrl'>>;

const moduleRef = await Test.createTestingModule({
controllers: [BrowserbaseController],
providers: [{ provide: BrowserbaseService, useValue: service }],
})
.overrideGuard(HybridAuthGuard)
.useValue({ canActivate: () => true })
.overrideGuard(PermissionGuard)
.useValue({ canActivate: () => true })
.compile();

controller = moduleRef.get(BrowserbaseController);
});

const makeRes = () => {
const res: Partial<Response> = { redirect: jest.fn() };
return res as Response & { redirect: jest.Mock };
};

it('302-redirects to the freshly minted presigned URL', async () => {
service.getScreenshotRedirectUrl.mockResolvedValue(
'https://s3.example.com/fresh-signed',
);
const res = makeRes();

await controller.redirectToScreenshot('bar_1', 'org_1', res);

expect(service.getScreenshotRedirectUrl).toHaveBeenCalledWith({
runId: 'bar_1',
organizationId: 'org_1',
download: false,
});
expect(res.redirect).toHaveBeenCalledWith(302, 'https://s3.example.com/fresh-signed');
});

it('passes download=true to the service when the query param is "true"', async () => {
service.getScreenshotRedirectUrl.mockResolvedValue(
'https://s3.example.com/fresh-signed-attachment',
);
const res = makeRes();

await controller.redirectToScreenshot('bar_1', 'org_1', res, 'true');

expect(service.getScreenshotRedirectUrl).toHaveBeenCalledWith({
runId: 'bar_1',
organizationId: 'org_1',
download: true,
});
});

it('propagates NotFoundException when the service throws', async () => {
service.getScreenshotRedirectUrl.mockRejectedValue(
new NotFoundException('Screenshot not found'),
);
const res = makeRes();

await expect(
controller.redirectToScreenshot('bar_missing', 'org_1', res),
).rejects.toBeInstanceOf(NotFoundException);
expect(res.redirect).not.toHaveBeenCalled();
});
});
27 changes: 27 additions & 0 deletions apps/api/src/browserbase/browserbase.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import {
Param,
Patch,
Post,
Query,
Res,
UseGuards,
} from '@nestjs/common';
import type { Response } from 'express';
import {
ApiOperation,
ApiParam,
Expand Down Expand Up @@ -371,4 +374,28 @@ export class BrowserbaseController {
runId,
)) as BrowserAutomationRunResponseDto | null;
}

@Get('runs/:runId/screenshot')
@RequirePermission('task', 'read')
@ApiOperation({
summary: 'Redirect to a freshly signed screenshot URL',
description:
'Issues a 302 redirect to a newly signed S3 URL so that "Open full size" links never serve an expired URL. Pass ?download=true to force an attachment download.',
})
@ApiParam({ name: 'runId', description: 'Run ID' })
@ApiResponse({ status: 302, description: 'Redirect to signed S3 URL' })
@ApiResponse({ status: 404, description: 'Run or screenshot not found' })
async redirectToScreenshot(
@Param('runId') runId: string,
@OrganizationId() organizationId: string,
@Res() res: Response,
@Query('download') download?: string,
): Promise<void> {
const url = await this.browserbaseService.getScreenshotRedirectUrl({
runId,
organizationId,
download: download === 'true' || download === '1',
});
res.redirect(302, url);
}
}
128 changes: 128 additions & 0 deletions apps/api/src/browserbase/browserbase.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// apps/api/src/browserbase/browserbase.service.spec.ts
import { Test } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { BrowserbaseService } from './browserbase.service';

jest.mock('@db', () => ({
db: {
browserAutomationRun: {
findUnique: jest.fn(),
},
},
}));

jest.mock('@/app/s3', () => ({
getSignedUrl: jest.fn().mockResolvedValue('https://s3.example.com/signed'),
s3Client: { send: jest.fn() },
BUCKET_NAME: 'test-bucket',
}));

import { db } from '@db';
import { getSignedUrl } from '@/app/s3';

describe('BrowserbaseService.getScreenshotRedirectUrl', () => {
let service: BrowserbaseService;

beforeEach(async () => {
jest.clearAllMocks();
const moduleRef = await Test.createTestingModule({
providers: [BrowserbaseService],
}).compile();
service = moduleRef.get(BrowserbaseService);
});

it('returns a freshly minted presigned URL for an in-scope run', async () => {
(db.browserAutomationRun.findUnique as jest.Mock).mockResolvedValue({
id: 'bar_1',
screenshotUrl: 'browser-automations/org_1/bau_1/bar_1.jpg',
automation: { task: { organizationId: 'org_1' } },
});

const url = await service.getScreenshotRedirectUrl({
runId: 'bar_1',
organizationId: 'org_1',
});

expect(url).toBe('https://s3.example.com/signed');
expect(db.browserAutomationRun.findUnique).toHaveBeenCalledWith({
where: { id: 'bar_1' },
include: { automation: { include: { task: true } } },
});
});

it('throws NotFoundException when the run does not exist', async () => {
(db.browserAutomationRun.findUnique as jest.Mock).mockResolvedValue(null);

await expect(
service.getScreenshotRedirectUrl({
runId: 'bar_missing',
organizationId: 'org_1',
}),
).rejects.toBeInstanceOf(NotFoundException);
});

it('throws NotFoundException when the run belongs to a different org', async () => {
(db.browserAutomationRun.findUnique as jest.Mock).mockResolvedValue({
id: 'bar_1',
screenshotUrl: 'browser-automations/org_2/bau_1/bar_1.jpg',
automation: { task: { organizationId: 'org_2' } },
});

await expect(
service.getScreenshotRedirectUrl({
runId: 'bar_1',
organizationId: 'org_1',
}),
).rejects.toBeInstanceOf(NotFoundException);
});

it('throws NotFoundException when the run has no screenshot', async () => {
(db.browserAutomationRun.findUnique as jest.Mock).mockResolvedValue({
id: 'bar_1',
screenshotUrl: null,
automation: { task: { organizationId: 'org_1' } },
});

await expect(
service.getScreenshotRedirectUrl({
runId: 'bar_1',
organizationId: 'org_1',
}),
).rejects.toBeInstanceOf(NotFoundException);
});

it('signs the URL without Content-Disposition when download is falsy', async () => {
(db.browserAutomationRun.findUnique as jest.Mock).mockResolvedValue({
id: 'bar_1',
screenshotUrl: 'browser-automations/org_1/bau_1/bar_1.jpg',
automation: { task: { organizationId: 'org_1' } },
});

await service.getScreenshotRedirectUrl({
runId: 'bar_1',
organizationId: 'org_1',
});

const command = (getSignedUrl as jest.Mock).mock.calls[0][1];
expect(command.input.ResponseContentDisposition).toBeUndefined();
});

it('signs the URL with attachment Content-Disposition when download is true', async () => {
(db.browserAutomationRun.findUnique as jest.Mock).mockResolvedValue({
id: 'bar_1',
screenshotUrl: 'browser-automations/org_1/bau_1/bar_1.jpg',
automation: { task: { organizationId: 'org_1' } },
});

await service.getScreenshotRedirectUrl({
runId: 'bar_1',
organizationId: 'org_1',
download: true,
});

const command = (getSignedUrl as jest.Mock).mock.calls[0][1];
expect(command.input.ResponseContentDisposition).toBe(
'attachment; filename="screenshot-bar_1.jpg"',
);
});
});
Loading
Loading