Repository: badges/shields (js)

Issue:
[liberapay] TypeError: Cannot read property 'amount' of null
Sentry Issue: [SHIELDS-3N](https://sentry.io/organizations/shields/issues/1143969995/?referrer=github_integration)

```
TypeError: Cannot read property 'amount' of null
  File "/home/m/shields/services/liberapay/liberapay-goal.service.js", line 34, in LiberapayGoal.handle
    (data.receiving.amount / data.goal.amount) * 100
  ?, in null.<anonymous>
```

Pull request:
fix [LiberapayGoal] type error when no receiving data exists
Fixes #4011 

Patch:
diff --git a/services/liberapay/liberapay-goal.service.js b/services/liberapay/liberapay-goal.service.js
index 0b78efd747..986b5f5cb7 100644
--- a/services/liberapay/liberapay-goal.service.js
+++ b/services/liberapay/liberapay-goal.service.js
@@ -14,28 +14,36 @@ module.exports = class LiberapayGoal extends LiberapayBase {
       {
         title: 'Liberapay goal progress',
         namedParams: { entity: 'Changaco' },
-        staticPreview: this.render({ percentAcheived: 33 }),
+        staticPreview: this.render({ percentAchieved: 33 }),
       },
     ]
   }
 
-  static render({ percentAcheived }) {
+  static render({ percentAchieved }) {
     return {
       label: 'goal progress',
-      message: `${percentAcheived}%`,
-      color: colorScale([0, 10, 100])(percentAcheived),
+      message: `${percentAchieved}%`,
+      color: colorScale([0, 10, 100])(percentAchieved),
     }
   }
 
-  async handle({ entity }) {
-    const data = await this.fetch({ entity })
-    if (data.goal) {
-      const percentAcheived = Math.round(
-        (data.receiving.amount / data.goal.amount) * 100
-      )
-      return this.constructor.render({ percentAcheived })
-    } else {
+  transform({ goal, receiving }) {
+    if (!goal) {
       throw new InvalidResponse({ prettyMessage: 'no public goals' })
     }
+
+    if (!receiving) {
+      return { percentAchieved: 0 }
+    }
+
+    const percentAchieved = Math.round((receiving.amount / goal.amount) * 100)
+
+    return { percentAchieved }
+  }
+
+  async handle({ entity }) {
+    const { goal, receiving } = await this.fetch({ entity })
+    const { percentAchieved } = this.transform({ goal, receiving })
+    return this.constructor.render({ percentAchieved })
   }
 }
diff --git a/services/liberapay/liberapay-goal.spec.js b/services/liberapay/liberapay-goal.spec.js
new file mode 100644
index 0000000000..579726068f
--- /dev/null
+++ b/services/liberapay/liberapay-goal.spec.js
@@ -0,0 +1,25 @@
+'use strict'
+
+const { expect } = require('chai')
+const { test, given } = require('sazerac')
+const LiberapayGoal = require('./liberapay-goal.service')
+const { InvalidResponse } = require('..')
+
+describe('LiberapayGoal', function() {
+  test(LiberapayGoal.prototype.transform, () => {
+    given({ goal: {}, receiving: null }).expect({
+      percentAchieved: 0,
+    })
+    given({ goal: { amount: 100 }, receiving: { amount: 89 } }).expect({
+      percentAchieved: 89,
+    })
+  })
+
+  it('throws InvalidResponse on missing goals', function() {
+    expect(() =>
+      LiberapayGoal.prototype.transform({ goal: null, receiving: null })
+    )
+      .to.throw(InvalidResponse)
+      .with.property('prettyMessage', 'no public goals')
+  })
+})


Test patch:
diff --git a/services/liberapay/liberapay-goal.tester.js b/services/liberapay/liberapay-goal.tester.js
index 42189d70cc..9fe3441388 100644
--- a/services/liberapay/liberapay-goal.tester.js
+++ b/services/liberapay/liberapay-goal.tester.js
@@ -13,17 +13,3 @@ t.create('Goal Progress (valid)')
 t.create('Goal Progress (not found)')
   .get('/does-not-exist.json')
   .expectBadge({ label: 'liberapay', message: 'not found' })
-
-t.create('Goal Progress (no goal set)')
-  .get('/Liberapay.json')
-  .intercept(nock =>
-    nock('https://liberapay.com')
-      .get('/Liberapay/public.json')
-      .reply(200, {
-        npatrons: 0,
-        giving: null,
-        receiving: null,
-        goal: null,
-      })
-  )
-  .expectBadge({ label: 'liberapay', message: 'no public goals' })