diff --git a/.gitignore b/.gitignore index 6704566..2518495 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ dist # TernJS port file .tern-port + +/temp diff --git a/LICENSE b/LICENSE index 90374c9..346097d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 NodeSecure +Copyright (c) 2023 NodeSecure Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package.json b/package.json index 72fc811..dd98b83 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,48 @@ -{ - "name": "@nodesecure/authors", - "version": "1.0.2", - "description": "NodeSecure (npm) authors analysis package", - "exports": "./src/index.js", - "type": "module", - "types": "./src/index.d.ts", - "scripts": { - "lint": "cross-env eslint ./src", - "test-only": "cross-env esm-tape-runner 'test/**/*.js' | tap-monkey", - "test": "npm run lint && npm run test-only", - "coverage": "c8 -r html npm test" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/NodeSecure/authors.git" - }, - "keywords": [ - "NodeSecure", - "authors", - "author", - "npm" - ], - "files": [ - "src" - ], - "author": "GENTILHOMME Thomas ", - "license": "MIT", - "bugs": { - "url": "https://github.com/NodeSecure/authors/issues" - }, - "homepage": "https://github.com/NodeSecure/authors#readme", - "devDependencies": { - "@nodesecure/eslint-config": "^1.6.0", - "@nodesecure/scanner": "^3.8.2", - "@npm/types": "^1.0.2", - "@small-tech/esm-tape-runner": "^2.0.0", - "@small-tech/tap-monkey": "^1.4.0", - "c8": "^7.12.0", - "cross-env": "^7.0.3", - "tape": "^5.6.3" - }, - "dependencies": { - "@myunisoft/httpie": "^1.10.0", - "@nodesecure/domain-check": "^1.0.1" - } -} +{ + "name": "@nodesecure/authors", + "version": "1.0.2", + "description": "NodeSecure (npm) authors analysis package", + "exports": "./dist/index.js", + "type": "module", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "lint": "eslint src/*.ts", + "test-only": "node --test test/", + "test": "npm run lint && npm run test-only", + "coverage": "c8 -r html npm test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NodeSecure/authors.git" + }, + "keywords": [ + "NodeSecure", + "authors", + "author", + "npm" + ], + "files": [ + "index.js", + "index.d.ts", + "src" + ], + "author": "GENTILHOMME Thomas ", + "license": "MIT", + "bugs": { + "url": "https://github.com/NodeSecure/authors/issues" + }, + "homepage": "https://github.com/NodeSecure/authors#readme", + "devDependencies": { + "@nodesecure/eslint-config": "^1.8.0", + "@nodesecure/scanner": "^4.0.0", + "@types/node": "^20.5.9", + "c8": "^8.0.1", + "typescript": "^5.2.2" + }, + "dependencies": { + "@myunisoft/httpie": "^2.0.1", + "@npm/types": "^1.0.2", + "@openally/result": "^1.2.0" + } +} diff --git a/src/domains.ts b/src/domains.ts new file mode 100644 index 0000000..60cb00e --- /dev/null +++ b/src/domains.ts @@ -0,0 +1,66 @@ +// Import Node.js Dependencies +import dns from "node:dns/promises"; + +// Import Third-party Dependencies +import { Some, None, Option } from "@openally/result"; + +// Import Internal Dependencies +import { whois } from "./utils/whois.js"; + +export type DnsNotFound = { + state: "not found"; +} + +export type DnsResolved = { state: "not found" } | { + state: "expired" | "active"; + expirationDate: string | null; + mxRecords: string[]; +} + +export type ResolveResult = DnsNotFound | DnsResolved; + +export class DomainResolver { + #cache: Map = new Map(); + + async resolve( + domain: string + ): Promise { + if (this.#cache.has(domain)) { + return this.#cache.get(domain)!; + } + + const mxRecords = await safeResolveMx(domain); + if (mxRecords.none) { + return { + state: "not found" + }; + } + + const expirationDate = await whois(domain); + const result: DnsResolved = { + state: expirationDate === null ? "expired" : "active", + expirationDate, + mxRecords: mxRecords.safeUnwrap() + }; + this.#cache.set(domain, result); + + return result; + } + + clear() { + this.#cache.clear(); + } +} + +async function safeResolveMx(domain: string): Promise> { + try { + const mxRecords = await dns.resolveMx(domain); + + return Some( + mxRecords.map(({ exchange }) => exchange) + ); + } + catch { + return None; + } +} diff --git a/src/helper.js b/src/helper.js deleted file mode 100644 index 8d0706e..0000000 --- a/src/helper.js +++ /dev/null @@ -1,9 +0,0 @@ -const domainsInformations = {}; - -export function storeDomainExpirationInMemory({ domain, expirationDate }) { - domainsInformations[domain] = expirationDate; -} - -export function getDomainExpirationFromMemory(domain) { - return domainsInformations[domain] ?? undefined; -} diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index 2bbee3c..0000000 --- a/src/index.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Import Third-party Dependencies -import { Scanner } from "@nodesecure/scanner"; - -export function extractAllAuthors(library: Scanner.Payload, opts: options): Promise - -export interface options { - flags: extractedAuthor[], - domainInformations: boolean, -} - -export interface extractionResult { - authors: author[], - flaggedAuthors: extractedAuthor[], -} - -export interface author { - name?: string; - email?: string; - url?: string; - packages: { - homepage: string, - spec: string, - version: string, - at?: string, - }[], - domain?: { - expirationDate?: string, - mxRecords?: unknown[], - } -} -export interface extractedAuthor { - name: string, - email: string, -} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 7f9a300..0000000 --- a/src/index.js +++ /dev/null @@ -1,148 +0,0 @@ -// Import Internal Dependencies -import { useLevenshtein } from "./levenshtein.js"; -import { getDomainExpirationFromMemory, storeDomainExpirationInMemory } from "./helper.js"; -import { whois, resolveMxRecords } from "@nodesecure/domain-check"; - -function splitAuthorNameEmail(author) { - const indexStartEmail = author.name.search(/[<]/g); - const indexEndEmail = author.name.search(/[>]/g); - - if (indexStartEmail === -1 && indexEndEmail === -1) { - return { - name: author.name, - email: "email" in author ? author.email : "" - }; - } - - return { - name: author.substring(0, indexStartEmail).trim(), - email: author.substring(indexStartEmail, indexEndEmail).trim() - }; -} - -export async function extractAllAuthors(library, opts = { flags: [], domainInformations: false }) { - if (!("dependencies" in library)) { - return []; - } - - const authors = []; - - for (let index = 0; index < Object.values(library.dependencies).length; index++) { - const currPackage = { - packageName: Object.keys(library.dependencies)[index], - ...Object.values(library.dependencies)[index] - }; - - const { author, maintainers, publishers } = currPackage.metadata; - const packageMeta = { - homepage: currPackage.metadata.homepage || "", - spec: currPackage.packageName, - versions: currPackage.metadata.lastVersion - }; - - const authorsFound = formatAuthors({ author, maintainers, publishers }); - - for (const author of authorsFound) { - if (author === undefined) { - continue; - } - authors.push({ - name: author.name, - email: author.email, - packages: [{ - ...packageMeta, - ...(author?.at ? { at: author.at } : {}) - }] - }); - } - } - if (authors.length === 0) { - return []; - } - - const authorsFlagged = findFlaggedAuthors(useLevenshtein(authors), opts.flags); - - if (opts.domainInformations === true) { - return addDomainInformations(authors); - } - - return { - authorsFlagged, - authors - }; -} - -async function addDomainInformations(authors) { - for (const author of authors) { - if (author.email === "") { - continue; - } - const domain = author.email.split("@")[1]; - const mxRecords = await resolveMxRecords(domain); - - if (getDomainExpirationFromMemory(domain) !== undefined) { - author.domain = { - expirationDate: getDomainExpirationFromMemory(domain), - mxRecords - }; - - continue; - } - - const expirationDate = await whois(domain); - storeDomainExpirationInMemory({ domain, expirationDate }); - author.domain = { - expirationDate, - mxRecords - }; - } - - return authors; -} - - -function findFlaggedAuthors(authors, flags) { - const res = []; - for (const author of authors) { - for (const flag of flags) { - if (flag.name === author.name || flag.email === author.email) { - res.push({ name: author.name, email: author.email }); - } - } - } - - return res; -} - - -function iterateOver(iterable, arrayAuthors) { - for (const contributor of iterable) { - if (arrayAuthors.find((el) => el.name === contributor.name)) { - const index = arrayAuthors.findIndex((el) => el.name === contributor.name); - - if (arrayAuthors[index].email && arrayAuthors[index].name) { - if (contributor.at && contributor.version) { - arrayAuthors[index].at = contributor.at; - arrayAuthors[index].version = contributor.version; - } - continue; - } - arrayAuthors[index] = contributor; - } - else { - arrayAuthors.push(contributor); - } - } -} - -function formatAuthors({ author, maintainers, publishers }) { - const authors = []; - - if (author?.name !== undefined) { - authors.push(splitAuthorNameEmail(author)); - } - iterateOver(maintainers, authors); - iterateOver(publishers, authors); - - return useLevenshtein(authors); -} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..73c2bd5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,122 @@ +// Import Third-party Dependencies +import { Scanner } from "@nodesecure/scanner"; + +// Import Internal Dependencies +// import { DomainResolver } from "./domains.js"; +// import { useLevenshtein } from "./levenshtein.js"; +import * as utils from "./utils.js"; + +export interface ExtractedAuthor { + name: string; + email?: string; +} + +export interface ExtractOptions { + flaggedAuthors: ExtractedAuthor[], + domainInformations: boolean, +} + +export interface ExtractResult { + authors: RegistryAuthor[], + flaggedAuthors: ExtractedAuthor[], +} + +export interface RegistryAuthor { + name: string; + email?: string; + url?: string; + packages: { + homepage: string, + spec: string, + version?: string, + at?: string, + }[], + domain?: { + expirationDate?: string, + mxRecords?: unknown[], + } +} + +export async function extractAllAuthors( + library: Scanner.Payload, + opts: ExtractOptions = { flaggedAuthors: [], domainInformations: false } +): Promise { + if (!("dependencies" in library)) { + throw new Error("You must provide a list of dependencies"); + } + + const authors: RegistryAuthor[] = []; + for (const [packageName, packageDependency] of Object.entries(library.dependencies)) { + const { author, maintainers, publishers } = packageDependency.metadata; + + const authorsFound = utils.formatAuthors({ + author, maintainers, publishers + }); + const packageMeta = { + homepage: packageDependency.metadata.homepage ?? "", + spec: packageName, + versions: packageDependency.metadata.lastVersion + }; + + for (const author of authorsFound) { + authors.push({ + name: author.name, + email: author.email, + packages: [{ + ...packageMeta, + ...(author?.at ? { at: author.at } : {}) + }] + }); + } + } + console.log(JSON.stringify(authors, null, 2)); + + // if (authors.length === 0) { + // return { + // flaggedAuthors: [], + // authors: [] + // }; + // } + + // const flaggedAuthors = Array.from(findFlaggedAuthors( + // useLevenshtein(authors), + // opts.flaggedAuthors + // )); + // if (opts.domainInformations) { + // await addDomainInformations(authors); + // } + + // return { + // flaggedAuthors, + // authors + // }; +} + +// async function addDomainInformations(authors) { +// const domainResolver = new DomainResolver(); +// for (const author of authors) { +// if (author.email === "") { +// continue; +// } +// const domain = author.email.split("@")[1]; + +// if (domain) { +// author.domain = await domainResolver.resolve(domain); +// } +// } + +// return authors; +// } + +// function* findFlaggedAuthors( +// authors: RegistryAuthor[], +// flaggedAuthors: ExtractedAuthor[] = [] +// ): IterableIterator<{ name: string, email: string }> { +// for (const author of authors) { +// for (const flaggedAuthor of flaggedAuthors) { +// if (flaggedAuthor.name === author.name || flaggedAuthor.email === author.email) { +// yield { name: author.name, email: author.email }; +// } +// } +// } +// } diff --git a/src/levenshtein.js b/src/levenshtein.ts similarity index 67% rename from src/levenshtein.js rename to src/levenshtein.ts index 4cb9a61..cc977d5 100644 --- a/src/levenshtein.js +++ b/src/levenshtein.ts @@ -1,7 +1,9 @@ -// Constant -const KMaxLevenshtein = 2; +import { FormattedAuthor } from "./utils"; -export function separateWord(word) { +// CONSTANTS +const kMaxLevenshteinDistance = 2; + +export function separateWord(word: string): string | string[] { const separators = [",", " ", "."]; let separatorFound = ""; let indexSeparator = -1; @@ -17,9 +19,13 @@ export function separateWord(word) { return indexSeparator === -1 ? word : word.split(separatorFound); } -export function isSimilar(firstWord, secondWord, isWordSeparated = false) { +export function isSimilar( + firstWord: string | undefined, + secondWord: string | undefined, + isWordSeparated = false +) { if (!firstWord || !secondWord) { - return KMaxLevenshtein; + return kMaxLevenshteinDistance; } const word1 = firstWord.toLowerCase(); const word2 = secondWord.toLowerCase(); @@ -29,9 +35,11 @@ export function isSimilar(firstWord, secondWord, isWordSeparated = false) { const word2Splitted = separateWord(word2); const firstWord = isSimilar( + // @ts-ignore word1Splitted instanceof Array ? word1Splitted[0] : null, word2Splitted instanceof Array ? word2Splitted[0] : null); const secondWord = isSimilar( + // @ts-ignore word1Splitted instanceof Array ? word1Splitted[1] : null, word2Splitted instanceof Array ? word2Splitted[1] : null ); @@ -64,22 +72,26 @@ export function isSimilar(firstWord, secondWord, isWordSeparated = false) { return track[word2.length][word1.length]; } -export function useLevenshtein(authors) { +export function useLevenshtein( + authors: FormattedAuthor[] +): FormattedAuthor[] { const authorsResponse = [authors[0]]; iterationAuthor: - for (let index = 1; index < authors.length; index++) { - const currAuthor = authors[index]; - + for (const currAuthor of authors) { for (const author of authorsResponse) { - if (isSimilar(author.email, currAuthor.email) < KMaxLevenshtein - || isSimilar(author.name, currAuthor.name, true) < KMaxLevenshtein) { - author.email = author.email.length > currAuthor.email.length ? author.email : currAuthor.email; - author.name = author.name.length > currAuthor.name.length ? author.name : currAuthor.name; + const hasSimilarEmail = isSimilar(author.email, currAuthor.email) < kMaxLevenshteinDistance; + const hasSimilarName = isSimilar(author.name, currAuthor.name, true) < kMaxLevenshteinDistance; - if ("packages" in currAuthor) { - author.packages.push(...currAuthor.packages); + if (hasSimilarEmail || hasSimilarName) { + if (author.email && currAuthor.email) { + author.email = author.email.length > currAuthor.email.length ? author.email : currAuthor.email; } + author.name = author.name.length > currAuthor.name.length ? author.name : currAuthor.name; + + // if ("packages" in currAuthor) { + // author.packages.push(...currAuthor.packages); + // } continue iterationAuthor; } } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..5a0768e --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,86 @@ +// // Import Internal Dependencies +import { useLevenshtein } from "./levenshtein.js"; + +// Import Third-party Dependencies +import { Maintainer } from "@npm/types"; +import { Scanner } from "@nodesecure/scanner"; + +type MaintainerBis = { name: string, email: string }; + +export interface FormatAuthorsOptions { + author: Maintainer; + maintainers: MaintainerBis[]; + publishers: Scanner.Publisher[]; +} + +export interface FormattedAuthor { + name: string; + email?: string; + version?: string; + at?: string; +} + +export function formatAuthors( + options: FormatAuthorsOptions +) { + const { author, maintainers, publishers } = options; + + const authors: FormattedAuthor[] = []; + const extractedAuthor = splitAuthorNameEmail(author); + if (extractedAuthor !== null) { + authors.push(extractedAuthor); + } + + iterateOver(maintainers, authors); + iterateOver(publishers, authors); + + return useLevenshtein(authors); +} + +export function iterateOver( + iterable: (MaintainerBis | Scanner.Publisher)[], + arrayAuthors: FormattedAuthor[] +) { + for (const contributor of iterable) { + const index = arrayAuthors.findIndex((el) => el.name === contributor.name); + + if (index === -1) { + arrayAuthors.push(contributor); + continue; + } + + const currAuthor = arrayAuthors[index]; + if ( + currAuthor.email && currAuthor.name && + ("at" in contributor && "version" in contributor) + ) { + currAuthor.at = contributor.at; + currAuthor.version = contributor.version; + } + else { + arrayAuthors[index] = contributor; + } + } +} + +export function splitAuthorNameEmail( + author: Maintainer +): FormattedAuthor | null { + if (typeof author === "string") { + const indexStartEmail = author.search(/[<]/g); + const indexEndEmail = author.search(/[>]/g); + + if (indexStartEmail === -1 && indexEndEmail === -1) { + return { name: author }; + } + + return { + name: author.slice(0, indexStartEmail).trim(), + email: author.slice(indexStartEmail, indexEndEmail).trim() + }; + } + + return "name" in author ? + { name: author.name!, email: author.email! } : + null; +} diff --git a/src/utils/whois.ts b/src/utils/whois.ts new file mode 100644 index 0000000..0586c2c --- /dev/null +++ b/src/utils/whois.ts @@ -0,0 +1,48 @@ +// Node.js Dependencies +import net from "node:net"; +import streamConsumers from "node:stream/consumers"; + +// CONSTANTS +const kDefaultSocketServer = "whois.iana.org"; + +function* lazyParseIanaWhoisResponse( + rawResponseStr: string +): IterableIterator<[string, string]> { + const lines = rawResponseStr.split(/\r?\n/); + + for (const line of lines) { + const safeLine = line.trim(); + + if (safeLine !== "" && safeLine.charAt(0) !== "%" && safeLine.includes(":")) { + const [key, value] = safeLine.split(":"); + + yield [key.trimStart(), value.trimStart()]; + } + } +} + +export async function whois( + domain: string, + server = kDefaultSocketServer +): Promise { + const client = new net.Socket(); + client.setTimeout(1_000); + client.connect(43, server, () => client.write(`${domain}\r\n`)); + + try { + const rawResponseStr = await streamConsumers.text(client); + const response = Object.fromEntries( + lazyParseIanaWhoisResponse(rawResponseStr) + ); + + if ("refer" in response && response.refer !== server) { + return whois(domain, response.refer); + } + + return response?.["Registry Expiry Date"] ?? null; + } + finally { + client.destroy(); + } +} + diff --git a/temp.js b/temp.js deleted file mode 100644 index 39d5ae1..0000000 --- a/temp.js +++ /dev/null @@ -1,11 +0,0 @@ -import { from } from "@nodesecure/scanner"; -import { writeFileSync } from "fs"; - -async function main() { - const result = await from("express", { - maxDepth: 10 - }); - - writeFileSync("./test/nsecure-result.json", JSON.stringify(result, null, 2)); -} -main().catch(console.error); diff --git a/test/extractAllAuthors.spec.js b/test/extractAllAuthors.spec.js new file mode 100644 index 0000000..e6535c4 --- /dev/null +++ b/test/extractAllAuthors.spec.js @@ -0,0 +1,51 @@ +// Import Node.js Dependencies +import { test } from "node:test"; +import assert from "node:assert"; +import { readFileSync } from "node:fs"; + +// Import Internal Dependencies +import { extractAllAuthors } from "../index.js"; + +// CONSTANTS +const kFixtureNodeSecurePayload = JSON.parse( + readFileSync( + new URL("./fixtures/nsecure-result.json", import.meta.url) + ) +); + +test("All authors from library without flags involved", async() => { + const res = await extractAllAuthors(kFixtureNodeSecurePayload, { + flags: [], + domainInformations: true + }); + + assert.ok(res.authors.length > 0, "There should be authors in the response"); +}); + +test("test authors from library with flag", async() => { + const flaggedAuthors = [ + { name: "kesla", email: "david.bjorklund@gmail.com" } + ]; + const res = await extractAllAuthors(kFixtureNodeSecurePayload, { + flaggedAuthors + }); + + assert.deepEqual(res.authorsFlagged, flaggedAuthors); + assert.deepEqual(res.authors.slice(1, 2), + [ + { + name: "kesla", + email: "david.bjorklund@gmail.com", + packages: + [ + { + homepage: "https://github.com/jshttp/etag#readme", + spec: "etag", + versions: "1.8.1", + at: "2014-05-18T11:14:58.281Z" + } + ] + } + ] + ); +}); diff --git a/test/nsecure-result.json b/test/fixtures/nsecure-result.json similarity index 100% rename from test/nsecure-result.json rename to test/fixtures/nsecure-result.json diff --git a/test/getDomainInformations.spec.js b/test/getDomainInformations.spec.js new file mode 100644 index 0000000..024e9e6 --- /dev/null +++ b/test/getDomainInformations.spec.js @@ -0,0 +1,30 @@ +// Import Node.js Dependencies +import { describe, it } from "node:test"; +import assert from "node:assert"; + +// Import Internal Dependencies +import { getDomainInformations } from "../src/domains.js"; + +function isValidDate(dateString) { + return dateString.match(/^\d{4}-\d{2}-\d{2}/) !== null; +} + +describe("getDomainInformations", () => { + it("should fetch/get domain informations for google.com", async() => { + const domain = "google.com"; + + const infos = await getDomainInformations(domain); + + assert.strict(infos.state, "active"); + assert.ok(isValidDate(infos.expirationDate)); + assert.ok(Array.isArray(infos.mxRecords)); + assert.strictEqual(infos.mxRecords[0], "smtp.google.com"); + }); + + it("should return state equal 'not found' for an unknown domain", async() => { + const domain = "zbvoepokxfkxlzkejnckekjx.ru"; + + const infos = await getDomainInformations(domain); + assert.strict(infos.state, "not found"); + }); +}); diff --git a/test/index.js b/test/index.js deleted file mode 100644 index 86a6fe4..0000000 --- a/test/index.js +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable max-len */ -// Import Node.js Dependencies -import { readFile } from "fs/promises"; - -// Import Third-party Dependencies -import test from "tape"; - -// Import Internal Dependencies -import { extractAllAuthors } from "../src/index.js"; - -const nsecureTestFile = JSON.parse( - await readFile( - new URL("./nsecure-result.json", import.meta.url) - ) -); - -test("All authors from library without flags involved", async(tape) => { - const packageTest = nsecureTestFile; - - const res = await extractAllAuthors(packageTest, { flags: [], domainInformations: true }); - - tape.isNot(res.authors.length, 0, "There should be authors in the response"); - tape.end(); -}); - -test("test authors from library with flag", async(tape) => { - const packageTest = nsecureTestFile; - const flaggedAuthors = [ - { name: "kesla", email: "david.bjorklund@gmail.com" } - ]; - const res = await extractAllAuthors(packageTest, { - flags: flaggedAuthors - }); - - tape.deepEqual(res.authorsFlagged, flaggedAuthors); - tape.deepEqual(res.authors.slice(1, 2), - [ - { - name: "kesla", - email: "david.bjorklund@gmail.com", - packages: - [ - { - homepage: "https://github.com/jshttp/etag#readme", - spec: "etag", - versions: "1.8.1", - at: "2014-05-18T11:14:58.281Z" - } - ] - } - ] - ); - tape.end(); -}); diff --git a/test/levenshtein.js b/test/levenshtein.spec.js similarity index 74% rename from test/levenshtein.js rename to test/levenshtein.spec.js index 444de57..5823178 100644 --- a/test/levenshtein.js +++ b/test/levenshtein.spec.js @@ -1,75 +1,71 @@ -// Import Third-party Dependencies -import test from "tape"; +// Import Node.js Dependencies +import { test } from "node:test"; +import assert from "node:assert"; // Import Internal Dependencies import { isSimilar, separateWord, useLevenshtein } from "../src/levenshtein.js"; -test("check separateWord", (tape) => { +test("check separateWord", () => { const author = "Vincent Dhennin"; const author2 = "poppins,virk"; const data = separateWord(author); - tape.deepEqual(data, [ + assert.deepEqual(data, [ "Vincent", "Dhennin" ]); const data2 = separateWord(author2); - tape.deepEqual(data2, [ + assert.deepEqual(data2, [ "poppins", "virk" ]); - tape.end(); }); -test("check isSimilar about email", (tape) => { +test("check isSimilar about email", () => { const author1 = "shtylman@gmail.com"; const author2 = "doug@somethingdoug.com"; const similar = isSimilar(author1, author2); - tape.equal(similar, 16); + assert.equal(similar, 16); const author3 = "shtylman@gmail.com"; const author4 = "shtylman@gmail.com"; const similar2 = isSimilar(author3, author4); - tape.equal(similar2, 0); - tape.end(); + assert.equal(similar2, 0); }); -test("check isSimilar about names", (tape) => { +test("check isSimilar about names", () => { const author1 = "Roman Shtylman"; const author2 = "doug@somethingdoug.com"; const similar = isSimilar(author1, author2); - tape.equal(similar, 19); - tape.end(); + assert.equal(similar, 19); }); -test("useLevenshtein on authors list", (tape) => { +test("useLevenshtein on authors list", () => { const authors = [ { name: "Roman Shtylman", email: "shtylman@gmail.com" }, { name: "dougwilson", email: "doug@somethingdoug.com" }, { name: "shtylman", email: "shtylman@gmail.com" } ]; const authorsFormatted = useLevenshtein(authors); - tape.deepEqual(authorsFormatted, [ + assert.deepEqual(authorsFormatted, [ { name: "Roman Shtylman", email: "shtylman@gmail.com" }, { name: "dougwilson", email: "doug@somethingdoug.com" } ]); - tape.end(); }); -test("useLevenshtein on authors list containing duplicate email", (tape) => { +test("useLevenshtein on authors list containing duplicate email", () => { const authors = [ { name: "Roman Shtylman", email: "shtylman@gmail.com" }, { name: "dougwilson", email: "doug@somethingdoug.com" }, { name: "shtylman", email: "shtylman@gmail.com" } ]; const authorsFormatted = useLevenshtein(authors); - tape.deepEqual(authorsFormatted, [ + assert.deepEqual(authorsFormatted, [ { name: "Roman Shtylman", email: "shtylman@gmail.com" }, { name: "dougwilson", email: "doug@somethingdoug.com" } ]); - tape.end(); }); diff --git a/test/whois.spec.js b/test/whois.spec.js new file mode 100644 index 0000000..471d7ba --- /dev/null +++ b/test/whois.spec.js @@ -0,0 +1,23 @@ +// Import Node.js Dependencies +import { describe, it } from "node:test"; +import assert from "node:assert"; + +// Import Internal Dependencies +import { whois } from "../src/whois.js"; + +function isValidDate(dateString) { + return dateString.match(/^\d{4}-\d{2}-\d{2}/) !== null; +} + +describe("whois", () => { + it("should resolve domain expiration date for 'google.com'", async() => { + // Given + const domain = "google.com"; + + // When + const domainExpirationDate = await whois(domain); + + // Then + assert.ok(isValidDate(domainExpirationDate)); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3265b53 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "declaration": true, + "strictNullChecks": true, + "target": "ES2022", + "outDir": "dist", + "module": "ES2020", + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + "skipDefaultLibCheck": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true, + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src", "src/schema/**/*.json"], + "exclude": ["node_modules", "dist"] +}