diff --git a/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/licenseFile.ts b/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/licenseFile.ts index 929bdb64..55b3a73f 100644 --- a/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/licenseFile.ts +++ b/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/licenseFile.ts @@ -5,6 +5,10 @@ import logger from "../../utils/console.utils"; import { readFile } from "../../utils/file.utils"; import { extname } from "path"; +// This file specifically handles cases where we're able to find +// a license file on disk that is a part of the package but it's +// not referenced in the package.json file. + // A 'best guess' for file extensions that are not license files // but that may have the same name as a license file export const extensionDenyList = [".js", ".ts", ".sh", ".ps1"]; diff --git a/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/packageJsonLicense.ts b/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/packageJsonLicense.ts index 4243d506..6ac1e0d3 100644 --- a/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/packageJsonLicense.ts +++ b/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/packageJsonLicense.ts @@ -1,18 +1,49 @@ import { doesFileExist, readFile } from "../../utils/file.utils"; import logger from "../../utils/console.utils"; import { join } from "path"; -import { Resolution } from "../resolveLicenseContent"; +import { Resolution } from "./index"; +import { PackageJson, PackageJsonLicense } from "../../utils/packageJson.utils"; + +// This file specifically handles cases where the package.json links +// to a license file that is on disk and is a part of the package. +// +// If it instead finds a URL to a license file, it will return that URL as-is. export const packageJsonLicense: Resolution = async inputs => { const { packageJson, directory } = inputs; + const { license, licenses } = packageJson; + + if (typeof license === "string") { + return parseStringLicense(license, directory); + } + + if (Array.isArray(license)) { + return parseArrayLicense(license, packageJson); + } + + if (typeof license === "object") { + return parseObjectLicense(license); + } - const spdxExpression = packageJson.license; + if (Array.isArray(licenses)) { + return parseArrayLicense(licenses, packageJson); + } + + return null; +}; +const parseStringLicense = async (spdxExpression: string, directory: string) => { if (!spdxExpression) { return null; } - if (!spdxExpression.toLowerCase().startsWith("see license in ")) { + const lowerCaseExpression = spdxExpression.toLowerCase(); + + if (lowerCaseExpression.startsWith("http") || lowerCaseExpression.startsWith("www")) { + return spdxExpression; + } + + if (!lowerCaseExpression.startsWith("see license in ")) { return null; } @@ -44,3 +75,33 @@ const readLicenseFromDisk = async (dir: string, path: string): Promise { + if (license.length === 0) { + return null; + } + + if (license.length === 1) { + return parseObjectLicense(license[0]); + } + + const warningLines = [ + `The license key for ${packageJson.name}@${packageJson.version} contains multiple licenses"`, + "We suggest you determine which license applies to your project and replace the license content", + `for ${packageJson.name}@${packageJson.version} using a generate-license-file config file.`, + "See: https://generate-license-file.js.org/docs/cli/config-file for more information.", + "", // Empty line for spacing + ]; + + logger.warn(warningLines.join("\n")); + + return parseObjectLicense(license[0]); +}; + +const parseObjectLicense = (license: PackageJsonLicense) => { + if (!license.url) { + return null; + } + + return license.url; +}; diff --git a/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/spdxExpression.ts b/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/spdxExpression.ts index 2ad424c2..a291cd88 100644 --- a/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/spdxExpression.ts +++ b/src/packages/generate-license-file/src/lib/internal/resolveLicenseContent/spdxExpression.ts @@ -1,12 +1,66 @@ -import { Resolution } from "../resolveLicenseContent"; +import { Resolution } from "./index"; import logger from "../../utils/console.utils"; +import { PackageJson, PackageJsonLicense } from "../../utils/packageJson.utils"; + +// This file specifically handles cases where the package.json contains an SPDX license expression. export const spdxExpression: Resolution = async input => { const { packageJson } = input; - const expression = packageJson.license; + const { license, licenses } = packageJson; + + if (typeof license === "string") { + return handleStringLicense(license, packageJson); + } + + if (Array.isArray(license)) { + return handleArrayLicense(license, packageJson); + } + + if (typeof license === "object") { + return handleObjectLicense(license, packageJson); + } + + if (Array.isArray(licenses)) { + return handleArrayLicense(licenses, packageJson); + } + + return null; +}; + +const handleArrayLicense = (licenses: PackageJsonLicense[], packageJson: PackageJson) => { + if (licenses.length === 0) { + return null; + } + + if (licenses.length === 1) { + return handleObjectLicense(licenses[0], packageJson); + } + + const warningLines = [ + `The license field for ${packageJson.name}@${packageJson.version} contains multiple licenses:`, + JSON.stringify(licenses), + "We suggest you determine which license applies to your project and replace the license content", + `for ${packageJson.name}@${packageJson.version} using a generate-license-file config file.`, + "See: https://generate-license-file.js.org/docs/cli/config-file for more information.", + "", // Empty line for spacing + ]; + + logger.warn(warningLines.join("\n")); + + return handleObjectLicense(licenses[0], packageJson); +}; + +const handleObjectLicense = (packageJsonLicence: PackageJsonLicense, packageJson: PackageJson) => { + if (!packageJsonLicence.type) { + return null; + } + + return handleStringLicense(packageJsonLicence.type, packageJson); +}; - if (!expression) { +const handleStringLicense = (expression: string, packageJson: PackageJson) => { + if (expression.length === 0) { return null; } diff --git a/src/packages/generate-license-file/src/lib/utils/packageJson.utils.ts b/src/packages/generate-license-file/src/lib/utils/packageJson.utils.ts index 6578a121..903f5009 100644 --- a/src/packages/generate-license-file/src/lib/utils/packageJson.utils.ts +++ b/src/packages/generate-license-file/src/lib/utils/packageJson.utils.ts @@ -3,7 +3,13 @@ import { doesFileExist, readFile } from "./file.utils"; export interface PackageJson { name?: string; version?: string; - license?: string; + license?: string | PackageJsonLicense | PackageJsonLicense[]; + licenses?: PackageJsonLicense[]; +} + +export interface PackageJsonLicense { + type?: string; + url?: string; } export const readPackageJson = async (pathToPackageJson: string): Promise => { @@ -14,6 +20,5 @@ export const readPackageJson = async (pathToPackageJson: string): Promise { const mockedDoesFileExist = jest.mocked(doesFileExist); const mockedReadFile = jest.mocked(readFile); + const mockedWarn = jest.mocked(logger.warn); beforeEach(jest.resetAllMocks); afterAll(jest.restoreAllMocks); - it("should return null if the package.json does not have a license field", async () => { + it("should return null if the package.json does not have a license or a licenses field", async () => { const inputs: ResolutionInputs = { packageJson: {}, directory: "/some/directory", @@ -25,7 +27,7 @@ describe("packageJsonLicense", () => { expect(result).toBeNull(); }); - it("should return null if the license field does not start with 'see license in '", async () => { + it("should return null if the license field is an SPDX expression", async () => { const inputs: ResolutionInputs = { packageJson: { license: "MIT", @@ -38,125 +40,425 @@ describe("packageJsonLicense", () => { expect(result).toBeNull(); }); - it("should try to read the license file from the directory specified in the inputs", async () => { + it("should return null if the license field is an empty string", async () => { const inputs: ResolutionInputs = { packageJson: { - license: "SEE LICENSE IN license.txt", + license: "", }, directory: "/some/directory", }; - const _ = await packageJsonLicense(inputs); + const result = await packageJsonLicense(inputs); - const expectedPath = join("/some/directory", "license.txt"); - expect(mockedDoesFileExist).toHaveBeenCalledWith(expectedPath); + expect(result).toBeNull(); }); - it("should return null if the license file does not exist", async () => { - const expectedPath = join("/some/directory", "license.txt"); + it.each(["http://some.url", "www.some.url"])( + "should return the license URL if the license field is a URL: %s", + async url => { + const inputs: ResolutionInputs = { + packageJson: { + license: url, + }, + directory: "/some/directory", + }; - when(mockedDoesFileExist).calledWith(expectedPath).mockResolvedValue(false); + const result = await packageJsonLicense(inputs); - const inputs: ResolutionInputs = { - packageJson: { - license: "SEE LICENSE IN license.txt", - }, - directory: "/some/directory", - }; + expect(result).toEqual(url); + }, + ); - const result = await packageJsonLicense(inputs); + describe("when the license field is a 'see license in' expression", () => { + it("should try to read the license file from the directory specified in the inputs", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: "SEE LICENSE IN license.txt", + }, + directory: "/some/directory", + }; - expect(result).toBeNull(); + const _ = await packageJsonLicense(inputs); - expect(mockedDoesFileExist).toHaveBeenCalledWith(expectedPath); - }); + const expectedPath = join("/some/directory", "license.txt"); + expect(mockedDoesFileExist).toHaveBeenCalledWith(expectedPath); + }); - it("should return null if the license file cannot be read", async () => { - const expectedPath = join("/some/directory", "license.txt"); + it("should return null if the license file does not exist", async () => { + const expectedPath = join("/some/directory", "license.txt"); - when(mockedDoesFileExist).calledWith(expectedPath).mockResolvedValue(true); - when(mockedReadFile) - .calledWith(expectedPath, { encoding: "utf-8" }) - .mockRejectedValue(new Error("Could not read file")); + when(mockedDoesFileExist).calledWith(expectedPath).mockResolvedValue(false); - const inputs: ResolutionInputs = { - packageJson: { - license: "SEE LICENSE IN license.txt", - }, - directory: "/some/directory", - }; + const inputs: ResolutionInputs = { + packageJson: { + license: "SEE LICENSE IN license.txt", + }, + directory: "/some/directory", + }; - const result = await packageJsonLicense(inputs); + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); - expect(mockedReadFile).toHaveBeenCalledWith(expectedPath, { - encoding: "utf-8", + expect(mockedDoesFileExist).toHaveBeenCalledWith(expectedPath); }); - expect(result).toBeNull(); - }); - it("should return the license file contents if the license file exists and can be read", async () => { - const expectedPath = join("/some/directory", "license.txt"); + it("should return null if the license file cannot be read", async () => { + const expectedPath = join("/some/directory", "license.txt"); - when(mockedDoesFileExist).calledWith(expectedPath).mockResolvedValue(true); - when(mockedReadFile) - .calledWith(expectedPath, { encoding: "utf-8" }) - .mockResolvedValue("license contents"); + when(mockedDoesFileExist).calledWith(expectedPath).mockResolvedValue(true); + when(mockedReadFile) + .calledWith(expectedPath, { encoding: "utf-8" }) + .mockRejectedValue(new Error("Could not read file")); - const inputs: ResolutionInputs = { - packageJson: { - license: "SEE LICENSE IN license.txt", + const inputs: ResolutionInputs = { + packageJson: { + license: "SEE LICENSE IN license.txt", + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(mockedReadFile).toHaveBeenCalledWith(expectedPath, { + encoding: "utf-8", + }); + expect(result).toBeNull(); + }); + + it("should return the license file contents if the license file exists and can be read", async () => { + const expectedPath = join("/some/directory", "license.txt"); + + when(mockedDoesFileExist).calledWith(expectedPath).mockResolvedValue(true); + when(mockedReadFile) + .calledWith(expectedPath, { encoding: "utf-8" }) + .mockResolvedValue("license contents"); + + const inputs: ResolutionInputs = { + packageJson: { + license: "SEE LICENSE IN license.txt", + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(mockedReadFile).toHaveBeenCalledWith(expectedPath, { + encoding: "utf-8", + }); + expect(result).toEqual("license contents"); + }); + + it.each([ + "SEE LICENSE IN 'license.txt'", + 'SEE LICENSE IN "license.txt"', + "SEE LICENSE IN ", + ])("should ignore punctuation wrapping the license file path", async licenseFile => { + const expectedPath = join("/some/directory", "license.txt"); + + when(mockedDoesFileExist).calledWith(expectedPath).mockResolvedValue(true); + when(mockedReadFile) + .calledWith(expectedPath, { encoding: "utf-8" }) + .mockResolvedValue("license contents"); + + const inputs: ResolutionInputs = { + packageJson: { + license: licenseFile, + }, + directory: "/some/directory", + }; + + const _ = await packageJsonLicense(inputs); + + expect(mockedDoesFileExist).toHaveBeenCalledWith(expectedPath); + expect(mockedReadFile).toHaveBeenCalledWith(expectedPath, { + encoding: "utf-8", + }); + }); + + it.each(["http://some.url", "www.some.url"])( + "should return the packages.json SPDX expression if the license file is a URL", + async url => { + const inputs: ResolutionInputs = { + packageJson: { license: `SEE LICENSE IN ${url}` }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toEqual(`SEE LICENSE IN ${url}`); }, - directory: "/some/directory", - }; + ); + }); - const result = await packageJsonLicense(inputs); + describe("when the license field is an object", () => { + it("should return the license URL if if it populated", async () => { + const url = "https://some.url"; - expect(mockedReadFile).toHaveBeenCalledWith(expectedPath, { - encoding: "utf-8", + const inputs: ResolutionInputs = { + packageJson: { + license: { url }, + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toEqual(url); + }); + + it("should return null if the license URL is an empty string", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: { url: "" }, + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); + }); + + it("should return null if the license URL is falsy", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: { url: undefined }, + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); }); - expect(result).toEqual("license contents"); }); - it.each([ - "SEE LICENSE IN 'license.txt'", - 'SEE LICENSE IN "license.txt"', - "SEE LICENSE IN ", - ])("should ignore punctuation wrapping the license file path", async licenseFile => { - const expectedPath = join("/some/directory", "license.txt"); + describe("when the license field is an array", () => { + it("should return null if the license array is empty", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: [], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); - when(mockedDoesFileExist).calledWith(expectedPath).mockResolvedValue(true); - when(mockedReadFile) - .calledWith(expectedPath, { encoding: "utf-8" }) - .mockResolvedValue("license contents"); + expect(result).toBeNull(); + }); - const inputs: ResolutionInputs = { - packageJson: { - license: licenseFile, - }, - directory: "/some/directory", - }; + it("should return null if the license array has a single element that has an empty URL", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: [{ url: "" }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); + }); + + it("should return null if the license array has a single element that has no URL", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: [{ url: undefined }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); + }); + + it("should return the license URL if the license array has a single element", async () => { + const url = "https://some.url"; + + const inputs: ResolutionInputs = { + packageJson: { + license: [{ url }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toEqual(url); + }); + + describe("when the license array has multiple elements", () => { + it("should warn", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: [{ url: "https://some.url" }, { url: "https://some.other.url" }], + }, + directory: "/some/directory", + }; + + expect(mockedWarn).toHaveBeenCalledTimes(0); + + const _ = await packageJsonLicense(inputs); + + expect(mockedWarn).toHaveBeenCalledTimes(1); + }); + + it("should return the URL of the first license", async () => { + const url = "https://some.url"; + + const inputs: ResolutionInputs = { + packageJson: { + license: [{ url }, { url: "https://some.other.url" }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toEqual(url); + }); + + it("should return null if the first license has an empty URL", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: [{ url: "" }, { url: "https://some.other.url" }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); - const _ = await packageJsonLicense(inputs); + expect(result).toBeNull(); + }); - expect(mockedDoesFileExist).toHaveBeenCalledWith(expectedPath); - expect(mockedReadFile).toHaveBeenCalledWith(expectedPath, { - encoding: "utf-8", + it("should return null if the first license has no URL", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: [{ url: undefined }, { url: "https://some.other.url" }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); + }); }); }); - it.each(["http://some.url", "www.some.url"])( - "should return the packages.json SPDX expression if the license file is a URL", - async url => { + describe("when the license field is undefined but the licenses field is an array", () => { + it("should return null if the licenses array is empty", async () => { const inputs: ResolutionInputs = { packageJson: { - license: `SEE LICENSE IN ${url}`, + licenses: [], }, directory: "/some/directory", }; const result = await packageJsonLicense(inputs); - expect(result).toEqual(`SEE LICENSE IN ${url}`); - }, - ); + expect(result).toBeNull(); + }); + + it("should return null if the licenses array has a single element that has an empty URL", async () => { + const inputs: ResolutionInputs = { + packageJson: { + licenses: [{ url: "" }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); + }); + + it("should return null if the licenses array has a single element that has no URL", async () => { + const inputs: ResolutionInputs = { + packageJson: { + licenses: [{ url: undefined }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); + }); + + it("should return the license URL if the licenses array has a single element", async () => { + const url = "https://some.url"; + + const inputs: ResolutionInputs = { + packageJson: { + licenses: [{ url }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toEqual(url); + }); + + describe("when the licenses array has multiple elements", () => { + it("should warn", async () => { + const inputs: ResolutionInputs = { + packageJson: { + licenses: [{ url: "https://some.url" }, { url: "https://some.other.url" }], + }, + directory: "/some/directory", + }; + + expect(mockedWarn).toHaveBeenCalledTimes(0); + + const _ = await packageJsonLicense(inputs); + + expect(mockedWarn).toHaveBeenCalledTimes(1); + }); + + it("should return the URL of the first license", async () => { + const url = "https://some.url"; + + const inputs: ResolutionInputs = { + packageJson: { + licenses: [{ url }, { url: "https://some.other.url" }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toEqual(url); + }); + + it("should return null if the first license has an empty URL", async () => { + const inputs: ResolutionInputs = { + packageJson: { + licenses: [{ url: "" }, { url: "https://some.other.url" }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); + }); + + it("should return null if the first license has no URL", async () => { + const inputs: ResolutionInputs = { + packageJson: { + licenses: [{ url: undefined }, { url: "https://some.other.url" }], + }, + directory: "/some/directory", + }; + + const result = await packageJsonLicense(inputs); + + expect(result).toBeNull(); + }); + }); + }); }); diff --git a/src/packages/generate-license-file/test/internal/resolveLicenseContent/spdxExpression.spec.ts b/src/packages/generate-license-file/test/internal/resolveLicenseContent/spdxExpression.spec.ts index 7e9e5fe8..bb80ba4f 100644 --- a/src/packages/generate-license-file/test/internal/resolveLicenseContent/spdxExpression.spec.ts +++ b/src/packages/generate-license-file/test/internal/resolveLicenseContent/spdxExpression.spec.ts @@ -2,7 +2,7 @@ import { spdxExpression } from "../../../src/lib/internal/resolveLicenseContent/ import { ResolutionInputs } from "../../../src/lib/internal/resolveLicenseContent"; import logger from "../../../src/lib/utils/console.utils"; -jest.mock("../../../src/lib/utils/console.utils"); // Stops logger.warn from being called +jest.mock("../../../src/lib/utils/console.utils"); describe("spdxExpression", () => { const mockedWarn = jest.mocked(logger.warn); @@ -10,7 +10,7 @@ describe("spdxExpression", () => { beforeEach(jest.resetAllMocks); afterAll(jest.restoreAllMocks); - it("should return null if the package.json does not have a license field", async () => { + it("should return null if the package.json does not have a license or a licenses field", async () => { const inputs: ResolutionInputs = { packageJson: {}, directory: "/some/directory", @@ -21,53 +21,232 @@ describe("spdxExpression", () => { expect(result).toBeNull(); }); - it("should return null if the license field is empty", async () => { - const inputs: ResolutionInputs = { - packageJson: { - license: "", + describe("when the license field is a string", () => { + it("should return null if the license field is empty", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: "", + }, + directory: "/some/directory", + }; + + const result = await spdxExpression(inputs); + + expect(result).toBeNull(); + }); + + it.each(["Apache-2.0", "BSD-2-Clause", "MIT"])( + "should return the license field if it is not empty", + async expression => { + const inputs: ResolutionInputs = { + packageJson: { + license: expression, + }, + directory: "/some/directory", + }; + + const result = await spdxExpression(inputs); + + expect(result).toBe(expression); }, - directory: "/some/directory", - }; + ); - const result = await spdxExpression(inputs); + it("should warning log if the license field contains ' OR '", async () => { + const inputs: ResolutionInputs = { + packageJson: { + name: "some-package", + version: "1.0.0", + license: "MIT OR Apache-2.0", + }, + directory: "/some/directory", + }; - expect(result).toBeNull(); + const _ = await spdxExpression(inputs); + + expect(mockedWarn).toHaveBeenCalledTimes(1); + expect(mockedWarn).toHaveBeenCalledWith( + `The license expression for ${inputs.packageJson.name}@${inputs.packageJson.version} contains multiple licenses: "MIT OR Apache-2.0"\n` + + "We suggest you determine which license applies to your project and replace the license content\n" + + `for ${inputs.packageJson.name}@${inputs.packageJson.version} using a generate-license-file config file.\n` + + "See: https://generate-license-file.js.org/docs/cli/config-file for more information.\n", + ); + }); }); - it.each(["Apache-2.0", "BSD-2-Clause", "MIT"])( - "should return the license field if it is not empty", - async expression => { + describe("when the license field is an array", () => { + it("should return null if the license field is empty", async () => { const inputs: ResolutionInputs = { packageJson: { - license: expression, + license: [], }, directory: "/some/directory", }; const result = await spdxExpression(inputs); - expect(result).toBe(expression); - }, - ); + expect(result).toBeNull(); + }); - it("should warning log if the license field contains ' OR '", async () => { - const inputs: ResolutionInputs = { - packageJson: { - name: "some-package", - version: "1.0.0", - license: "MIT OR Apache-2.0", - }, - directory: "/some/directory", - }; + describe("when the license field contains a single object", () => { + it("should return null if the license field contains an object with no type", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: [{ url: "https://some.url" }], + }, + directory: "/some/directory", + }; - const _ = await spdxExpression(inputs); + const result = await spdxExpression(inputs); - expect(mockedWarn).toHaveBeenCalledTimes(1); - expect(mockedWarn).toHaveBeenCalledWith( - `The license expression for ${inputs.packageJson.name}@${inputs.packageJson.version} contains multiple licenses: "MIT OR Apache-2.0"\n` + - "We suggest you determine which license applies to your project and replace the license content\n" + - `for ${inputs.packageJson.name}@${inputs.packageJson.version} using a generate-license-file config file.\n` + - "See: https://generate-license-file.js.org/docs/cli/config-file for more information.\n", - ); + expect(result).toBeNull(); + }); + + it("should return the license type field if it contains an object with a type", async () => { + const license = "MIT"; + + const inputs: ResolutionInputs = { + packageJson: { + license: [{ type: license, url: "https://some.url" }], + }, + directory: "/some/directory", + }; + + const result = await spdxExpression(inputs); + + expect(result).toBe(license); + }); + }); + + describe("when the license field contains multiple objects", () => { + it("should warn", () => { + const inputs: ResolutionInputs = { + packageJson: { + name: "some-package", + version: "1.0.0", + license: [ + { type: "MIT", url: "https://some.url" }, + { type: "Apache-2.0", url: "https://some.url" }, + ], + }, + directory: "/some/directory", + }; + + expect(mockedWarn).toHaveBeenCalledTimes(0); + + const _ = spdxExpression(inputs); + + expect(mockedWarn).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe("when the license field is an object", () => { + it("should return null if the license field contains an empty object", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: {}, + }, + directory: "/some/directory", + }; + + const result = await spdxExpression(inputs); + + expect(result).toBeNull(); + }); + + it("should return null if the license field contains an object with no type", async () => { + const inputs: ResolutionInputs = { + packageJson: { + license: { url: "https://some.url" }, + }, + directory: "/some/directory", + }; + + const result = await spdxExpression(inputs); + + expect(result).toBeNull(); + }); + + it("should return the license type field if it contains an object with a type", async () => { + const license = "MIT"; + + const inputs: ResolutionInputs = { + packageJson: { + license: { type: license, url: "https://some.url" }, + }, + directory: "/some/directory", + }; + + const result = await spdxExpression(inputs); + + expect(result).toBe(license); + }); + }); + + describe("when the licenses field is an array", () => { + it("should return null if the licenses field is empty", async () => { + const inputs: ResolutionInputs = { + packageJson: { + licenses: [], + }, + directory: "/some/directory", + }; + + const result = await spdxExpression(inputs); + + expect(result).toBeNull(); + }); + + describe("when the licenses field contains a single object", () => { + it("should return null if the licenses field contains an object with no type", async () => { + const inputs: ResolutionInputs = { + packageJson: { + licenses: [{ url: "https://some.url" }], + }, + directory: "/some/directory", + }; + + const result = await spdxExpression(inputs); + + expect(result).toBeNull(); + }); + + it("should return the license type field if it contains an object with a type", async () => { + const license = "MIT"; + + const inputs: ResolutionInputs = { + packageJson: { + licenses: [{ type: license, url: "https://some.url" }], + }, + directory: "/some/directory", + }; + + const result = await spdxExpression(inputs); + + expect(result).toBe(license); + }); + }); + + describe("when the licenses field contains multiple objects", () => { + it("should warn", () => { + const inputs: ResolutionInputs = { + packageJson: { + name: "some-package", + version: "1.0.0", + licenses: [ + { type: "MIT", url: "https://some.url" }, + { type: "Apache-2.0", url: "https://some.other.url" }, + ], + }, + directory: "/some/directory", + }; + + expect(mockedWarn).toHaveBeenCalledTimes(0); + + const _ = spdxExpression(inputs); + + expect(mockedWarn).toHaveBeenCalledTimes(1); + }); + }); }); });