mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-19 05:09:41 +02:00
Implement yaml config (#271)
This commit is contained in:
parent
5a7f1c843b
commit
13762096c4
20 changed files with 164 additions and 479 deletions
|
@ -3,15 +3,13 @@ import path from 'path';
|
|||
import { fileURLToPath } from 'url';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import morgan from 'morgan';
|
||||
import rcParser from './utils/rcparser.js';
|
||||
import routes from './routes/index.js';
|
||||
import HttpException from './exceptions/httpException.js';
|
||||
import * as dotenv from 'dotenv';
|
||||
import config from 'config';
|
||||
import HawkCatcher from '@hawk.so/nodejs';
|
||||
import os from 'os';
|
||||
import appConfig from 'config';
|
||||
import { downloadFavicon, FaviconData } from './utils/downloadFavicon.js';
|
||||
import appConfig from "./utils/appConfig.js";
|
||||
|
||||
/**
|
||||
* The __dirname CommonJS variables are not available in ES modules.
|
||||
|
@ -22,20 +20,20 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||
|
||||
dotenv.config();
|
||||
const app = express();
|
||||
const localConfig = rcParser.getConfiguration();
|
||||
const localConfig = appConfig.frontend;
|
||||
|
||||
// Initialize the backend error tracking catcher.
|
||||
if (process.env.HAWK_TOKEN_BACKEND) {
|
||||
HawkCatcher.init(process.env.HAWK_TOKEN_BACKEND);
|
||||
if (appConfig.hawk?.backendToken) {
|
||||
HawkCatcher.init(appConfig.hawk.backendToken);
|
||||
}
|
||||
|
||||
// Get url to upload favicon from config
|
||||
const favicon: string = appConfig.get('favicon');
|
||||
const favicon = appConfig.favicon
|
||||
|
||||
app.locals.config = localConfig;
|
||||
// Set client error tracking token as app local.
|
||||
if (process.env.HAWK_TOKEN_CLIENT) {
|
||||
app.locals.config.hawkClientToken = process.env.HAWK_TOKEN_CLIENT;
|
||||
if (appConfig.hawk?.frontendToken) {
|
||||
app.locals.config.hawkClientToken = appConfig.hawk.frontendToken;
|
||||
}
|
||||
|
||||
// view engine setup
|
||||
|
@ -69,7 +67,7 @@ app.use(express.json());
|
|||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, '../../public')));
|
||||
app.use('/uploads', express.static(config.get('uploads')));
|
||||
app.use('/uploads', express.static(appConfig.uploads));
|
||||
app.use('/favicon', express.static(downloadedFaviconFolder));
|
||||
|
||||
app.use('/', routes);
|
||||
|
@ -78,7 +76,7 @@ app.use('/', routes);
|
|||
// global error handler
|
||||
app.use(function (err: unknown, req: Request, res: Response, next: NextFunction) {
|
||||
// send any type of error to hawk server.
|
||||
if (process.env.HAWK_TOKEN_BACKEND && err instanceof Error) {
|
||||
if (appConfig.hawk?.backendToken && err instanceof Error) {
|
||||
HawkCatcher.send(err);
|
||||
}
|
||||
// only send Http based exception to client.
|
||||
|
|
|
@ -2,10 +2,10 @@ import fileType from 'file-type';
|
|||
import fetch from 'node-fetch';
|
||||
import fs from 'fs';
|
||||
import nodePath from 'path';
|
||||
import config from 'config';
|
||||
import File, { FileData } from '../models/file.js';
|
||||
import crypto from '../utils/crypto.js';
|
||||
import deepMerge from '../utils/objects.js';
|
||||
import appConfig from "../utils/appConfig.js";
|
||||
|
||||
const random16 = crypto.random16;
|
||||
|
||||
|
@ -71,7 +71,7 @@ class Transport {
|
|||
const type = await fileType.fromBuffer(buffer);
|
||||
const ext = type ? type.ext : nodePath.extname(url).slice(1);
|
||||
|
||||
fs.writeFileSync(`${config.get('uploads')}/${filename}.${ext}`, buffer);
|
||||
fs.writeFileSync(`${appConfig.uploads}/${filename}.${ext}`, buffer);
|
||||
|
||||
const fetchedContentType: string | null = fetchedFile.headers.get('content-type');
|
||||
let fetchedMimeType: string | undefined;
|
||||
|
@ -87,7 +87,7 @@ class Transport {
|
|||
const file = new File({
|
||||
name: url,
|
||||
filename: `${filename}.${ext}`,
|
||||
path: `${config.get('uploads')}/${filename}.${ext}`,
|
||||
path: `${appConfig.uploads}/${filename}.${ext}`,
|
||||
size: buffer.length,
|
||||
mimetype: mimeType,
|
||||
});
|
||||
|
|
|
@ -2,9 +2,9 @@ import { Request, Response, Router } from 'express';
|
|||
import multer, { StorageEngine } from 'multer';
|
||||
import mime from 'mime';
|
||||
import mkdirp from 'mkdirp';
|
||||
import config from 'config';
|
||||
import Transport from '../../controllers/transport.js';
|
||||
import { random16 } from '../../utils/crypto.js';
|
||||
import appConfig from "../../utils/appConfig.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
@ -15,7 +15,7 @@ const router = Router();
|
|||
*/
|
||||
const storage: StorageEngine = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const dir: string = config.get('uploads') || 'public/uploads';
|
||||
const dir: string = appConfig.uploads || 'public/uploads';
|
||||
|
||||
mkdirp(dir);
|
||||
cb(null, dir);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import express, { Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import config from 'config';
|
||||
import csrf from 'csurf';
|
||||
import appConfig from "../utils/appConfig.js";
|
||||
|
||||
const router = express.Router();
|
||||
const csrfProtection = csrf({ cookie: true });
|
||||
|
@ -22,7 +22,7 @@ router.get('/auth', csrfProtection, function (req: Request, res: Response) {
|
|||
*/
|
||||
router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!process.env.PASSWORD) {
|
||||
if (!appConfig.password) {
|
||||
res.render('auth', {
|
||||
title: 'Login page',
|
||||
header: 'Password not set',
|
||||
|
@ -32,7 +32,7 @@ router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Respon
|
|||
return;
|
||||
}
|
||||
|
||||
if (req.body.password !== process.env.PASSWORD) {
|
||||
if (req.body.password !== appConfig.password) {
|
||||
res.render('auth', {
|
||||
title: 'Login page',
|
||||
header: 'Wrong password',
|
||||
|
@ -46,7 +46,7 @@ router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Respon
|
|||
iss: 'Codex Team',
|
||||
sub: 'auth',
|
||||
iat: Date.now(),
|
||||
}, process.env.PASSWORD + config.get('secret'));
|
||||
}, appConfig.password + appConfig.auth.secret);
|
||||
|
||||
res.cookie('authToken', token, {
|
||||
httpOnly: true,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import config from 'config';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import appConfig from "../../utils/appConfig.js";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -14,14 +14,14 @@ export default async function verifyToken(req: Request, res: Response, next: Nex
|
|||
const token = req.cookies.authToken;
|
||||
|
||||
try {
|
||||
if (!process.env.PASSWORD) {
|
||||
if (!appConfig.password) {
|
||||
res.locals.isAuthorized = false;
|
||||
next();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const decodedToken = jwt.verify(token, process.env.PASSWORD + config.get('secret'));
|
||||
const decodedToken = jwt.verify(token, appConfig.password + appConfig.auth.secret);
|
||||
|
||||
res.locals.isAuthorized = !!decodedToken;
|
||||
|
||||
|
|
74
src/backend/utils/appConfig.ts
Normal file
74
src/backend/utils/appConfig.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { loadConfig } from 'config-loader';
|
||||
import * as process from 'process';
|
||||
import arg from 'arg';
|
||||
import path from 'path';
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Configuration for Hawk errors catcher
|
||||
*/
|
||||
const HawkConfig = z.object({
|
||||
backendToken: z.string().optional(), // Hawk backend token
|
||||
frontendToken: z.string().optional(), // Hawk frontend token
|
||||
})
|
||||
|
||||
const LocalDatabaseConfig = z.object({
|
||||
driver: z.literal('local'),
|
||||
local: z.object({
|
||||
path: z.string()
|
||||
})
|
||||
})
|
||||
|
||||
const AuthConfig = z.object({
|
||||
secret: z.string() // Secret for JWT
|
||||
})
|
||||
|
||||
const FrontendConfig = z.object({
|
||||
title: z.string(), // Title for pages
|
||||
description: z.string(), // Description for pages
|
||||
startPage: z.string(), // Start page
|
||||
misprintsChatId: z.string().optional(), // Telegram chat id for misprints
|
||||
yandexMetrikaId: z.string().optional(), // Yandex metrika id
|
||||
carbon: z.object({
|
||||
serve: z.string().optional(), // Carbon serve url
|
||||
placement: z.string().optional(), // Carbon placement
|
||||
}),
|
||||
menu: z.array(z.union([z.string(), z.object({title: z.string(), uri: z.string()})])), // Menu for pages
|
||||
})
|
||||
|
||||
/**
|
||||
* Application configuration
|
||||
*/
|
||||
const AppConfig = z.object({
|
||||
port: z.number(), // Port to listen on
|
||||
host: z.string(), // Host to listen on
|
||||
favicon: z.string().optional(), // Path or URL to favicon
|
||||
uploads: z.string(), // Path to uploads folder
|
||||
hawk: HawkConfig.optional().nullable(), // Hawk configuration
|
||||
password: z.string(), // Password for admin panel
|
||||
frontend: FrontendConfig, // Frontend configuration
|
||||
auth: AuthConfig, // Auth configuration
|
||||
database: LocalDatabaseConfig, // Database configuration
|
||||
})
|
||||
|
||||
export type AppConfig = z.infer<typeof AppConfig>;
|
||||
|
||||
const args = arg({ /* eslint-disable @typescript-eslint/naming-convention */
|
||||
'--config': [ String ],
|
||||
'-c': '--config',
|
||||
});
|
||||
|
||||
const cwd = process.cwd();
|
||||
const paths = (args['--config'] || ['./app-config.yaml']).map((configPath) => {
|
||||
if (path.isAbsolute(configPath)) {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
return path.join(cwd, configPath);
|
||||
});
|
||||
|
||||
const loadedConfig = loadConfig<AppConfig>(...paths);
|
||||
|
||||
const appConfig = AppConfig.parse(loadedConfig)
|
||||
|
||||
export default appConfig;
|
|
@ -1,6 +1,6 @@
|
|||
import Datastore from 'nedb';
|
||||
import config from 'config';
|
||||
import path from 'path';
|
||||
import appConfig from "../appConfig.js";
|
||||
|
||||
/**
|
||||
* Init function for nedb instance
|
||||
|
@ -10,7 +10,7 @@ import path from 'path';
|
|||
*/
|
||||
export default function initDb(name: string): Datastore {
|
||||
return new Datastore({
|
||||
filename: path.resolve(`${config.get('database')}/${name}.db`),
|
||||
filename: path.resolve(`${appConfig.database.local.path}/${name}.db`),
|
||||
autoload: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import config from 'config';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* The __dirname CommonJS variables are not available in ES modules.
|
||||
* https://nodejs.org/api/esm.html#no-__filename-or-__dirname
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const rcPath = path.resolve(__dirname, '../../../', config.get('rcFile') || './.codexdocsrc');
|
||||
|
||||
/**
|
||||
* @typedef {object} menu
|
||||
* @property {string} title - menu option title
|
||||
* @property {string} uri - menu option href
|
||||
*/
|
||||
interface Menu {
|
||||
title: string;
|
||||
uri: string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} RCData
|
||||
* @property {string} title - website title
|
||||
* @property {Menu[]} menu - options for website menu
|
||||
*/
|
||||
interface RCData {
|
||||
title: string;
|
||||
menu: Menu[];
|
||||
[key: string]: string | Menu[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @class RCParser
|
||||
* @classdesc Class to parse runtime configuration file for CodeX Docs engine
|
||||
*/
|
||||
export default class RCParser {
|
||||
/**
|
||||
* Default CodeX Docs configuration
|
||||
*
|
||||
* @static
|
||||
* @returns {{title: string, menu: Array}}
|
||||
*/
|
||||
public static get DEFAULTS():RCData {
|
||||
return {
|
||||
title: 'CodeX Docs',
|
||||
menu: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and parse runtime configuration file
|
||||
*
|
||||
* @static
|
||||
* @returns {{title: string, menu: []}}
|
||||
*/
|
||||
public static getConfiguration(): RCData {
|
||||
if (!fs.existsSync(rcPath)) {
|
||||
return RCParser.DEFAULTS;
|
||||
}
|
||||
|
||||
const file = fs.readFileSync(rcPath, 'utf-8');
|
||||
const rConfig = RCParser.DEFAULTS;
|
||||
let userConfig;
|
||||
|
||||
try {
|
||||
userConfig = JSON.parse(file);
|
||||
} catch (e) {
|
||||
console.log('CodeX Docs rc file should be in JSON format.');
|
||||
|
||||
return RCParser.DEFAULTS;
|
||||
}
|
||||
|
||||
for (const option in userConfig) {
|
||||
if (Object.prototype.hasOwnProperty.call(userConfig, option)) {
|
||||
rConfig[option] = userConfig[option] || RCParser.DEFAULTS[option] || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(rConfig.menu instanceof Array)) {
|
||||
console.log('Menu section in the rc file must be an array.');
|
||||
rConfig.menu = RCParser.DEFAULTS.menu;
|
||||
}
|
||||
|
||||
rConfig.menu = rConfig.menu.filter((option: string | Menu, i:number) => {
|
||||
i = i + 1;
|
||||
if (typeof option === 'string') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!option || option instanceof Array || typeof option !== 'object') {
|
||||
console.log(`Menu option #${i} in rc file must be a string or an object`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const { title, uri } = option;
|
||||
|
||||
if (!title || typeof title !== 'string') {
|
||||
console.log(`Menu option #${i} title must be a string.`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uri || typeof uri !== 'string') {
|
||||
console.log(`Menu option #${i} uri must be a string.`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
rConfig.menu = rConfig.menu.map((option: string | Menu) => {
|
||||
if (typeof option === 'string') {
|
||||
return {
|
||||
title: option,
|
||||
/* Replace all non alpha- and numeric-symbols with '-' */
|
||||
uri: '/' + option.toLowerCase().replace(/[ -/:-@[-`{-~]+/, '-'),
|
||||
};
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
|
||||
return rConfig;
|
||||
}
|
||||
}
|
|
@ -3,15 +3,15 @@
|
|||
*/
|
||||
import app from '../backend/app.js';
|
||||
import http from 'http';
|
||||
import config from 'config';
|
||||
import Debug from 'debug';
|
||||
import appConfig from "../backend/utils/appConfig.js";
|
||||
|
||||
const debug = Debug.debug('codex.editor.docs:server');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
const port = normalizePort(config.get('port') || '3000');
|
||||
const port = normalizePort(appConfig.port.toString() || '3000');
|
||||
|
||||
app.set('port', port);
|
||||
|
||||
|
|
|
@ -1,272 +0,0 @@
|
|||
import { expect } from 'chai';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import config from 'config';
|
||||
import sinon = require('sinon');
|
||||
|
||||
import rcParser from '../backend/utils/rcparser.js';
|
||||
|
||||
const rcPath = path.resolve(process.cwd(), config.get('rcFile'));
|
||||
|
||||
describe('RC file parser test', () => {
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(rcPath)) {
|
||||
fs.unlinkSync(rcPath);
|
||||
}
|
||||
});
|
||||
|
||||
it('Default config', async () => {
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(parsedConfig).to.be.deep.equal(rcParser.DEFAULTS);
|
||||
});
|
||||
|
||||
it('Invalid JSON formatted config', () => {
|
||||
const invalidJson = '{title: "Codex Docs"}';
|
||||
const spy = sinon.spy(console, 'log');
|
||||
|
||||
fs.writeFileSync(rcPath, invalidJson, 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy.calledWith('CodeX Docs rc file should be in JSON format.')).to.be.true;
|
||||
|
||||
expect(parsedConfig).to.be.deep.equal(rcParser.DEFAULTS);
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
it('Normal config', () => {
|
||||
const normalConfig = {
|
||||
title: 'Documentation',
|
||||
menu: [
|
||||
{ title: 'Option 1', uri: '/option1' },
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
],
|
||||
};
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(parsedConfig).to.be.deep.equal(normalConfig);
|
||||
});
|
||||
|
||||
it('Missed title', () => {
|
||||
const normalConfig = {
|
||||
menu: [
|
||||
{ title: 'Option 1', uri: '/option1' },
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
],
|
||||
};
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(parsedConfig.menu).to.be.deep.equal(normalConfig.menu);
|
||||
expect(parsedConfig.title).to.be.equal(rcParser.DEFAULTS.title);
|
||||
});
|
||||
|
||||
it('Missed menu', () => {
|
||||
const normalConfig = {
|
||||
title: 'Documentation',
|
||||
};
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(parsedConfig.title).to.be.equal(normalConfig.title);
|
||||
expect(parsedConfig.menu).to.be.deep.equal(rcParser.DEFAULTS.menu);
|
||||
});
|
||||
|
||||
it('Menu is not an array', () => {
|
||||
const normalConfig = {
|
||||
title: 'Documentation',
|
||||
menu: {
|
||||
0: { title: 'Option 1', uri: '/option1' },
|
||||
1: { title: 'Option 2', uri: '/option2' },
|
||||
2: { title: 'Option 3', uri: '/option3' },
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
const spy = sinon.spy(console, 'log');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy.calledWith('Menu section in the rc file must be an array.')).to.be.true;
|
||||
|
||||
expect(parsedConfig.title).to.be.equal(normalConfig.title);
|
||||
expect(parsedConfig.menu).to.be.deep.equal(rcParser.DEFAULTS.menu);
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
it('Menu option is a string', () => {
|
||||
const normalConfig = {
|
||||
title: 'Documentation',
|
||||
menu: [
|
||||
'Option 1',
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
],
|
||||
};
|
||||
|
||||
const expectedMenu = [
|
||||
{ title: 'Option 1', uri: '/option-1' },
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
];
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(parsedConfig.title).to.be.equal(normalConfig.title);
|
||||
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
|
||||
});
|
||||
|
||||
it('Menu option is not a string or an object', () => {
|
||||
const normalConfig = {
|
||||
title: 'Documentation',
|
||||
menu: [
|
||||
[ { title: 'Option 1', uri: '/option1' } ],
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
],
|
||||
};
|
||||
|
||||
const expectedMenu = [
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
];
|
||||
const spy = sinon.spy(console, 'log');
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy.calledWith('Menu option #1 in rc file must be a string or an object')).to.be.true;
|
||||
|
||||
expect(parsedConfig.title).to.be.equal(normalConfig.title);
|
||||
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
it('Menu option title is undefined', () => {
|
||||
const normalConfig = {
|
||||
title: 'Documentation',
|
||||
menu: [
|
||||
{ uri: '/option1' },
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
],
|
||||
};
|
||||
|
||||
const expectedMenu = [
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
];
|
||||
const spy = sinon.spy(console, 'log');
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy.calledWith('Menu option #1 title must be a string.')).to.be.true;
|
||||
|
||||
expect(parsedConfig.title).to.be.equal(normalConfig.title);
|
||||
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
it('Menu option title is not a string', () => {
|
||||
const normalConfig = {
|
||||
title: 'Documentation',
|
||||
menu: [
|
||||
{ title: [], uri: '/option1' },
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
],
|
||||
};
|
||||
|
||||
const expectedMenu = [
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
];
|
||||
const spy = sinon.spy(console, 'log');
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy.calledWith('Menu option #1 title must be a string.')).to.be.true;
|
||||
|
||||
expect(parsedConfig.title).to.be.equal(normalConfig.title);
|
||||
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
it('Menu option uri is undefined', () => {
|
||||
const normalConfig = {
|
||||
title: 'Documentation',
|
||||
menu: [
|
||||
{ title: 'Option 1' },
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
],
|
||||
};
|
||||
|
||||
const expectedMenu = [
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
];
|
||||
const spy = sinon.spy(console, 'log');
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy.calledWith('Menu option #1 uri must be a string.')).to.be.true;
|
||||
|
||||
expect(parsedConfig.title).to.be.equal(normalConfig.title);
|
||||
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
it('Menu option title is not a string', () => {
|
||||
const normalConfig = {
|
||||
title: 'Documentation',
|
||||
menu: [
|
||||
{ title: 'Option 1', uri: [] },
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
],
|
||||
};
|
||||
|
||||
const expectedMenu = [
|
||||
{ title: 'Option 2', uri: '/option2' },
|
||||
{ title: 'Option 3', uri: '/option3' },
|
||||
];
|
||||
const spy = sinon.spy(console, 'log');
|
||||
|
||||
fs.writeFileSync(rcPath, JSON.stringify(normalConfig), 'utf8');
|
||||
|
||||
const parsedConfig = rcParser.getConfiguration();
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy.calledWith('Menu option #1 uri must be a string.')).to.be.true;
|
||||
|
||||
expect(parsedConfig.title).to.be.equal(normalConfig.title);
|
||||
expect(parsedConfig.menu).to.be.deep.equal(expectedMenu);
|
||||
spy.restore();
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue