-
-
-
- Back home
-
-
-
+
+
+
+ 404 Not Found
+ Sorry, this page does not seem to exist
+ Maybe the cache is doing tricky things, try force-refreshing?
+
+
+ Back home
+
-
-
diff --git a/src/tools/benchmark-builder/index.ts b/src/tools/benchmark-builder/index.ts
index b9dcf114..51eb8058 100644
--- a/src/tools/benchmark-builder/index.ts
+++ b/src/tools/benchmark-builder/index.ts
@@ -8,4 +8,5 @@ export const tool = defineTool({
keywords: ['benchmark', 'builder', 'execution', 'duration', 'mean', 'variance'],
component: () => import('./benchmark-builder.vue'),
icon: SpeedFilled,
+ createdAt: new Date('2023-04-05'),
});
diff --git a/src/tools/date-time-converter/date-time-converter.e2e.spec.ts b/src/tools/date-time-converter/date-time-converter.e2e.spec.ts
new file mode 100644
index 00000000..50155609
--- /dev/null
+++ b/src/tools/date-time-converter/date-time-converter.e2e.spec.ts
@@ -0,0 +1,33 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Date time converter - json to yaml', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/date-converter');
+ });
+
+ test('Has correct title', async ({ page }) => {
+ await expect(page).toHaveTitle('Date-time converter - IT Tools');
+ });
+
+ test('Format is auto detected from a date and the date is correctly converted', async ({ page }) => {
+ const initialFormat = await page.getByTestId('date-time-converter-format-select').innerText();
+ expect(initialFormat.trim()).toEqual('Timestamp');
+
+ await page.getByTestId('date-time-converter-input').fill('2023-04-12T23:10:24+02:00');
+
+ const detectedFormat = await page.getByTestId('date-time-converter-format-select').innerText();
+ expect(detectedFormat.trim()).toEqual('ISO 8601');
+
+ expect((await page.getByTestId('JS locale date string').inputValue()).trim()).toEqual(
+ 'Wed Apr 12 2023 23:10:24 GMT+0200 (Central European Summer Time)',
+ );
+ expect((await page.getByTestId('ISO 8601').inputValue()).trim()).toEqual('2023-04-12T23:10:24+02:00');
+ expect((await page.getByTestId('ISO 9075').inputValue()).trim()).toEqual('2023-04-12 23:10:24');
+ expect((await page.getByTestId('Unix timestamp').inputValue()).trim()).toEqual('1681333824');
+ expect((await page.getByTestId('RFC 7231').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
+ expect((await page.getByTestId('RFC 3339').inputValue()).trim()).toEqual('2023-04-12T23:10:24+02:00');
+ expect((await page.getByTestId('Timestamp').inputValue()).trim()).toEqual('1681333824000');
+ expect((await page.getByTestId('UTC format').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
+ expect((await page.getByTestId('Mongo ObjectID').inputValue()).trim()).toEqual('64371e400000000000000000');
+ });
+});
diff --git a/src/tools/date-time-converter/date-time-converter.models.test.ts b/src/tools/date-time-converter/date-time-converter.models.test.ts
new file mode 100644
index 00000000..e3a82f88
--- /dev/null
+++ b/src/tools/date-time-converter/date-time-converter.models.test.ts
@@ -0,0 +1,142 @@
+import { describe, test, expect } from 'vitest';
+import {
+ isISO8601DateTimeString,
+ isISO9075DateString,
+ isRFC3339DateString,
+ isRFC7231DateString,
+ isUnixTimestamp,
+ isTimestamp,
+ isUTCDateString,
+ isMongoObjectId,
+} from './date-time-converter.models';
+
+describe('date-time-converter models', () => {
+ describe('isISO8601DateTimeString', () => {
+ test('should return true for valid ISO 8601 date strings', () => {
+ expect(isISO8601DateTimeString('2021-01-01T00:00:00.000Z')).toBe(true);
+ expect(isISO8601DateTimeString('2023-04-12T14:56:00+01:00')).toBe(true);
+ expect(isISO8601DateTimeString('20230412T145600+0100')).toBe(true);
+ expect(isISO8601DateTimeString('20230412T145600Z')).toBe(true);
+ expect(isISO8601DateTimeString('2016-02-01')).toBe(true);
+ expect(isISO8601DateTimeString('2016')).toBe(true);
+ });
+
+ test('should return false for invalid ISO 8601 date strings', () => {
+ expect(isISO8601DateTimeString()).toBe(false);
+ expect(isISO8601DateTimeString('')).toBe(false);
+ expect(isISO8601DateTimeString('qsdqsd')).toBe(false);
+ expect(isISO8601DateTimeString('2016-02-01-')).toBe(false);
+ expect(isISO8601DateTimeString('2021-01-01T00:00:00.')).toBe(false);
+ });
+ });
+
+ describe('isISO9075DateString', () => {
+ test('should return true for valid ISO 9075 date strings', () => {
+ expect(isISO9075DateString('2022-01-01 12:00:00Z')).toBe(true);
+ expect(isISO9075DateString('2022-01-01 12:00:00.123456Z')).toBe(true);
+ expect(isISO9075DateString('2022-01-01 12:00:00+01:00')).toBe(true);
+ expect(isISO9075DateString('2022-01-01 12:00:00-05:00')).toBe(true);
+ });
+
+ test('should return false for invalid ISO 9075 date strings', () => {
+ expect(isISO9075DateString('2022/01/01T12:00:00Z')).toBe(false);
+ expect(isISO9075DateString('2022-01-01 12:00:00.123456789Z')).toBe(false);
+ expect(isISO9075DateString('2022-01-01 12:00:00+1:00')).toBe(false);
+ expect(isISO9075DateString('2022-01-01 12:00:00-05:')).toBe(false);
+ expect(isISO9075DateString('2022-01-01 12:00:00-05:00:00')).toBe(false);
+ expect(isISO9075DateString('2022-01-01')).toBe(false);
+ expect(isISO9075DateString('12:00:00Z')).toBe(false);
+ expect(isISO9075DateString('2022-01-01T12:00:00Zfoo')).toBe(false);
+ });
+ });
+
+ describe('isRFC3339DateString', () => {
+ test('should return true for valid RFC 3339 date strings', () => {
+ expect(isRFC3339DateString('2022-01-01T12:00:00Z')).toBe(true);
+ expect(isRFC3339DateString('2022-01-01T12:00:00.123456789Z')).toBe(true);
+ expect(isRFC3339DateString('2022-01-01T12:00:00.123456789+01:00')).toBe(true);
+ expect(isRFC3339DateString('2022-01-01T12:00:00-05:00')).toBe(true);
+ });
+
+ test('should return false for invalid RFC 3339 date strings', () => {
+ expect(isRFC3339DateString('2022/01/01T12:00:00Z')).toBe(false);
+ expect(isRFC3339DateString('2022-01-01T12:00:00.123456789+1:00')).toBe(false);
+ expect(isRFC3339DateString('2022-01-01T12:00:00-05:')).toBe(false);
+ expect(isRFC3339DateString('2022-01-01T12:00:00-05:00:00')).toBe(false);
+ expect(isRFC3339DateString('2022-01-01')).toBe(false);
+ expect(isRFC3339DateString('12:00:00Z')).toBe(false);
+ expect(isRFC3339DateString('2022-01-01T12:00:00Zfoo')).toBe(false);
+ });
+ });
+
+ describe('isRFC7231DateString', () => {
+ test('should return true for valid RFC 7231 date strings', () => {
+ expect(isRFC7231DateString('Sun, 06 Nov 1994 08:49:37 GMT')).toBe(true);
+ expect(isRFC7231DateString('Tue, 22 Apr 2014 07:00:00 GMT')).toBe(true);
+ });
+
+ test('should return false for invalid RFC 7231 date strings', () => {
+ expect(isRFC7231DateString('06 Nov 1994 08:49:37 GMT')).toBe(false);
+ expect(isRFC7231DateString('Sun, 06 Nov 94 08:49:37 GMT')).toBe(false);
+ expect(isRFC7231DateString('Sun, 06 Nov 1994 8:49:37 GMT')).toBe(false);
+ expect(isRFC7231DateString('Sun, 06 Nov 1994 08:49:37 GMT-0500')).toBe(false);
+ expect(isRFC7231DateString('Sun, 06 November 1994 08:49:37 GMT')).toBe(false);
+ expect(isRFC7231DateString('Sunday, 06 Nov 1994 08:49:37 GMT')).toBe(false);
+ expect(isRFC7231DateString('06 Nov 1994')).toBe(false);
+ });
+ });
+
+ describe('isUnixTimestamp', () => {
+ test('should return true for valid Unix timestamps', () => {
+ expect(isUnixTimestamp('1649789394')).toBe(true);
+ expect(isUnixTimestamp('1234567890')).toBe(true);
+ expect(isUnixTimestamp('0')).toBe(true);
+ });
+
+ test('should return false for invalid Unix timestamps', () => {
+ expect(isUnixTimestamp('foo')).toBe(false);
+ expect(isUnixTimestamp('')).toBe(false);
+ });
+ });
+
+ describe('isTimestamp', () => {
+ test('should return true for valid Unix timestamps in milliseconds', () => {
+ expect(isTimestamp('1649792026123')).toBe(true);
+ expect(isTimestamp('1234567890000')).toBe(true);
+ expect(isTimestamp('0')).toBe(true);
+ });
+
+ test('should return false for invalid Unix timestamps in milliseconds', () => {
+ expect(isTimestamp('foo')).toBe(false);
+ expect(isTimestamp('')).toBe(false);
+ });
+ });
+
+ describe('isUTCDateString', () => {
+ test('should return true for valid UTC date strings', () => {
+ expect(isUTCDateString('Sun, 06 Nov 1994 08:49:37 GMT')).toBe(true);
+ expect(isUTCDateString('Tue, 22 Apr 2014 07:00:00 GMT')).toBe(true);
+ });
+
+ test('should return false for invalid UTC date strings', () => {
+ expect(isUTCDateString('06 Nov 1994 08:49:37 GMT')).toBe(false);
+ expect(isUTCDateString('16497920261')).toBe(false);
+ expect(isUTCDateString('foo')).toBe(false);
+ expect(isUTCDateString('')).toBe(false);
+ });
+ });
+
+ describe('isMongoObjectId', () => {
+ test('should return true for valid Mongo ObjectIds', () => {
+ expect(isMongoObjectId('507f1f77bcf86cd799439011')).toBe(true);
+ expect(isMongoObjectId('507f1f77bcf86cd799439012')).toBe(true);
+ });
+
+ test('should return false for invalid Mongo ObjectIds', () => {
+ expect(isMongoObjectId('507f1f77bcf86cd79943901')).toBe(false);
+ expect(isMongoObjectId('507f1f77bcf86cd79943901z')).toBe(false);
+ expect(isMongoObjectId('foo')).toBe(false);
+ expect(isMongoObjectId('')).toBe(false);
+ });
+ });
+});
diff --git a/src/tools/date-time-converter/date-time-converter.models.ts b/src/tools/date-time-converter/date-time-converter.models.ts
new file mode 100644
index 00000000..3697c166
--- /dev/null
+++ b/src/tools/date-time-converter/date-time-converter.models.ts
@@ -0,0 +1,46 @@
+import _ from 'lodash';
+
+export {
+ isISO8601DateTimeString,
+ isISO9075DateString,
+ isRFC3339DateString,
+ isRFC7231DateString,
+ isUnixTimestamp,
+ isTimestamp,
+ isUTCDateString,
+ isMongoObjectId,
+};
+
+const ISO8601_REGEX =
+ /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
+const ISO9075_REGEX =
+ /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
+
+const RFC3339_REGEX =
+ /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
+
+const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
+
+function createRegexMatcher(regex: RegExp) {
+ return (date?: string) => !_.isNil(date) && regex.test(date);
+}
+
+const isISO8601DateTimeString = createRegexMatcher(ISO8601_REGEX);
+const isISO9075DateString = createRegexMatcher(ISO9075_REGEX);
+const isRFC3339DateString = createRegexMatcher(RFC3339_REGEX);
+const isRFC7231DateString = createRegexMatcher(RFC7231_REGEX);
+const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/);
+const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
+const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
+
+function isUTCDateString(date?: string) {
+ if (_.isNil(date)) {
+ return false;
+ }
+
+ try {
+ return new Date(date).toUTCString() === date;
+ } catch (_ignored) {
+ return false;
+ }
+}
diff --git a/src/tools/date-time-converter/date-time-converter.types.ts b/src/tools/date-time-converter/date-time-converter.types.ts
new file mode 100644
index 00000000..7c78b115
--- /dev/null
+++ b/src/tools/date-time-converter/date-time-converter.types.ts
@@ -0,0 +1,8 @@
+export type ToDateMapper = (value: string) => Date;
+
+export type DateFormat = {
+ name: string;
+ fromDate: (date: Date) => string;
+ toDate: (value: string) => Date;
+ formatMatcher: (dateString: string) => boolean;
+};
diff --git a/src/tools/date-time-converter/date-time-converter.vue b/src/tools/date-time-converter/date-time-converter.vue
index c83a8b5f..c18b1b71 100644
--- a/src/tools/date-time-converter/date-time-converter.vue
+++ b/src/tools/date-time-converter/date-time-converter.vue
@@ -1,44 +1,38 @@
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
- {{ name }}:
-
-
-
-
+
+
+
+
+
+
+ {{ name }}:
+
+
+
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 16e37f67..b3882975 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,6 +1,10 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
+import { tool as yamlToJson } from './yaml-to-json-converter';
+import { tool as jsonToYaml } from './json-to-yaml-converter';
+import { tool as ipv6UlaGenerator } from './ipv6-ula-generator';
+import { tool as ipv4AddressConverter } from './ipv4-address-converter';
import { tool as jsonToGo } from './json-to-go';
import { tool as benchmarkBuilder } from './benchmark-builder';
import { tool as userAgentParser } from './user-agent-parser';
@@ -65,6 +69,8 @@ export const toolsByCategory: ToolCategory[] = [
colorConverter,
caseConverter,
textToNatoAlphabet,
+ yamlToJson,
+ jsonToYaml,
],
},
{
@@ -105,7 +111,7 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Network',
- components: [ipv4SubnetCalculator, macAddressLookup],
+ components: [ipv4SubnetCalculator, ipv4AddressConverter, macAddressLookup, ipv6UlaGenerator],
},
{
name: 'Math',
diff --git a/src/tools/ipv4-address-converter/index.ts b/src/tools/ipv4-address-converter/index.ts
new file mode 100644
index 00000000..62d2daf1
--- /dev/null
+++ b/src/tools/ipv4-address-converter/index.ts
@@ -0,0 +1,12 @@
+import { Binary } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+ name: 'Ipv4 address converter',
+ path: '/ipv4-address-converter',
+ description: 'Convert an ip address into decimal, binary, hexadecimal or event in ipv6',
+ keywords: ['ipv4', 'address', 'converter', 'decimal', 'hexadecimal', 'binary', 'ipv6'],
+ component: () => import('./ipv4-address-converter.vue'),
+ icon: Binary,
+ createdAt: new Date('2023-04-08'),
+});
diff --git a/src/tools/ipv4-address-converter/ipv4-address-converter.service.test.ts b/src/tools/ipv4-address-converter/ipv4-address-converter.service.test.ts
new file mode 100644
index 00000000..ecdcfa2c
--- /dev/null
+++ b/src/tools/ipv4-address-converter/ipv4-address-converter.service.test.ts
@@ -0,0 +1,36 @@
+import { expect, describe, it } from 'vitest';
+import { isValidIpv4, ipv4ToInt } from './ipv4-address-converter.service';
+
+describe('ipv4-address-converter', () => {
+ describe('ipv4ToInt', () => {
+ it('should convert an IPv4 address to an integer', () => {
+ expect(ipv4ToInt({ ip: '192.168.0.1' })).toBe(3232235521);
+ expect(ipv4ToInt({ ip: '10.0.0.1' })).toBe(167772161);
+ expect(ipv4ToInt({ ip: '255.255.255.255' })).toBe(4294967295);
+ });
+ });
+
+ describe('isValidIpv4', () => {
+ it('should return true for a valid IP address', () => {
+ expect(isValidIpv4({ ip: '192.168.0.1' })).to.equal(true);
+ expect(isValidIpv4({ ip: '10.0.0.1' })).to.equal(true);
+ });
+
+ it('should return false for an invalid IP address', () => {
+ expect(isValidIpv4({ ip: '256.168.0.1' })).to.equal(false);
+ expect(isValidIpv4({ ip: '192.168.0' })).to.equal(false);
+ expect(isValidIpv4({ ip: '192.168.0.1.2' })).to.equal(false);
+ expect(isValidIpv4({ ip: '192.168.0.1.' })).to.equal(false);
+ expect(isValidIpv4({ ip: '.192.168.0.1' })).to.equal(false);
+ expect(isValidIpv4({ ip: '192.168.0.a' })).to.equal(false);
+ });
+
+ it('should return false for crap as input', () => {
+ expect(isValidIpv4({ ip: '' })).to.equal(false);
+ expect(isValidIpv4({ ip: ' ' })).to.equal(false);
+ expect(isValidIpv4({ ip: 'foo' })).to.equal(false);
+ expect(isValidIpv4({ ip: '-1' })).to.equal(false);
+ expect(isValidIpv4({ ip: '0' })).to.equal(false);
+ });
+ });
+});
diff --git a/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts b/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts
new file mode 100644
index 00000000..ffd5d804
--- /dev/null
+++ b/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts
@@ -0,0 +1,38 @@
+import _ from 'lodash';
+
+export { ipv4ToInt, ipv4ToIpv6, isValidIpv4 };
+
+function ipv4ToInt({ ip }: { ip: string }) {
+ if (!isValidIpv4({ ip })) {
+ return 0;
+ }
+
+ return ip
+ .trim()
+ .split('.')
+ .reduce((acc, part, index) => acc + Number(part) * Math.pow(256, 3 - index), 0);
+}
+
+function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: string; prefix?: string }) {
+ if (!isValidIpv4({ ip })) {
+ return '';
+ }
+
+ return (
+ prefix +
+ _.chain(ip)
+ .trim()
+ .split('.')
+ .map((part) => parseInt(part).toString(16).padStart(2, '0'))
+ .chunk(2)
+ .map((blocks) => blocks.join(''))
+ .join(':')
+ .value()
+ );
+}
+
+function isValidIpv4({ ip }: { ip: string }) {
+ const cleanIp = ip.trim();
+
+ return /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/.test(cleanIp);
+}
diff --git a/src/tools/ipv4-address-converter/ipv4-address-converter.vue b/src/tools/ipv4-address-converter/ipv4-address-converter.vue
new file mode 100644
index 00000000..7474608e
--- /dev/null
+++ b/src/tools/ipv4-address-converter/ipv4-address-converter.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/ipv6-ula-generator/index.ts b/src/tools/ipv6-ula-generator/index.ts
new file mode 100644
index 00000000..24efaeba
--- /dev/null
+++ b/src/tools/ipv6-ula-generator/index.ts
@@ -0,0 +1,12 @@
+import { BuildingFactory } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+ name: 'IPv6 ULA generator',
+ path: '/ipv6-ula-generator',
+ description: 'Generate your own local, non-routable IP addresses on your network according to RFC4193.',
+ keywords: ['ipv6', 'ula', 'generator', 'rfc4193', 'network', 'private'],
+ component: () => import('./ipv6-ula-generator.vue'),
+ icon: BuildingFactory,
+ createdAt: new Date('2023-04-09'),
+});
diff --git a/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue b/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue
new file mode 100644
index 00000000..ee74d4cc
--- /dev/null
+++ b/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue
@@ -0,0 +1,65 @@
+
+
+
+
+ This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed,
+ and the lower 40 bits to generate your random ULA.
+
+
+
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/json-minify/json-minify.vue b/src/tools/json-minify/json-minify.vue
index 92ab7d2e..d7e9d15c 100644
--- a/src/tools/json-minify/json-minify.vue
+++ b/src/tools/json-minify/json-minify.vue
@@ -1,57 +1,27 @@
-
-
-
-
-
-
+
-
-
diff --git a/src/tools/json-to-yaml-converter/index.ts b/src/tools/json-to-yaml-converter/index.ts
new file mode 100644
index 00000000..9db09d3e
--- /dev/null
+++ b/src/tools/json-to-yaml-converter/index.ts
@@ -0,0 +1,12 @@
+import { Braces } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+ name: 'JSON to YAML converter',
+ path: '/json-to-yaml-converter',
+ description: 'Simply convert JSON to YAML with this live online converter.',
+ keywords: ['yaml', 'to', 'json'],
+ component: () => import('./json-to-yaml.vue'),
+ icon: Braces,
+ createdAt: new Date('2023-04-10'),
+});
diff --git a/src/tools/json-to-yaml-converter/json-to-yaml.e2e.spec.ts b/src/tools/json-to-yaml-converter/json-to-yaml.e2e.spec.ts
new file mode 100644
index 00000000..bce095b6
--- /dev/null
+++ b/src/tools/json-to-yaml-converter/json-to-yaml.e2e.spec.ts
@@ -0,0 +1,19 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Tool - json to yaml', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/json-to-yaml-converter');
+ });
+
+ test('Has correct title', async ({ page }) => {
+ await expect(page).toHaveTitle('JSON to YAML converter - IT Tools');
+ });
+
+ test('json is parsed and output clean yaml', async ({ page }) => {
+ await page.getByTestId('input').fill('{"foo":"bar","list":["item",{"key":"value"}]}');
+
+ const generatedJson = await page.getByTestId('area-content').innerText();
+
+ expect(generatedJson.trim()).toEqual(`foo: bar\nlist:\n - item\n - key: value`.trim());
+ });
+});
diff --git a/src/tools/json-to-yaml-converter/json-to-yaml.vue b/src/tools/json-to-yaml-converter/json-to-yaml.vue
new file mode 100644
index 00000000..f25ef37b
--- /dev/null
+++ b/src/tools/json-to-yaml-converter/json-to-yaml.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
index d7528735..65a536ef 100644
--- a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
+++ b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
@@ -18,7 +18,7 @@
-
+
diff --git a/src/tools/mac-address-lookup/index.ts b/src/tools/mac-address-lookup/index.ts
index 574ee3f8..4108bc33 100644
--- a/src/tools/mac-address-lookup/index.ts
+++ b/src/tools/mac-address-lookup/index.ts
@@ -2,10 +2,10 @@ import { Devices } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
- name: 'MAC adrdress lookup',
- path: '/mac-adrdress-lookup',
+ name: 'MAC address lookup',
+ path: '/mac-address-lookup',
description: 'Find the vendor and manufacturer of a device by its MAC address.',
- keywords: ['mac', 'adrdress', 'lookup', 'vendor', 'parser', 'manufacturer'],
+ keywords: ['mac', 'address', 'lookup', 'vendor', 'parser', 'manufacturer'],
component: () => import('./mac-address-lookup.vue'),
icon: Devices,
createdAt: new Date('2023-04-06'),
diff --git a/src/tools/mac-address-lookup/mac-address-lookup.vue b/src/tools/mac-address-lookup/mac-address-lookup.vue
index 60e4836b..99b8342d 100644
--- a/src/tools/mac-address-lookup/mac-address-lookup.vue
+++ b/src/tools/mac-address-lookup/mac-address-lookup.vue
@@ -30,23 +30,15 @@
diff --git a/src/tools/otp-code-generator-and-validator/otp-code-generator.e2e.spec.ts b/src/tools/otp-code-generator-and-validator/otp-code-generator.e2e.spec.ts
new file mode 100644
index 00000000..6188f82f
--- /dev/null
+++ b/src/tools/otp-code-generator-and-validator/otp-code-generator.e2e.spec.ts
@@ -0,0 +1,48 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Tool - OTP code generator', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/otp-generator');
+ });
+
+ test('Has title', async ({ page }) => {
+ await expect(page).toHaveTitle('OTP code generator - IT Tools');
+ });
+
+ test('Secret hexa value is computed from provided secret', async ({ page }) => {
+ await page.getByPlaceholder('Paste your TOTP secret...').fill('ITTOOLS');
+
+ const secretInHex = await page.getByPlaceholder('Secret in hex will be displayed here').inputValue();
+
+ expect(secretInHex).toEqual('44e6e72e02');
+ });
+
+ test('OTP a generated from the provided secret', async ({ page }) => {
+ page.evaluate(() => {
+ Date.now = () => 1609477200000; //Jan 1, 2021
+ });
+
+ await page.getByPlaceholder('Paste your TOTP secret...').fill('ITTOOLS');
+
+ const previousOtp = await page.getByTestId('previous-otp').innerText();
+ const currentOtp = await page.getByTestId('current-otp').innerText();
+ const nextOtp = await page.getByTestId('next-otp').innerText();
+
+ expect(previousOtp.trim()).toEqual('028034');
+ expect(currentOtp.trim()).toEqual('162195');
+ expect(nextOtp.trim()).toEqual('452815');
+ });
+
+ test('You can generate a new random secret', async ({ page }) => {
+ const initialSecret = await page.getByPlaceholder('Paste your TOTP secret...').inputValue();
+ await page
+ .locator('div')
+ .filter({ hasText: /^Secret$/ })
+ .getByRole('button')
+ .click();
+
+ const newSecret = await page.getByPlaceholder('Paste your TOTP secret...').inputValue();
+
+ expect(newSecret).not.toEqual(initialSecret);
+ });
+});
diff --git a/src/tools/otp-code-generator-and-validator/token-display.vue b/src/tools/otp-code-generator-and-validator/token-display.vue
index 6ead65c8..ce11ccd5 100644
--- a/src/tools/otp-code-generator-and-validator/token-display.vue
+++ b/src/tools/otp-code-generator-and-validator/token-display.vue
@@ -8,13 +8,21 @@
- {{ tokens.previous }}
+ {{
+ tokens.previous
+ }}
{{ previousCopied ? 'Copied !' : 'Copy previous OTP' }}
-
+
{{ tokens.current }}
@@ -22,7 +30,9 @@
- {{ tokens.next }}
+ {{
+ tokens.next
+ }}
{{ nextCopied ? 'Copied !' : 'Copy next OTP' }}
diff --git a/src/tools/roman-numeral-converter/roman-numeral-converter.service.test.ts b/src/tools/roman-numeral-converter/roman-numeral-converter.service.test.ts
index 21a747cf..5ec9dd47 100644
--- a/src/tools/roman-numeral-converter/roman-numeral-converter.service.test.ts
+++ b/src/tools/roman-numeral-converter/roman-numeral-converter.service.test.ts
@@ -13,14 +13,17 @@ describe('roman-numeral-converter', () => {
expect(arabicToRoman(0.9)).toEqual('');
});
+ it('should convert numbers greater than 3999 to empty string', () => {
+ expect(arabicToRoman(3999.1)).toEqual('');
+ expect(arabicToRoman(4000)).toEqual('');
+ expect(arabicToRoman(10000)).toEqual('');
+ });
+
it('should convert floating points number to the lower integer in roman version', () => {
- expect(arabicToRoman(-100)).toEqual('');
- expect(arabicToRoman(-42)).toEqual('');
- expect(arabicToRoman(-26)).toEqual('');
- expect(arabicToRoman(-10)).toEqual('');
- expect(arabicToRoman(0)).toEqual('');
- expect(arabicToRoman(0.5)).toEqual('');
- expect(arabicToRoman(0.9)).toEqual('');
+ expect(arabicToRoman(1.1)).toEqual('I');
+ expect(arabicToRoman(1.9)).toEqual('I');
+ expect(arabicToRoman(17.6)).toEqual('XVII');
+ expect(arabicToRoman(29.999)).toEqual('XXIX');
});
it('should convert positive integers to roman numbers', () => {
@@ -67,7 +70,6 @@ describe('roman-numeral-converter', () => {
expect(arabicToRoman(999)).toEqual('CMXCIX');
expect(arabicToRoman(1000)).toEqual('M');
expect(arabicToRoman(2000)).toEqual('MM');
- expect(arabicToRoman(9000)).toEqual('MMMMMMMMM');
});
});
});
diff --git a/src/tools/roman-numeral-converter/roman-numeral-converter.service.ts b/src/tools/roman-numeral-converter/roman-numeral-converter.service.ts
index df2408a0..98afec67 100644
--- a/src/tools/roman-numeral-converter/roman-numeral-converter.service.ts
+++ b/src/tools/roman-numeral-converter/roman-numeral-converter.service.ts
@@ -1,5 +1,7 @@
+export const MIN_ARABIC_TO_ROMAN = 1;
+export const MAX_ARABIC_TO_ROMAN = 3999;
export function arabicToRoman(num: number) {
- if (num < 1) return '';
+ if (num < MIN_ARABIC_TO_ROMAN || num > MAX_ARABIC_TO_ROMAN) return '';
const lookup: { [key: string]: number } = {
M: 1000,
@@ -26,7 +28,16 @@ export function arabicToRoman(num: number) {
return roman;
}
+const ROMAN_NUMBER_REGEX = new RegExp(/^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/);
+
+export function isValidRomanNumber(romanNumber: string) {
+ return ROMAN_NUMBER_REGEX.test(romanNumber);
+}
+
export function romanToArabic(s: string) {
+ if (!isValidRomanNumber(s)) {
+ return null;
+ }
const map: { [key: string]: number } = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 };
return [...s].reduce((r, c, i, s) => (map[s[i + 1]] > map[c] ? r - map[c] : r + map[c]), 0);
}
diff --git a/src/tools/roman-numeral-converter/roman-numeral-converter.vue b/src/tools/roman-numeral-converter/roman-numeral-converter.vue
index c55380d0..609b46c4 100644
--- a/src/tools/roman-numeral-converter/roman-numeral-converter.vue
+++ b/src/tools/roman-numeral-converter/roman-numeral-converter.vue
@@ -2,21 +2,29 @@
-
+
+
+
{{ outputRoman }}
- Copy
+
+ Copy
+
-
+
+
+
{{ outputNumeral }}
- Copy
+
+ Copy
+
@@ -25,14 +33,41 @@
diff --git a/src/tools/token-generator/token-generator.e2e.spec.ts b/src/tools/token-generator/token-generator.e2e.spec.ts
new file mode 100644
index 00000000..905a81cc
--- /dev/null
+++ b/src/tools/token-generator/token-generator.e2e.spec.ts
@@ -0,0 +1,19 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Tool - Token generator', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/token-generator');
+ });
+
+ test('Has title', async ({ page }) => {
+ await expect(page).toHaveTitle('Token generator - IT Tools');
+ });
+
+ test('New token on refresh', async ({ page }) => {
+ const initialToken = await page.getByPlaceholder('The token...').inputValue();
+ await page.getByRole('button', { name: 'Refresh' }).click();
+ const newToken = await page.getByPlaceholder('The token...').inputValue();
+
+ expect(newToken).not.toEqual(initialToken);
+ });
+});
diff --git a/src/tools/tool.ts b/src/tools/tool.ts
index a5d157e9..3bf4e4cc 100644
--- a/src/tools/tool.ts
+++ b/src/tools/tool.ts
@@ -1,17 +1,10 @@
-import { config } from '@/config';
import { isAfter, subWeeks } from 'date-fns';
import type { Tool } from './tools.types';
type WithOptional = Omit & Partial>;
-export function defineTool(
- tool: WithOptional,
- { newTools }: { newTools: string[] } = { newTools: config.tools.newTools },
-) {
- const isInNewToolConfig = newTools.includes(tool.name);
- const isRecentTool = tool.createdAt ? isAfter(tool.createdAt, subWeeks(new Date(), 2)) : false;
-
- const isNew = isInNewToolConfig || isRecentTool;
+export function defineTool(tool: WithOptional) {
+ const isNew = tool.createdAt ? isAfter(tool.createdAt, subWeeks(new Date(), 2)) : false;
return {
isNew,
diff --git a/src/tools/yaml-to-json-converter/index.ts b/src/tools/yaml-to-json-converter/index.ts
new file mode 100644
index 00000000..724ecdb7
--- /dev/null
+++ b/src/tools/yaml-to-json-converter/index.ts
@@ -0,0 +1,12 @@
+import { AlignJustified } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+ name: 'YAML to JSON converter',
+ path: '/yaml-to-json-converter',
+ description: 'Simply convert YAML to JSON with this live online converter.',
+ keywords: ['yaml', 'to', 'json'],
+ component: () => import('./yaml-to-json.vue'),
+ icon: AlignJustified,
+ createdAt: new Date('2023-04-10'),
+});
diff --git a/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts b/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts
new file mode 100644
index 00000000..10db4495
--- /dev/null
+++ b/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts
@@ -0,0 +1,31 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Tool - Yaml to json', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/yaml-to-json-converter');
+ });
+
+ test('Has correct title', async ({ page }) => {
+ await expect(page).toHaveTitle('YAML to JSON converter - IT Tools');
+ });
+
+ test('Yaml is parsed and output clean json', async ({ page }) => {
+ await page.getByTestId('input').fill('foo: bar\nlist:\n - item\n - key: value');
+
+ const generatedJson = await page.getByTestId('area-content').innerText();
+
+ expect(generatedJson.trim()).toEqual(
+ `
+{
+ "foo": "bar",
+ "list": [
+ "item",
+ {
+ "key": "value"
+ }
+ ]
+}
+ `.trim(),
+ );
+ });
+});
diff --git a/src/tools/yaml-to-json-converter/yaml-to-json.vue b/src/tools/yaml-to-json-converter/yaml-to-json.vue
new file mode 100644
index 00000000..c066bdd5
--- /dev/null
+++ b/src/tools/yaml-to-json-converter/yaml-to-json.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
diff --git a/src/utils/macAddress.ts b/src/utils/macAddress.ts
new file mode 100644
index 00000000..ff6000cb
--- /dev/null
+++ b/src/utils/macAddress.ts
@@ -0,0 +1,16 @@
+import { useValidation } from '@/composable/validation';
+import type { Ref } from 'vue';
+
+function macAddressValidation(value: Ref) {
+ return useValidation({
+ source: value,
+ rules: [
+ {
+ message: 'Invalid MAC address',
+ validator: (value) => value.trim().match(/^([0-9A-Fa-f]{2}[:-]){2,5}([0-9A-Fa-f]{2})$/),
+ },
+ ],
+ });
+}
+
+export { macAddressValidation };
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 00000000..1c0d1e52
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,13 @@
+import { configDefaults, defineConfig } from 'vitest/config';
+import path from 'path';
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ test: {
+ exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'],
+ },
+});