257 lines
9.1 KiB
JavaScript
257 lines
9.1 KiB
JavaScript
const request = require('supertest');
|
|
const express = require('express');
|
|
const helmet = require('helmet');
|
|
|
|
// --- Mock axios before requiring the router ---
|
|
jest.mock('axios');
|
|
const axios = require('axios');
|
|
|
|
const MOCK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>SimpleIcons</title><path fill="#333" d="M12 2L2 22h20z"/></svg>`;
|
|
|
|
beforeEach(() => {
|
|
axios.get.mockResolvedValue({ status: 200, data: MOCK_SVG });
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
// Fresh app for each test to avoid middleware leakage
|
|
function createApp() {
|
|
const badgeRouter = require('../src/api/badge');
|
|
const app = express();
|
|
app.use(helmet({ crossOriginResourcePolicy: false }));
|
|
app.use(badgeRouter);
|
|
return app;
|
|
}
|
|
|
|
describe('GET /badge', () => {
|
|
|
|
// --- Basic happy-path tests ---
|
|
|
|
it('should return SVG with default params', async () => {
|
|
const res = await request(createApp()).get('/badge');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.headers['content-type']).toMatch(/image\/svg\+xml/);
|
|
expect(body).toContain('<svg');
|
|
expect(body).toContain('<rect');
|
|
});
|
|
|
|
it('should use custom icon and label', async () => {
|
|
const res = await request(createApp()).get('/badge?icon=github&label=GitHub');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('GitHub');
|
|
expect(body).toContain('<svg');
|
|
});
|
|
|
|
it('should use label from SVG title if label param is missing', async () => {
|
|
const res = await request(createApp()).get('/badge?icon=github');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<svg');
|
|
expect(body.toLowerCase()).toContain('simpleicons');
|
|
});
|
|
|
|
// --- Background & colour ---
|
|
|
|
it('should set icon background to transparent', async () => {
|
|
const res = await request(createApp()).get('/badge?bgicon=none');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('fill="none"');
|
|
});
|
|
|
|
it('should set label background to transparent', async () => {
|
|
const res = await request(createApp()).get('/badge?bglabel=none');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('fill="none"');
|
|
});
|
|
|
|
it('should apply custom bgicon and bglabel colours', async () => {
|
|
const res = await request(createApp()).get('/badge?bgicon=red&bglabel=blue');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('fill="red"');
|
|
expect(body).toContain('fill="blue"');
|
|
});
|
|
|
|
it('should apply custom text colour', async () => {
|
|
const res = await request(createApp()).get('/badge?color=white');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('fill="white"');
|
|
});
|
|
|
|
// --- Styles ---
|
|
|
|
it('should use flat style with rounded corners (rx=3)', async () => {
|
|
const res = await request(createApp()).get('/badge?style=flat');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('rx="3"');
|
|
});
|
|
|
|
it('should use rect style with square corners (rx=0)', async () => {
|
|
const res = await request(createApp()).get('/badge?style=rect');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('rx="0"');
|
|
});
|
|
|
|
it('should fall back to rect for invalid style', async () => {
|
|
const res = await request(createApp()).get('/badge?style=invalid');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('rx="0"');
|
|
});
|
|
|
|
// --- labelpos ---
|
|
|
|
it('should handle labelpos left', async () => {
|
|
const res = await request(createApp()).get('/badge?labelpos=left');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<rect');
|
|
expect(body).toContain('<text');
|
|
});
|
|
|
|
it('should handle labelpos above', async () => {
|
|
const res = await request(createApp()).get('/badge?labelpos=above');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<rect');
|
|
expect(body).toContain('<text');
|
|
});
|
|
|
|
it('should handle labelpos below', async () => {
|
|
const res = await request(createApp()).get('/badge?labelpos=below');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<rect');
|
|
expect(body).toContain('<text');
|
|
});
|
|
|
|
it('should fall back to right for invalid labelpos', async () => {
|
|
const res = await request(createApp()).get('/badge?labelpos=diagonal');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<svg');
|
|
});
|
|
|
|
// --- fontweight ---
|
|
|
|
it('should handle custom fontweight bold', async () => {
|
|
const res = await request(createApp()).get('/badge?fontweight=bold');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('font-weight="bold"');
|
|
});
|
|
|
|
it('should handle numeric fontweight 600', async () => {
|
|
const res = await request(createApp()).get('/badge?fontweight=600');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('font-weight="600"');
|
|
});
|
|
|
|
it('should fall back to normal for invalid fontweight', async () => {
|
|
const res = await request(createApp()).get('/badge?fontweight=ultra');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('font-weight="normal"');
|
|
});
|
|
|
|
// --- size ---
|
|
|
|
it('should handle minimum valid size (8)', async () => {
|
|
const res = await request(createApp()).get('/badge?size=8');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<svg');
|
|
});
|
|
|
|
it('should handle large size (128)', async () => {
|
|
const res = await request(createApp()).get('/badge?size=128');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<svg');
|
|
});
|
|
|
|
it('should fall back to default size for NaN', async () => {
|
|
const res = await request(createApp()).get('/badge?size=abc');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<svg');
|
|
});
|
|
|
|
it('should fall back to default size for negative value', async () => {
|
|
const res = await request(createApp()).get('/badge?size=-5');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<svg');
|
|
});
|
|
|
|
it('should fall back to default size for value above max', async () => {
|
|
const res = await request(createApp()).get('/badge?size=9999');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('<svg');
|
|
});
|
|
|
|
// --- XSS / injection ---
|
|
|
|
it('should escape XSS in label', async () => {
|
|
const res = await request(createApp()).get('/badge?label=<script>alert(1)</script>');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).not.toContain('<script>');
|
|
expect(body).toContain('<script>');
|
|
});
|
|
|
|
it('should escape quotes in label', async () => {
|
|
const res = await request(createApp()).get('/badge?label=he%20said%20%22hi%22');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).toContain('"');
|
|
});
|
|
|
|
it('should reject invalid color with injection attempt', async () => {
|
|
const res = await request(createApp()).get('/badge?color=javascript:alert(1)');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(res.statusCode).toBe(200);
|
|
expect(body).not.toContain('javascript:');
|
|
});
|
|
|
|
// --- CDN error handling ---
|
|
|
|
it('should return 404 when CDN returns 404', async () => {
|
|
axios.get.mockRejectedValue({ response: { status: 404 } });
|
|
const res = await request(createApp()).get('/badge?icon=notarealicon');
|
|
expect(res.statusCode).toBe(404);
|
|
});
|
|
|
|
it('should return 500 on unexpected CDN error', async () => {
|
|
axios.get.mockRejectedValue(new Error('Network timeout'));
|
|
const res = await request(createApp()).get('/badge?icon=github');
|
|
expect(res.statusCode).toBe(500);
|
|
expect(res.text).toBe('Internal Server Error');
|
|
});
|
|
|
|
// --- SVG structure ---
|
|
|
|
it('should include drop shadow filter', async () => {
|
|
const res = await request(createApp()).get('/badge');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(body).toContain('filter id="shadow"');
|
|
expect(body).toContain('feDropShadow');
|
|
});
|
|
|
|
it('should include xmlns', async () => {
|
|
const res = await request(createApp()).get('/badge');
|
|
const body = Buffer.from(res.body).toString();
|
|
expect(body).toContain('xmlns="http://www.w3.org/2000/svg"');
|
|
});
|
|
}); |