Add comprehensive route-level unit tests
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
- Add playground.test.ts with 14 tests for playground endpoint - Add screenshot.test.ts with 17 tests for screenshot endpoint - Add health.test.ts with 7 tests for health endpoint - Add watermark.test.ts with 14 tests for watermark service Total: 52 new tests covering: - Input validation and error handling - Authentication and authorization scenarios - Caching behavior and cache bypass - Parameter normalization and limits - SSRF protection and blocked URLs - Service error conditions (timeouts, queue full) - Browser pool integration - Watermark image processing logic All tests pass and use proper mocking of dependencies.
This commit is contained in:
parent
f696cb36db
commit
a20828b09c
4 changed files with 1255 additions and 0 deletions
220
src/routes/__tests__/health.test.ts
Normal file
220
src/routes/__tests__/health.test.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { Request, Response } from 'express'
|
||||
import { healthRouter } from '../health.js'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../services/browser.js', () => ({
|
||||
getPoolStats: vi.fn()
|
||||
}))
|
||||
|
||||
const { getPoolStats } = await import('../../services/browser.js')
|
||||
const mockGetPoolStats = vi.mocked(getPoolStats)
|
||||
|
||||
function createMockRequest(overrides: any = {}): Partial<Request> {
|
||||
return {
|
||||
method: 'GET',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function createMockResponse(): Partial<Response> {
|
||||
const res: any = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn().mockReturnThis()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
describe('Health Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('should return health status with browser pool stats', async () => {
|
||||
const mockPoolStats = {
|
||||
size: 3,
|
||||
available: 2,
|
||||
pending: 0,
|
||||
borrowed: 1,
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
|
||||
mockGetPoolStats.mockReturnValueOnce(mockPoolStats)
|
||||
|
||||
const req = createMockRequest()
|
||||
const res = createMockResponse()
|
||||
|
||||
// Get the GET handler from the router
|
||||
const handler = healthRouter.stack.find(layer =>
|
||||
layer.route?.methods.get && layer.route.path === '/'
|
||||
)?.route.stack[0].handle
|
||||
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockGetPoolStats).toHaveBeenCalledOnce()
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
status: "ok",
|
||||
version: "0.1.0",
|
||||
uptime: expect.any(Number),
|
||||
browser: mockPoolStats
|
||||
})
|
||||
})
|
||||
|
||||
it('should include process uptime in response', async () => {
|
||||
const mockPoolStats = {
|
||||
size: 2,
|
||||
available: 1,
|
||||
pending: 1,
|
||||
borrowed: 1,
|
||||
min: 1,
|
||||
max: 3
|
||||
}
|
||||
|
||||
mockGetPoolStats.mockReturnValueOnce(mockPoolStats)
|
||||
|
||||
const req = createMockRequest()
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = healthRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
const responseCall = res.json.mock.calls[0][0]
|
||||
expect(responseCall.uptime).toBeTypeOf('number')
|
||||
expect(responseCall.uptime).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should handle browser pool stats with different values', async () => {
|
||||
const mockPoolStats = {
|
||||
size: 0,
|
||||
available: 0,
|
||||
pending: 5,
|
||||
borrowed: 0,
|
||||
min: 0,
|
||||
max: 10
|
||||
}
|
||||
|
||||
mockGetPoolStats.mockReturnValueOnce(mockPoolStats)
|
||||
|
||||
const req = createMockRequest()
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = healthRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
status: "ok",
|
||||
version: "0.1.0",
|
||||
uptime: expect.any(Number),
|
||||
browser: mockPoolStats
|
||||
})
|
||||
})
|
||||
|
||||
it('should return consistent status format', async () => {
|
||||
mockGetPoolStats.mockReturnValueOnce({})
|
||||
|
||||
const req = createMockRequest()
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = healthRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
const responseCall = res.json.mock.calls[0][0]
|
||||
expect(responseCall).toHaveProperty('status', 'ok')
|
||||
expect(responseCall).toHaveProperty('version', '0.1.0')
|
||||
expect(responseCall).toHaveProperty('uptime')
|
||||
expect(responseCall).toHaveProperty('browser')
|
||||
})
|
||||
|
||||
it('should handle browser pool errors gracefully', async () => {
|
||||
// If getPoolStats throws, we should still get a response
|
||||
mockGetPoolStats.mockImplementationOnce(() => {
|
||||
throw new Error('Browser pool error')
|
||||
})
|
||||
|
||||
const req = createMockRequest()
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = healthRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
|
||||
// The handler doesn't try/catch getPoolStats, so this would throw
|
||||
// But in real usage, it might be wrapped by error middleware
|
||||
await expect(async () => {
|
||||
await handler(req, res, vi.fn())
|
||||
}).rejects.toThrow('Browser pool error')
|
||||
})
|
||||
|
||||
it('should not require authentication', async () => {
|
||||
// Health endpoint should be accessible without auth
|
||||
const mockPoolStats = { size: 1, available: 1 }
|
||||
mockGetPoolStats.mockReturnValueOnce(mockPoolStats)
|
||||
|
||||
const req = createMockRequest({
|
||||
headers: {} // No auth headers
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = healthRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
status: "ok",
|
||||
version: "0.1.0",
|
||||
uptime: expect.any(Number),
|
||||
browser: mockPoolStats
|
||||
})
|
||||
})
|
||||
|
||||
it('should return health data in expected format for monitoring', async () => {
|
||||
const mockPoolStats = {
|
||||
size: 3,
|
||||
available: 2,
|
||||
pending: 1,
|
||||
borrowed: 1,
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
|
||||
mockGetPoolStats.mockReturnValueOnce(mockPoolStats)
|
||||
|
||||
const req = createMockRequest()
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = healthRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
const response = res.json.mock.calls[0][0]
|
||||
|
||||
// Verify structure for monitoring systems
|
||||
expect(response.status).toBe('ok')
|
||||
expect(typeof response.version).toBe('string')
|
||||
expect(typeof response.uptime).toBe('number')
|
||||
expect(typeof response.browser).toBe('object')
|
||||
|
||||
// Browser stats should contain pool metrics
|
||||
expect(response.browser).toEqual(mockPoolStats)
|
||||
})
|
||||
})
|
||||
})
|
||||
321
src/routes/__tests__/playground.test.ts
Normal file
321
src/routes/__tests__/playground.test.ts
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { Request, Response } from 'express'
|
||||
import { playgroundRouter } from '../playground.js'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../services/screenshot.js', () => ({
|
||||
takeScreenshot: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../services/watermark.js', () => ({
|
||||
addWatermark: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../services/logger.js', () => ({
|
||||
default: {
|
||||
error: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('express-rate-limit', () => ({
|
||||
default: vi.fn(() => (req: any, res: any, next: any) => next())
|
||||
}))
|
||||
|
||||
const { takeScreenshot } = await import('../../services/screenshot.js')
|
||||
const { addWatermark } = await import('../../services/watermark.js')
|
||||
const mockTakeScreenshot = vi.mocked(takeScreenshot)
|
||||
const mockAddWatermark = vi.mocked(addWatermark)
|
||||
|
||||
function createMockRequest(body: any = {}, overrides: any = {}): Partial<Request> {
|
||||
return {
|
||||
body,
|
||||
ip: '127.0.0.1',
|
||||
socket: { remoteAddress: '127.0.0.1' } as any,
|
||||
method: 'POST',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function createMockResponse(): Partial<Response> {
|
||||
const res: any = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn().mockReturnThis(),
|
||||
send: vi.fn().mockReturnThis(),
|
||||
setHeader: vi.fn().mockReturnThis()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
describe('Playground Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('POST /v1/playground', () => {
|
||||
it('should return 400 when URL is missing', async () => {
|
||||
const req = createMockRequest({})
|
||||
const res = createMockResponse()
|
||||
|
||||
// Get the handler from the router
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Missing required parameter: url" })
|
||||
})
|
||||
|
||||
it('should return 400 when URL is not a string', async () => {
|
||||
const req = createMockRequest({ url: 123 })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Missing required parameter: url" })
|
||||
})
|
||||
|
||||
it('should return 400 when URL is empty string', async () => {
|
||||
const req = createMockRequest({ url: "" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Missing required parameter: url" })
|
||||
})
|
||||
|
||||
it('should successfully take screenshot with valid URL and return watermarked result', async () => {
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
const mockWatermarkedBuffer = Buffer.from('fake-watermarked-data')
|
||||
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
mockAddWatermark.mockResolvedValueOnce(mockWatermarkedBuffer)
|
||||
|
||||
const req = createMockRequest({ url: "https://example.com" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith({
|
||||
url: "https://example.com",
|
||||
format: "png",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
fullPage: false,
|
||||
quality: undefined,
|
||||
deviceScale: 1,
|
||||
waitUntil: "domcontentloaded",
|
||||
waitForSelector: undefined
|
||||
})
|
||||
expect(mockAddWatermark).toHaveBeenCalledWith(mockBuffer, 1280, 800)
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/png")
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Length", mockWatermarkedBuffer.length)
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Cache-Control", "no-store")
|
||||
expect(res.setHeader).toHaveBeenCalledWith("X-Playground", "true")
|
||||
expect(res.send).toHaveBeenCalledWith(mockWatermarkedBuffer)
|
||||
})
|
||||
|
||||
it('should enforce width limits (min 320, max 1920)', async () => {
|
||||
mockTakeScreenshot.mockResolvedValueOnce({ buffer: Buffer.from('test'), contentType: 'image/png' })
|
||||
mockAddWatermark.mockResolvedValueOnce(Buffer.from('watermarked'))
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
width: 100 // Below minimum
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
width: 320 // Should be clamped to minimum
|
||||
}))
|
||||
})
|
||||
|
||||
it('should enforce height limits (min 200, max 1080)', async () => {
|
||||
mockTakeScreenshot.mockResolvedValueOnce({ buffer: Buffer.from('test'), contentType: 'image/png' })
|
||||
mockAddWatermark.mockResolvedValueOnce(Buffer.from('watermarked'))
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
height: 2000 // Above maximum
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
height: 1080 // Should be clamped to maximum
|
||||
}))
|
||||
})
|
||||
|
||||
it('should handle format parameter correctly', async () => {
|
||||
mockTakeScreenshot.mockResolvedValueOnce({ buffer: Buffer.from('test'), contentType: 'image/jpeg' })
|
||||
mockAddWatermark.mockResolvedValueOnce(Buffer.from('watermarked'))
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
format: "jpeg",
|
||||
quality: 95
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
format: "jpeg",
|
||||
quality: 95
|
||||
}))
|
||||
})
|
||||
|
||||
it('should sanitize waitForSelector parameter', async () => {
|
||||
mockTakeScreenshot.mockResolvedValueOnce({ buffer: Buffer.from('test'), contentType: 'image/png' })
|
||||
mockAddWatermark.mockResolvedValueOnce(Buffer.from('watermarked'))
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
waitForSelector: "<script>alert('xss')</script>" // Malicious selector
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
waitForSelector: undefined // Should be sanitized out
|
||||
}))
|
||||
})
|
||||
|
||||
it('should allow valid CSS selector for waitForSelector', async () => {
|
||||
mockTakeScreenshot.mockResolvedValueOnce({ buffer: Buffer.from('test'), contentType: 'image/png' })
|
||||
mockAddWatermark.mockResolvedValueOnce(Buffer.from('watermarked'))
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
waitForSelector: "#main-content .article" // Valid CSS selector
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
waitForSelector: "#main-content .article"
|
||||
}))
|
||||
})
|
||||
|
||||
it('should return 503 when service queue is full', async () => {
|
||||
mockTakeScreenshot.mockRejectedValueOnce(new Error('QUEUE_FULL'))
|
||||
|
||||
const req = createMockRequest({ url: "https://example.com" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(503)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Service busy. Try again shortly." })
|
||||
})
|
||||
|
||||
it('should return 504 when screenshot times out', async () => {
|
||||
mockTakeScreenshot.mockRejectedValueOnce(new Error('SCREENSHOT_TIMEOUT'))
|
||||
|
||||
const req = createMockRequest({ url: "https://example.com" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(504)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Screenshot timed out." })
|
||||
})
|
||||
|
||||
it('should return 400 when URL is blocked (SSRF protection)', async () => {
|
||||
mockTakeScreenshot.mockRejectedValueOnce(new Error('URL blocked: private IP detected'))
|
||||
|
||||
const req = createMockRequest({ url: "http://192.168.1.1" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "URL blocked: private IP detected" })
|
||||
})
|
||||
|
||||
it('should return 400 when URL cannot be resolved', async () => {
|
||||
mockTakeScreenshot.mockRejectedValueOnce(new Error('Could not resolve hostname'))
|
||||
|
||||
const req = createMockRequest({ url: "https://nonexistent.invalid" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Could not resolve hostname" })
|
||||
})
|
||||
|
||||
it('should return 500 for generic screenshot errors', async () => {
|
||||
mockTakeScreenshot.mockRejectedValueOnce(new Error('Unexpected browser error'))
|
||||
|
||||
const req = createMockRequest({ url: "https://example.com" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Screenshot failed" })
|
||||
})
|
||||
|
||||
it('should enforce device scale limits (1-3)', async () => {
|
||||
mockTakeScreenshot.mockResolvedValueOnce({ buffer: Buffer.from('test'), contentType: 'image/png' })
|
||||
mockAddWatermark.mockResolvedValueOnce(Buffer.from('watermarked'))
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
deviceScale: 5 // Above maximum
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
deviceScale: 3 // Should be clamped to maximum
|
||||
}))
|
||||
})
|
||||
|
||||
it('should handle fullPage parameter', async () => {
|
||||
mockTakeScreenshot.mockResolvedValueOnce({ buffer: Buffer.from('test'), contentType: 'image/png' })
|
||||
mockAddWatermark.mockResolvedValueOnce(Buffer.from('watermarked'))
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
fullPage: true
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = playgroundRouter.stack.find(layer => layer.route?.methods.post)?.route.stack[1].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
fullPage: true
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
452
src/routes/__tests__/screenshot.test.ts
Normal file
452
src/routes/__tests__/screenshot.test.ts
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { Request, Response } from 'express'
|
||||
import { screenshotRouter } from '../screenshot.js'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../services/screenshot.js', () => ({
|
||||
takeScreenshot: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../services/cache.js', () => ({
|
||||
screenshotCache: {
|
||||
get: vi.fn(),
|
||||
put: vi.fn(),
|
||||
shouldBypass: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('../../services/logger.js', () => ({
|
||||
default: {
|
||||
error: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('../../middleware/auth.js', () => ({
|
||||
authMiddleware: vi.fn((req, res, next) => {
|
||||
// Mock successful authentication by default
|
||||
req.apiKeyInfo = { key: 'test_key', tier: 'pro', email: 'test@test.com' }
|
||||
next()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('../../middleware/usage.js', () => ({
|
||||
usageMiddleware: vi.fn((req, res, next) => next())
|
||||
}))
|
||||
|
||||
const { takeScreenshot } = await import('../../services/screenshot.js')
|
||||
const { screenshotCache } = await import('../../services/cache.js')
|
||||
const mockTakeScreenshot = vi.mocked(takeScreenshot)
|
||||
const mockCache = vi.mocked(screenshotCache)
|
||||
|
||||
function createMockRequest(params: any = {}, overrides: any = {}): Partial<Request> {
|
||||
const method = overrides.method || 'POST'
|
||||
return {
|
||||
method,
|
||||
body: method === 'POST' ? params : {},
|
||||
query: method === 'GET' ? params : {},
|
||||
headers: { authorization: 'Bearer test_key' },
|
||||
apiKeyInfo: { key: 'test_key', tier: 'pro', email: 'test@test.com' },
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function createMockResponse(): Partial<Response> {
|
||||
const res: any = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn().mockReturnThis(),
|
||||
send: vi.fn().mockReturnThis(),
|
||||
setHeader: vi.fn().mockReturnThis()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
describe('Screenshot Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Default cache behavior - no cache hit, no bypass
|
||||
mockCache.shouldBypass.mockReturnValue(false)
|
||||
mockCache.get.mockReturnValue(null)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('POST /v1/screenshot', () => {
|
||||
it('should return 400 when URL is missing', async () => {
|
||||
const req = createMockRequest({})
|
||||
const res = createMockResponse()
|
||||
|
||||
// Get the POST handler from the router
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post && layer.route.path === '/'
|
||||
)?.route.stack[0].handle
|
||||
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Missing required parameter: url" })
|
||||
})
|
||||
|
||||
it('should return 400 when URL is not a string', async () => {
|
||||
const req = createMockRequest({ url: 123 })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Missing required parameter: url" })
|
||||
})
|
||||
|
||||
it('should successfully take screenshot with valid parameters', async () => {
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
format: "png"
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith({
|
||||
url: "https://example.com",
|
||||
format: "png",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fullPage: false,
|
||||
quality: undefined,
|
||||
waitForSelector: undefined,
|
||||
deviceScale: undefined,
|
||||
delay: undefined,
|
||||
waitUntil: undefined,
|
||||
cache: undefined
|
||||
})
|
||||
|
||||
expect(mockCache.put).toHaveBeenCalledWith(expect.any(Object), mockBuffer, 'image/png')
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/png")
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Length", mockBuffer.length)
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Cache-Control", "no-store")
|
||||
expect(res.setHeader).toHaveBeenCalledWith("X-Cache", "MISS")
|
||||
expect(res.send).toHaveBeenCalledWith(mockBuffer)
|
||||
})
|
||||
|
||||
it('should return cached result when available', async () => {
|
||||
const cachedBuffer = Buffer.from('cached-screenshot-data')
|
||||
mockCache.get.mockReturnValueOnce({
|
||||
buffer: cachedBuffer,
|
||||
contentType: 'image/jpeg'
|
||||
})
|
||||
|
||||
const req = createMockRequest({ url: "https://example.com" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).not.toHaveBeenCalled()
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/jpeg")
|
||||
expect(res.setHeader).toHaveBeenCalledWith("X-Cache", "HIT")
|
||||
expect(res.send).toHaveBeenCalledWith(cachedBuffer)
|
||||
})
|
||||
|
||||
it('should bypass cache when cache=false', async () => {
|
||||
mockCache.shouldBypass.mockReturnValue(true)
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
cache: false
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockCache.get).not.toHaveBeenCalled()
|
||||
expect(mockCache.put).not.toHaveBeenCalled()
|
||||
expect(mockTakeScreenshot).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return 503 when service queue is full', async () => {
|
||||
mockTakeScreenshot.mockRejectedValueOnce(new Error('QUEUE_FULL'))
|
||||
|
||||
const req = createMockRequest({ url: "https://example.com" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(503)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Service busy. Try again shortly." })
|
||||
})
|
||||
|
||||
it('should return 504 when screenshot times out', async () => {
|
||||
mockTakeScreenshot.mockRejectedValueOnce(new Error('SCREENSHOT_TIMEOUT'))
|
||||
|
||||
const req = createMockRequest({ url: "https://example.com" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(504)
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: "Screenshot timed out. The page may be too slow to load."
|
||||
})
|
||||
})
|
||||
|
||||
it('should return 400 for blocked/invalid URLs', async () => {
|
||||
mockTakeScreenshot.mockRejectedValueOnce(new Error('URL blocked: private IP detected'))
|
||||
|
||||
const req = createMockRequest({ url: "http://192.168.1.1" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "URL blocked: private IP detected" })
|
||||
})
|
||||
|
||||
it('should return 500 for generic errors with details', async () => {
|
||||
mockTakeScreenshot.mockRejectedValueOnce(new Error('Browser crashed'))
|
||||
|
||||
const req = createMockRequest({ url: "https://example.com" })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500)
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: "Screenshot failed",
|
||||
details: "Browser crashed"
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /v1/screenshot', () => {
|
||||
it('should handle GET request with query parameters', async () => {
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
width: "800",
|
||||
height: "600",
|
||||
format: "png"
|
||||
}, { method: 'GET' })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith({
|
||||
url: "https://example.com",
|
||||
format: "png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
fullPage: false,
|
||||
quality: undefined,
|
||||
waitForSelector: undefined,
|
||||
deviceScale: undefined,
|
||||
delay: undefined,
|
||||
waitUntil: undefined,
|
||||
cache: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle fullPage parameter from query string', async () => {
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
fullPage: "true"
|
||||
}, { method: 'GET' })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
fullPage: true
|
||||
}))
|
||||
})
|
||||
|
||||
it('should handle deviceScale parameter from query string', async () => {
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
deviceScale: "2.5"
|
||||
}, { method: 'GET' })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
deviceScale: 2.5
|
||||
}))
|
||||
})
|
||||
|
||||
it('should validate waitUntil parameter', async () => {
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
waitUntil: "networkidle2"
|
||||
}, { method: 'GET' })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
waitUntil: "networkidle2"
|
||||
}))
|
||||
})
|
||||
|
||||
it('should ignore invalid waitUntil values', async () => {
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
waitUntil: "invalid"
|
||||
}, { method: 'GET' })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
waitUntil: undefined
|
||||
}))
|
||||
})
|
||||
|
||||
it('should return 400 when URL is missing in GET request', async () => {
|
||||
const req = createMockRequest({}, { method: 'GET' })
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.get
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400)
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Missing required parameter: url" })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parameter normalization', () => {
|
||||
it('should parse integer parameters correctly', async () => {
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/jpeg'
|
||||
})
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
width: "1200",
|
||||
height: "900",
|
||||
quality: "85",
|
||||
delay: "500"
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
width: 1200,
|
||||
height: 900,
|
||||
quality: 85,
|
||||
delay: 500
|
||||
}))
|
||||
})
|
||||
|
||||
it('should handle boolean parameters from strings', async () => {
|
||||
const mockBuffer = Buffer.from('fake-screenshot-data')
|
||||
mockTakeScreenshot.mockResolvedValueOnce({
|
||||
buffer: mockBuffer,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
const req = createMockRequest({
|
||||
url: "https://example.com",
|
||||
fullPage: "false"
|
||||
})
|
||||
const res = createMockResponse()
|
||||
|
||||
const handler = screenshotRouter.stack.find(layer =>
|
||||
layer.route?.methods.post
|
||||
)?.route.stack[0].handle
|
||||
await handler(req, res, vi.fn())
|
||||
|
||||
expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({
|
||||
fullPage: false
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
262
src/services/__tests__/watermark.test.ts
Normal file
262
src/services/__tests__/watermark.test.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { addWatermark } from '../watermark.js'
|
||||
|
||||
// Mock browser service
|
||||
vi.mock('../browser.js', () => ({
|
||||
acquirePage: vi.fn(),
|
||||
releasePage: vi.fn()
|
||||
}))
|
||||
|
||||
const { acquirePage, releasePage } = await import('../browser.js')
|
||||
const mockAcquirePage = vi.mocked(acquirePage)
|
||||
const mockReleasePage = vi.mocked(releasePage)
|
||||
|
||||
function createMockPage() {
|
||||
return {
|
||||
setViewport: vi.fn(),
|
||||
setContent: vi.fn(),
|
||||
screenshot: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
function createMockInstance() {
|
||||
return { id: 'test-instance' }
|
||||
}
|
||||
|
||||
describe('Watermark Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('addWatermark', () => {
|
||||
it('should add watermark to image buffer', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('input-image-data')
|
||||
const outputBuffer = Buffer.from('watermarked-image-data')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(outputBuffer as any)
|
||||
|
||||
const result = await addWatermark(inputBuffer, 1280, 800)
|
||||
|
||||
expect(mockAcquirePage).toHaveBeenCalledOnce()
|
||||
expect(mockPage.setViewport).toHaveBeenCalledWith({ width: 1280, height: 800 })
|
||||
expect(mockPage.setContent).toHaveBeenCalledWith(
|
||||
expect.stringContaining('data:image/png;base64,'),
|
||||
{ waitUntil: "load" }
|
||||
)
|
||||
expect(mockPage.screenshot).toHaveBeenCalledWith({
|
||||
type: "png",
|
||||
encoding: "binary"
|
||||
})
|
||||
expect(mockReleasePage).toHaveBeenCalledWith(mockPage, mockInstance)
|
||||
expect(result).toBeInstanceOf(Buffer)
|
||||
expect(result).toEqual(outputBuffer)
|
||||
})
|
||||
|
||||
it('should set viewport to specified dimensions', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test-image')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
await addWatermark(inputBuffer, 1920, 1080)
|
||||
|
||||
expect(mockPage.setViewport).toHaveBeenCalledWith({ width: 1920, height: 1080 })
|
||||
})
|
||||
|
||||
it('should include base64 encoded image in HTML content', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test-image-data')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
await addWatermark(inputBuffer, 800, 600)
|
||||
|
||||
const expectedBase64 = inputBuffer.toString('base64')
|
||||
const setContentCall = mockPage.setContent.mock.calls[0][0]
|
||||
|
||||
expect(setContentCall).toContain(`data:image/png;base64,${expectedBase64}`)
|
||||
})
|
||||
|
||||
it('should include watermark text in HTML content', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
await addWatermark(inputBuffer, 1000, 700)
|
||||
|
||||
const setContentCall = mockPage.setContent.mock.calls[0][0]
|
||||
|
||||
expect(setContentCall).toContain('snapapi.eu — upgrade for clean screenshots')
|
||||
})
|
||||
|
||||
it('should scale font size based on image width', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
await addWatermark(inputBuffer, 2000, 1000)
|
||||
|
||||
const setContentCall = mockPage.setContent.mock.calls[0][0]
|
||||
const expectedFontSize = Math.max(2000 / 20, 24) // 100px for 2000px width
|
||||
|
||||
expect(setContentCall).toContain(`font-size: ${expectedFontSize}px`)
|
||||
})
|
||||
|
||||
it('should enforce minimum font size', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
// Small width that would result in font size < 24px
|
||||
await addWatermark(inputBuffer, 400, 300)
|
||||
|
||||
const setContentCall = mockPage.setContent.mock.calls[0][0]
|
||||
|
||||
// Should use minimum font size of 24px
|
||||
expect(setContentCall).toContain('font-size: 24px')
|
||||
})
|
||||
|
||||
it('should include CSS styling for watermark', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
await addWatermark(inputBuffer, 1200, 800)
|
||||
|
||||
const setContentCall = mockPage.setContent.mock.calls[0][0]
|
||||
|
||||
// Check for key CSS properties
|
||||
expect(setContentCall).toContain('transform: rotate(-30deg)')
|
||||
expect(setContentCall).toContain('color: rgba(255, 255, 255, 0.35)')
|
||||
expect(setContentCall).toContain('text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.5)')
|
||||
expect(setContentCall).toContain('font-weight: 900')
|
||||
expect(setContentCall).toContain('pointer-events: none')
|
||||
})
|
||||
|
||||
it('should wait for page load before taking screenshot', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
await addWatermark(inputBuffer, 1000, 600)
|
||||
|
||||
expect(mockPage.setContent).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
{ waitUntil: "load" }
|
||||
)
|
||||
})
|
||||
|
||||
it('should release page even if screenshot fails', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockRejectedValueOnce(new Error('Screenshot failed'))
|
||||
|
||||
await expect(addWatermark(inputBuffer, 800, 600)).rejects.toThrow('Screenshot failed')
|
||||
|
||||
expect(mockReleasePage).toHaveBeenCalledWith(mockPage, mockInstance)
|
||||
})
|
||||
|
||||
it('should release page even if setContent fails', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.setContent.mockRejectedValueOnce(new Error('SetContent failed'))
|
||||
|
||||
await expect(addWatermark(inputBuffer, 800, 600)).rejects.toThrow('SetContent failed')
|
||||
|
||||
expect(mockReleasePage).toHaveBeenCalledWith(mockPage, mockInstance)
|
||||
})
|
||||
|
||||
it('should handle page acquisition failure', async () => {
|
||||
mockAcquirePage.mockRejectedValueOnce(new Error('No pages available'))
|
||||
|
||||
await expect(addWatermark(Buffer.from('test'), 800, 600))
|
||||
.rejects.toThrow('No pages available')
|
||||
|
||||
expect(mockReleasePage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should generate valid HTML structure', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
await addWatermark(inputBuffer, 1000, 700)
|
||||
|
||||
const html = mockPage.setContent.mock.calls[0][0]
|
||||
|
||||
// Check HTML structure
|
||||
expect(html).toContain('<!DOCTYPE html>')
|
||||
expect(html).toContain('<html>')
|
||||
expect(html).toContain('<head>')
|
||||
expect(html).toContain('<style>')
|
||||
expect(html).toContain('<body>')
|
||||
expect(html).toContain('<img')
|
||||
expect(html).toContain('<div class="watermark">')
|
||||
expect(html).toContain('</html>')
|
||||
})
|
||||
|
||||
it('should set correct body dimensions', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const inputBuffer = Buffer.from('test')
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
await addWatermark(inputBuffer, 1500, 900)
|
||||
|
||||
const html = mockPage.setContent.mock.calls[0][0]
|
||||
|
||||
expect(html).toContain('width: 1500px; height: 900px')
|
||||
})
|
||||
|
||||
it('should handle various image buffer sizes', async () => {
|
||||
const mockPage = createMockPage()
|
||||
const mockInstance = createMockInstance()
|
||||
const largeBuffer = Buffer.alloc(1024 * 1024, 'test') // 1MB buffer
|
||||
|
||||
mockAcquirePage.mockResolvedValueOnce({ page: mockPage, instance: mockInstance })
|
||||
mockPage.screenshot.mockResolvedValueOnce(Buffer.from('result') as any)
|
||||
|
||||
const result = await addWatermark(largeBuffer, 2000, 1200)
|
||||
|
||||
expect(mockPage.setContent).toHaveBeenCalled()
|
||||
expect(result).toBeInstanceOf(Buffer)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue