Unit Testing Licenses: Monitoring the legality of your node_modules with Jest
Moritz Jacobs
March 23, 2023
Packages from the npm registry are the heart and soul of most larger JavaScript projects. Libraries, frameworks, tooling, etc. - all of our projects stand on the shoulders of giants open source projects. Writing and maintaining all that code in-house would be impossible, even for the biggest companies out there. But there is a price to pay, and supply chain attacks are just one of the risks of using third-party code. In a commercial context, you must always be aware of the legal implications of open source licenses. There are a myriad of different licenses, and they all allow different kinds of use. I will explicitly not go into the details (I am not a lawyer, you should always consult legal professionals for these matters), but it is important to keep track of the licenses you use in your projects. Not every open source license is suitable for every use case, and we have to be aware of this!
There are 3 main concerns we need to deal with:
- We want to allow our clients (or their legal team) to have an overview of all the licenses we use in their product (often a contractual obligation).
- We want to avoid certain licenses, so we need to prevent developers from unknowingly adding them.
- We want to know if a package has changed its license for some reason, and then check if the new license is still allowed.
Today I want to show how we can address these problems by making licenses part of the test suite. This test suite could be run on every proposed change to the codebase (e.g. as a GitHub action on every Pull Request), so that an unwanted change in licensing will cause our CI to fail early.
Example project
Let's say we have a TypeScript project that already has a few unit tests in place. It uses Jest / ts-jest for that, but you can probably adapt the idea for any other test framework. If you're new to Jest, they have an excellent setup guide. ts-jest also has you covered if you want to use TypeScript.
To easily compile the information we want, we are going to use license-checker-rseidelsohn, which gathers the packages from our package file and then extracts license and author information from node_modules. It's then able to generate a list in multiple formats -- in our case, markdown. This package is also handy because it is able to provide a CLI as well as exports for programmatic use. Here's what a CLI run could look like (--direct means: only include direct dependencies, not their transitive dependencies):
→ npx license-checker-rseidelsohn --markdown --direct
We can now write the result to a markdown file (→ npx license-checker-rseidelsohn --markdown > LICENSES.md). If this file is in the repo of our project, GitHub will render pretty HTML for us. For 99% of our projects this will solve use case #1, our client's legal team gets access to that GitHub URL and we're done. To keep the file up to date we can also use a GitHub action like so:
This will open a PR when the LICENSES.md needs to change.
That was easy. Now to the fun part:
Unit testing against legality
Let's put all of the other checks inside a new unit test, let's call it licenses.test.ts (again, we're using TypeScript, which is not necessary for this, if you don't want to). In this test we need to do a few things:
First, let's import the checker and promisify it, so we can use async/await later:
The result from the checker is an object with a packageName@version as its keys and some more information as its values:
Since we don't need all of it, yet the version number is in the key, we should probably tidy it up before we use it. Let's do all of that in beforeAll:
As a first test, let's find out if any current packages use disallowed licenses. As an example, I will only allow ["UNLICENSED", "MIT", "BSD-3-Clause", "Apache-2.0"]. The actual test uses a custom matcher, so we can control the output of failing test messages better. We also need to tell TypeScript what to expect from our custom matcher.
Now the first test is very simple:
This test will run every package against our custom matcher and fail, if its license(s) is not in the allow list. This solves our use case #2.
Now for our second test, we want to future-proof the set of different licenses in the project in order to detect if a package was added, removed or had its license changed. We can do this using a snapshot test:
For those unaware of what a snapshot test is: when it is executed for the first time, it will save a current snapshot of the result to a file __snapshots__/licenses.test.ts.snap. This file must then be checked into the repository. Everytime the test runs in the future, its result will be compared to that snapshot. If it changed, the test will fail and Jest will tell you.
Alright, let's run our tests: → npm run test
Everything seems good, all our packages honor our LICENSE_ALLOW_LIST and we now have a snapshot file, that we can commit:
Let's see what happens when we add a dependency: → npm install @react-hookz/web → npm run test
From this output we can see, that @react-hookz/web's license is allowed (the first test passed!) but it's not in the snapshot yet. Let's fix that by running npm run test -- -u:
Let's add another dependency: → npm install rimraf → npm run test.:
Uh oh! rimraf uses the ISC license, which is not in our allow list and our first test caught that: "ISC" is not an allowed license (dependency: rimraf). We can now decide if we want to remove this package and use something else or change the allow list of the test.
Summary:
This should do the trick! All 3 use cases are covered by a few lines of TypeScript and YAML. You can check out a fully working example repo (including GitHub actions) here: https://github.com/peerigon/blog-license-check-demo
If you only want the test file, here you go:
open source licenses
unit testing
node_modules
Read also
Klara, 12/05/2024
Accessibility – An Essential Ingredient in the Batter or the "Icing" on the Cake?
Web Accessibility
Post Mortem
Konsens
Digital Inclusion
Web Development
Digitale Barrierefreiheit
Francesca, Ricarda, 11/21/2024
Top 10 Mistakes to Avoid When Building a Digital Product
MVP development
UX/UI design
product vision
agile process
user engagement
product development
Leonhard, 10/22/2024
Strategies to Quickly Explore a New Codebase
Web App Development
Consulting
Audit