From a3e2b2ff282c25d5c2e47b57dbf04f51724ff34c Mon Sep 17 00:00:00 2001
From: Caleb Cartwright <calebcartwright@users.noreply.github.com>
Date: Mon, 27 Sep 2021 17:53:09 -0500
Subject: [PATCH] handle null licenses in crates.io response schema, run
 [crates] (#7074)

* fix: handle null licenses in crates.io response schema

* more tests for crates license badge

Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
---
 services/crates/crates-base.js             |  4 +--
 services/crates/crates-downloads.tester.js |  9 ++---
 services/crates/crates-license.service.js  | 39 ++++++++++++----------
 services/crates/crates-license.spec.js     | 38 +++++++++++++++++++++
 services/crates/crates-license.tester.js   | 20 ++++++-----
 5 files changed, 75 insertions(+), 35 deletions(-)
 create mode 100644 services/crates/crates-license.spec.js

diff --git a/services/crates/crates-base.js b/services/crates/crates-base.js
index e87fe5cea8..63699a581e 100644
--- a/services/crates/crates-base.js
+++ b/services/crates/crates-base.js
@@ -14,7 +14,7 @@ const crateSchema = Joi.object({
     .items(
       Joi.object({
         downloads: nonNegativeInteger,
-        license: Joi.string().required(),
+        license: Joi.string().required().allow(null),
       })
     )
     .min(1)
@@ -25,7 +25,7 @@ const versionSchema = Joi.object({
   version: Joi.object({
     downloads: nonNegativeInteger,
     num: Joi.string().required(),
-    license: Joi.string().required(),
+    license: Joi.string().required().allow(null),
   }).required(),
 }).required()
 
diff --git a/services/crates/crates-downloads.tester.js b/services/crates/crates-downloads.tester.js
index 9e2e13ccf4..629cd05ed0 100644
--- a/services/crates/crates-downloads.tester.js
+++ b/services/crates/crates-downloads.tester.js
@@ -1,11 +1,6 @@
-import { ServiceTester } from '../tester.js'
 import { isMetric } from '../test-validators.js'
-
-export const t = new ServiceTester({
-  id: 'crates',
-  title: 'crates.io',
-  pathPrefix: '/crates',
-})
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
 
 t.create('total downloads')
   .get('/d/libc.json')
diff --git a/services/crates/crates-license.service.js b/services/crates/crates-license.service.js
index c32ab9517b..562f44841c 100644
--- a/services/crates/crates-license.service.js
+++ b/services/crates/crates-license.service.js
@@ -1,3 +1,4 @@
+import { InvalidResponse } from '../index.js'
 import { BaseCratesService, keywords } from './crates-base.js'
 
 export default class CratesLicense extends BaseCratesService {
@@ -21,28 +22,30 @@ export default class CratesLicense extends BaseCratesService {
     },
   ]
 
-  static render({ license }) {
-    return {
-      label: 'license',
-      message: license,
-      color: 'blue',
-    }
+  static defaultBadgeData = { label: 'license', color: 'blue' }
+
+  static render({ license: message }) {
+    return { message }
   }
 
-  async handle({ crate, version }) {
-    const json = await this.fetch({ crate, version })
+  static transform({ errors, version, versions }) {
+    // crates.io returns a 200 response with an errors object in
+    // error scenarios, e.g. https://crates.io/api/v1/crates/libc/0.1
+    if (errors) {
+      throw new InvalidResponse({ prettyMessage: errors[0].detail })
+    }
 
-    if (json.errors) {
-      /* a call like
-         https://crates.io/api/v1/crates/libc/0.1
-         or
-         https://crates.io/api/v1/crates/libc/0.1.76
-         returns a 200 OK with an errors object */
-      return { message: json.errors[0].detail }
+    const license = version ? version.license : versions[0].license
+    if (!license) {
+      throw new InvalidResponse({ prettyMessage: 'invalid null license' })
     }
 
-    return this.constructor.render({
-      license: json.version ? json.version.license : json.versions[0].license,
-    })
+    return { license }
+  }
+
+  async handle({ crate, version }) {
+    const json = await this.fetch({ crate, version })
+    const { license } = this.constructor.transform(json)
+    return this.constructor.render({ license })
   }
 }
diff --git a/services/crates/crates-license.spec.js b/services/crates/crates-license.spec.js
new file mode 100644
index 0000000000..f6454094dc
--- /dev/null
+++ b/services/crates/crates-license.spec.js
@@ -0,0 +1,38 @@
+import { expect } from 'chai'
+import { test, given } from 'sazerac'
+import { InvalidResponse } from '../index.js'
+import CratesLicense from './crates-license.service.js'
+
+describe('CratesLicense', function () {
+  test(CratesLicense.transform, () => {
+    given({
+      version: { num: '1.0.0', license: 'MIT' },
+      versions: [{ license: 'MIT/Apache 2.0' }],
+    }).expect({ license: 'MIT' })
+    given({
+      versions: [{ license: 'MIT/Apache 2.0' }],
+    }).expect({ license: 'MIT/Apache 2.0' })
+  })
+
+  it('throws InvalidResponse on error response', function () {
+    expect(() =>
+      CratesLicense.transform({ errors: [{ detail: 'invalid semver' }] })
+    )
+      .to.throw(InvalidResponse)
+      .with.property('prettyMessage', 'invalid semver')
+  })
+
+  it('throws InvalidResponse on null license with specific version', function () {
+    expect(() =>
+      CratesLicense.transform({ version: { num: '1.2.3', license: null } })
+    )
+      .to.throw(InvalidResponse)
+      .with.property('prettyMessage', 'invalid null license')
+  })
+
+  it('throws InvalidResponse on null license with latest version', function () {
+    expect(() => CratesLicense.transform({ versions: [{ license: null }] }))
+      .to.throw(InvalidResponse)
+      .with.property('prettyMessage', 'invalid null license')
+  })
+})
diff --git a/services/crates/crates-license.tester.js b/services/crates/crates-license.tester.js
index 390295433b..6351c14f85 100644
--- a/services/crates/crates-license.tester.js
+++ b/services/crates/crates-license.tester.js
@@ -1,10 +1,5 @@
-import { ServiceTester } from '../tester.js'
-
-export const t = new ServiceTester({
-  id: 'crates',
-  title: 'crates.io',
-  pathPrefix: '/crates/l',
-})
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
 
 t.create('license')
   .get('/libc.json')
@@ -16,4 +11,13 @@ t.create('license (with version)')
 
 t.create('license (not found)')
   .get('/not-a-real-package.json')
-  .expectBadge({ label: 'crates.io', message: 'not found' })
+  .expectBadge({ label: 'license', message: 'not found' })
+
+// https://github.com/badges/shields/issues/7073
+t.create('license (null licenses in history)')
+  .get('/stun.json')
+  .expectBadge({ label: 'license', message: 'MIT/Apache-2.0' })
+
+t.create('license (version with null license)')
+  .get('/stun/0.0.1.json')
+  .expectBadge({ label: 'license', message: 'invalid null license' })
-- 
GitLab