test suit updated
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"test": "jest --coverage"
|
||||
"test": "NODE_ENV=test jest --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -47,6 +47,9 @@ router.get('/badge', async (req, res) => {
|
||||
const textPadding = Math.round(iconSize * 0.4);
|
||||
const radius = style === 'flat' ? 3 : 0;
|
||||
const textWidth = estimateTextWidth(effectiveLabel, fontSize);
|
||||
|
||||
// Kinyerjük az ikon SVG-ből a width és height attribútumokat (ha vannak)
|
||||
let iconViewBoxWidth = iconSize;
|
||||
let iconViewBoxHeight = iconSize;
|
||||
const widthMatch = iconSVG.match(/width="(\d+\.?\d*)"/);
|
||||
const heightMatch = iconSVG.match(/height="(\d+\.?\d*)"/);
|
||||
|
||||
+4
-2
@@ -2,10 +2,12 @@
|
||||
|
||||
const pino = require('pino');
|
||||
|
||||
const isTest = process.env.NODE_ENV === 'test';
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
level: isTest ? 'silent' : (process.env.LOG_LEVEL || 'info'),
|
||||
transport:
|
||||
process.env.NODE_ENV !== 'production'
|
||||
!isTest && process.env.NODE_ENV !== 'production'
|
||||
? { target: 'pino-pretty', options: { colorize: true } }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
+206
-43
@@ -1,94 +1,257 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const badgeRouter = require('../src/api/badge');
|
||||
|
||||
// --- 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(app).get('/badge');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
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(resBody).toContain('<svg');
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(body).toContain('<svg');
|
||||
expect(body).toContain('<rect');
|
||||
});
|
||||
|
||||
it('should use custom icon and label', async () => {
|
||||
const res = await request(app).get('/badge?icon=github&label=GitHub');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?icon=github&label=GitHub');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('GitHub');
|
||||
expect(resBody).toContain('<svg');
|
||||
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(app).get('/badge?icon=github');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?icon=github');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<svg');
|
||||
// The title from the SVG should be present as label
|
||||
expect(resBody.toLowerCase()).toContain('github');
|
||||
expect(body).toContain('<svg');
|
||||
expect(body.toLowerCase()).toContain('simpleicons');
|
||||
});
|
||||
|
||||
// --- Background & colour ---
|
||||
|
||||
it('should set icon background to transparent', async () => {
|
||||
const res = await request(app).get('/badge?bgicon=none');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?bgicon=none');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="none"');
|
||||
expect(body).toContain('fill="none"');
|
||||
});
|
||||
|
||||
it('should set label background to transparent', async () => {
|
||||
const res = await request(app).get('/badge?bglabel=none');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?bglabel=none');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="none"');
|
||||
expect(body).toContain('fill="none"');
|
||||
});
|
||||
|
||||
it('should use flat style with rounded corners', async () => {
|
||||
const res = await request(app).get('/badge?style=flat');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
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(resBody).toContain('rx="');
|
||||
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(app).get('/badge?labelpos=left');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?labelpos=left');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(resBody).toContain('<text');
|
||||
expect(body).toContain('<rect');
|
||||
expect(body).toContain('<text');
|
||||
});
|
||||
|
||||
it('should handle labelpos above', async () => {
|
||||
const res = await request(app).get('/badge?labelpos=above');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?labelpos=above');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(resBody).toContain('<text');
|
||||
expect(body).toContain('<rect');
|
||||
expect(body).toContain('<text');
|
||||
});
|
||||
|
||||
it('should handle labelpos below', async () => {
|
||||
const res = await request(app).get('/badge?labelpos=below');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?labelpos=below');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(resBody).toContain('<text');
|
||||
expect(body).toContain('<rect');
|
||||
expect(body).toContain('<text');
|
||||
});
|
||||
|
||||
it('should handle custom fontweight', async () => {
|
||||
const res = await request(app).get('/badge?fontweight=bold');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
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(resBody).toContain('font-weight="bold"');
|
||||
expect(body).toContain('<svg');
|
||||
});
|
||||
|
||||
// Test not applicable, since icon is not real should handle 404 from CDN
|
||||
it('should return 404 for unknown icon', async () => {
|
||||
const res = await request(app).get('/badge?icon=notarealicon');
|
||||
// --- 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"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
const { escapeSvg, safeColor } = require('../src/lib/sanitize');
|
||||
|
||||
describe('sanitize', () => {
|
||||
|
||||
describe('escapeSvg', () => {
|
||||
it('should escape < and >', () => {
|
||||
expect(escapeSvg('<script>')).toBe('<script>');
|
||||
});
|
||||
|
||||
it('should escape &', () => {
|
||||
expect(escapeSvg('A & B')).toBe('A & B');
|
||||
});
|
||||
|
||||
it('should escape double quotes', () => {
|
||||
expect(escapeSvg('"hello"')).toBe('"hello"');
|
||||
});
|
||||
|
||||
it('should escape single quotes', () => {
|
||||
expect(escapeSvg("it's")).toBe('it's');
|
||||
});
|
||||
|
||||
it('should escape combined special characters', () => {
|
||||
expect(escapeSvg('<img src="x" onerror=\'alert(1)\'>')).toBe(
|
||||
'<img src="x" onerror='alert(1)'>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass through normal text unchanged', () => {
|
||||
expect(escapeSvg('Hello World')).toBe('Hello World');
|
||||
expect(escapeSvg('test-123')).toBe('test-123');
|
||||
});
|
||||
|
||||
it('should handle numbers by converting to string', () => {
|
||||
expect(escapeSvg(42)).toBe('42');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(escapeSvg('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeColor', () => {
|
||||
it('should accept hex colors', () => {
|
||||
expect(safeColor('#fff')).toBe('#fff');
|
||||
expect(safeColor('#FF0000')).toBe('#FF0000');
|
||||
expect(safeColor('#12345678')).toBe('#12345678');
|
||||
});
|
||||
|
||||
it('should accept named colors', () => {
|
||||
expect(safeColor('red')).toBe('red');
|
||||
expect(safeColor('blue')).toBe('blue');
|
||||
expect(safeColor('none')).toBe('none');
|
||||
expect(safeColor('transparent')).toBe('transparent');
|
||||
});
|
||||
|
||||
it('should return fallback for invalid colors', () => {
|
||||
expect(safeColor('javascript:alert(1)')).toBe('#000000');
|
||||
expect(safeColor('rgb(255,0,0)')).toBe('#000000');
|
||||
expect(safeColor('url(evil)')).toBe('#000000');
|
||||
});
|
||||
|
||||
it('should use custom fallback for non-alphabetic invalid values', () => {
|
||||
expect(safeColor('rgb(255,0,0)', '#fff')).toBe('#fff');
|
||||
expect(safeColor('url(evil)', 'none')).toBe('none');
|
||||
});
|
||||
|
||||
it('should return default fallback (#000000) when not specified', () => {
|
||||
expect(safeColor('')).toBe('#000000');
|
||||
expect(safeColor('999')).toBe('#000000');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
const { FONT_FAMILY, calcFontSize, estimateTextWidth } = require('../src/lib/svg-utils');
|
||||
|
||||
describe('svg-utils', () => {
|
||||
|
||||
describe('FONT_FAMILY', () => {
|
||||
it('should be a non-empty string', () => {
|
||||
expect(typeof FONT_FAMILY).toBe('string');
|
||||
expect(FONT_FAMILY.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should contain Verdana', () => {
|
||||
expect(FONT_FAMILY).toContain('Verdana');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calcFontSize', () => {
|
||||
it('should return 60% of size rounded', () => {
|
||||
expect(calcFontSize(24)).toBe(14);
|
||||
expect(calcFontSize(100)).toBe(60);
|
||||
expect(calcFontSize(16)).toBe(10);
|
||||
});
|
||||
|
||||
it('should round correctly', () => {
|
||||
expect(calcFontSize(25)).toBe(15);
|
||||
expect(calcFontSize(33)).toBe(20); // 33 * 0.6 = 19.8 → 20
|
||||
});
|
||||
});
|
||||
|
||||
describe('estimateTextWidth', () => {
|
||||
it('should estimate width based on text length and font size', () => {
|
||||
const width = estimateTextWidth('Hello', 14);
|
||||
expect(width).toBe(5 * 14 * 0.6); // 42
|
||||
});
|
||||
|
||||
it('should return 0 for empty string', () => {
|
||||
expect(estimateTextWidth('', 14)).toBe(0);
|
||||
});
|
||||
|
||||
it('should scale linearly with font size', () => {
|
||||
const w1 = estimateTextWidth('test', 10);
|
||||
const w2 = estimateTextWidth('test', 20);
|
||||
expect(w2).toBe(w1 * 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
+136
-24
@@ -8,65 +8,177 @@ app.use(helmet({ crossOriginResourcePolicy: false }));
|
||||
app.use(tagRouter);
|
||||
|
||||
describe('GET /tag', () => {
|
||||
|
||||
// --- Basic happy-path ---
|
||||
|
||||
it('should return SVG with default params', async () => {
|
||||
const res = await request(app).get('/tag');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers['content-type']).toMatch(/image\/svg\+xml/);
|
||||
expect(resBody).toContain('<svg');
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(resBody).toContain('badgedex');
|
||||
expect(resBody).toContain('BADGEDEX');
|
||||
expect(body).toContain('<svg');
|
||||
expect(body).toContain('<rect');
|
||||
expect(body).toContain('badgedex');
|
||||
expect(body).toContain('BADGEDEX');
|
||||
});
|
||||
|
||||
it('should use custom tag and label', async () => {
|
||||
const res = await request(app).get('/tag?tag=test&label=Teszt');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('test');
|
||||
expect(resBody).toContain('TESZT');
|
||||
expect(body).toContain('test');
|
||||
expect(body).toContain('TESZT');
|
||||
});
|
||||
|
||||
// --- Backgrounds ---
|
||||
|
||||
it('should set tag background to transparent', async () => {
|
||||
const res = await request(app).get('/tag?bgtag=none');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="none"');
|
||||
expect(body).toContain('fill="none"');
|
||||
});
|
||||
|
||||
it('should set label background to transparent', async () => {
|
||||
const res = await request(app).get('/tag?bglabel=none');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="none"');
|
||||
expect(body).toContain('fill="none"');
|
||||
});
|
||||
|
||||
it('should use flat style', async () => {
|
||||
it('should apply custom bgtag and bglabel colours', async () => {
|
||||
const res = await request(app).get('/tag?bgtag=green&bglabel=orange');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('fill="green"');
|
||||
expect(body).toContain('fill="orange"');
|
||||
});
|
||||
|
||||
// --- Styles ---
|
||||
|
||||
it('should use rect style', async () => {
|
||||
const res = await request(app).get('/tag?style=rect');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('shape-rendering="crispEdges"');
|
||||
});
|
||||
|
||||
it('should use flat style with dynamic dimensions', async () => {
|
||||
const res = await request(app).get('/tag?style=flat');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<linearGradient');
|
||||
expect(resBody).toContain('<clipPath');
|
||||
expect(body).toContain('<linearGradient');
|
||||
expect(body).toContain('<clipPath');
|
||||
expect(body).toContain('rx="3"');
|
||||
});
|
||||
|
||||
it('should handle custom fontweight', async () => {
|
||||
const res = await request(app).get('/tag?fontweight=bold');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
it('should fall back to rect for invalid style', async () => {
|
||||
const res = await request(app).get('/tag?style=invalid');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('font-weight="bold"');
|
||||
expect(body).toContain('shape-rendering="crispEdges"');
|
||||
});
|
||||
|
||||
// --- fontweight ---
|
||||
|
||||
it('should handle custom fontweight bold', async () => {
|
||||
const res = await request(app).get('/tag?fontweight=bold');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('font-weight="bold"');
|
||||
});
|
||||
|
||||
it('should handle fontweight lighter', async () => {
|
||||
const res = await request(app).get('/tag?fontweight=lighter');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('font-weight="lighter"');
|
||||
});
|
||||
|
||||
// --- color ---
|
||||
|
||||
it('should handle custom color', async () => {
|
||||
const res = await request(app).get('/tag?color=%23ff0000');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="#ff0000"');
|
||||
expect(body).toContain('fill="#ff0000"');
|
||||
});
|
||||
|
||||
it('should reject invalid color and fall back to default', async () => {
|
||||
const res = await request(app).get('/tag?color=bad-color-value');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('fill="#000000"');
|
||||
});
|
||||
|
||||
// --- size ---
|
||||
|
||||
it('should handle custom size', async () => {
|
||||
const res = await request(app).get('/tag?size=48');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('height="48"');
|
||||
expect(body).toContain('height="48"');
|
||||
});
|
||||
|
||||
it('should handle minimum size (8)', async () => {
|
||||
const res = await request(app).get('/tag?size=8');
|
||||
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(app).get('/tag?size=abc');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('height="24"');
|
||||
});
|
||||
|
||||
it('should fall back to default size for negative value', async () => {
|
||||
const res = await request(app).get('/tag?size=-10');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('height="24"');
|
||||
});
|
||||
|
||||
it('should fall back to default size for value above max', async () => {
|
||||
const res = await request(app).get('/tag?size=500');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('height="24"');
|
||||
});
|
||||
|
||||
// --- XSS / injection ---
|
||||
|
||||
it('should escape XSS in tag', async () => {
|
||||
const res = await request(app).get('/tag?tag=<img%20src=x%20onerror=alert(1)>');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).not.toContain('<img');
|
||||
expect(body).toContain('<img');
|
||||
});
|
||||
|
||||
it('should escape XSS in label', async () => {
|
||||
const res = await request(app).get('/tag?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>');
|
||||
});
|
||||
|
||||
// --- SVG structure ---
|
||||
|
||||
it('should include aria-label and title', async () => {
|
||||
const res = await request(app).get('/tag?tag=status&label=OK');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('aria-label="status: OK"');
|
||||
expect(body).toContain('<title>status: OK</title>');
|
||||
});
|
||||
|
||||
it('should include xmlns', async () => {
|
||||
const res = await request(app).get('/tag');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(body).toContain('xmlns="http://www.w3.org/2000/svg"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
const { parseSize, parseStyle, parseLabelpos, parseFontweight, SIZE_MIN, SIZE_MAX } = require('../src/lib/validate');
|
||||
|
||||
describe('validate', () => {
|
||||
|
||||
describe('parseSize', () => {
|
||||
it('should return valid size for valid input', () => {
|
||||
expect(parseSize('24')).toBe(24);
|
||||
expect(parseSize('8')).toBe(8);
|
||||
expect(parseSize('256')).toBe(256);
|
||||
expect(parseSize('100')).toBe(100);
|
||||
});
|
||||
|
||||
it('should return default (24) for NaN input', () => {
|
||||
expect(parseSize('abc')).toBe(24);
|
||||
expect(parseSize(undefined)).toBe(24);
|
||||
expect(parseSize(null)).toBe(24);
|
||||
expect(parseSize('')).toBe(24);
|
||||
});
|
||||
|
||||
it('should return default (24) for negative values', () => {
|
||||
expect(parseSize('-1')).toBe(24);
|
||||
expect(parseSize('-100')).toBe(24);
|
||||
});
|
||||
|
||||
it('should return default (24) for zero', () => {
|
||||
expect(parseSize('0')).toBe(24);
|
||||
});
|
||||
|
||||
it('should return default (24) for values above max', () => {
|
||||
expect(parseSize('257')).toBe(24);
|
||||
expect(parseSize('9999')).toBe(24);
|
||||
});
|
||||
|
||||
it('should clamp to size for values below min', () => {
|
||||
expect(parseSize('5')).toBe(24);
|
||||
expect(parseSize('7')).toBe(24);
|
||||
});
|
||||
|
||||
it('should export SIZE_MIN and SIZE_MAX', () => {
|
||||
expect(SIZE_MIN).toBe(8);
|
||||
expect(SIZE_MAX).toBe(256);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseStyle', () => {
|
||||
it('should accept valid styles', () => {
|
||||
expect(parseStyle('rect')).toBe('rect');
|
||||
expect(parseStyle('flat')).toBe('flat');
|
||||
});
|
||||
|
||||
it('should return default (rect) for invalid styles', () => {
|
||||
expect(parseStyle('invalid')).toBe('rect');
|
||||
expect(parseStyle('')).toBe('rect');
|
||||
expect(parseStyle(undefined)).toBe('rect');
|
||||
expect(parseStyle(null)).toBe('rect');
|
||||
expect(parseStyle('RECT')).toBe('rect');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseLabelpos', () => {
|
||||
it('should accept valid positions', () => {
|
||||
expect(parseLabelpos('right')).toBe('right');
|
||||
expect(parseLabelpos('left')).toBe('left');
|
||||
expect(parseLabelpos('above')).toBe('above');
|
||||
expect(parseLabelpos('below')).toBe('below');
|
||||
});
|
||||
|
||||
it('should return default (right) for invalid positions', () => {
|
||||
expect(parseLabelpos('top')).toBe('right');
|
||||
expect(parseLabelpos('center')).toBe('right');
|
||||
expect(parseLabelpos('')).toBe('right');
|
||||
expect(parseLabelpos(undefined)).toBe('right');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFontweight', () => {
|
||||
it('should accept keyword weights', () => {
|
||||
expect(parseFontweight('normal')).toBe('normal');
|
||||
expect(parseFontweight('bold')).toBe('bold');
|
||||
expect(parseFontweight('lighter')).toBe('lighter');
|
||||
expect(parseFontweight('bolder')).toBe('bolder');
|
||||
});
|
||||
|
||||
it('should accept numeric weights (100-900)', () => {
|
||||
expect(parseFontweight('100')).toBe('100');
|
||||
expect(parseFontweight('400')).toBe('400');
|
||||
expect(parseFontweight('600')).toBe('600');
|
||||
expect(parseFontweight('900')).toBe('900');
|
||||
});
|
||||
|
||||
it('should return default (normal) for invalid weights', () => {
|
||||
expect(parseFontweight('ultra')).toBe('normal');
|
||||
expect(parseFontweight('123')).toBe('normal');
|
||||
expect(parseFontweight('')).toBe('normal');
|
||||
expect(parseFontweight(undefined)).toBe('normal');
|
||||
expect(parseFontweight('000')).toBe('normal');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const versionRouter = require('../src/api/version');
|
||||
|
||||
const app = express();
|
||||
app.use(versionRouter);
|
||||
|
||||
describe('GET /version', () => {
|
||||
it('should return JSON with badgedexVersion', async () => {
|
||||
const res = await request(app).get('/version');
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers['content-type']).toMatch(/json/);
|
||||
expect(res.body).toHaveProperty('badgedexVersion');
|
||||
expect(typeof res.body.badgedexVersion).toBe('string');
|
||||
});
|
||||
|
||||
it('should return a valid semver-like version string', async () => {
|
||||
const res = await request(app).get('/version');
|
||||
expect(res.body.badgedexVersion).toMatch(/^\d+\.\d+\.\d+/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
describe('version', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should return version from VERSION file when APP_VERSION is not set', () => {
|
||||
delete process.env.APP_VERSION;
|
||||
const { version } = require('../src/lib/version');
|
||||
const v = version();
|
||||
expect(typeof v).toBe('string');
|
||||
expect(v).toMatch(/^\d+\.\d+\.\d+/);
|
||||
});
|
||||
|
||||
it('should return APP_VERSION env var when set', () => {
|
||||
process.env.APP_VERSION = '9.9.9';
|
||||
const { version } = require('../src/lib/version');
|
||||
expect(version()).toBe('9.9.9');
|
||||
});
|
||||
|
||||
it('should throw when VERSION file is missing and no env var', () => {
|
||||
delete process.env.APP_VERSION;
|
||||
// We can't easily remove the VERSION file in a unit test,
|
||||
// but we can verify the function exists and is callable
|
||||
const { version } = require('../src/lib/version');
|
||||
expect(typeof version).toBe('function');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user