Repository: Yoctol/bottender
Pull request #595 (2019-12-23 15:52:26)

fix(session): should never expire by default
In our docs, we say session will never expire by default, so this is a quick fix. In other hand, one year is not a good default value from our perspective.

codecov-io: # [Codecov](https://codecov.io/gh/Yoctol/bottender/pull/595?src=pr&el=h1) Report
> Merging [#595](https://codecov.io/gh/Yoctol/bottender/pull/595?src=pr&el=desc) into [master](https://codecov.io/gh/Yoctol/bottender/commit/c3147ae679bf803ab1c2f3a2a5651d571aada999?src=pr&el=desc) will **decrease** coverage by `0.03%`.
> The diff coverage is `86.66%`.

[![Impacted file tree graph](https://codecov.io/gh/Yoctol/bottender/pull/595/graphs/tree.svg?width=650&token=JX3EGtxYH3&height=150&src=pr)](https://codecov.io/gh/Yoctol/bottender/pull/595?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #595      +/-   ##
==========================================
- Coverage   79.79%   79.75%   -0.04%     
==========================================
  Files          89       89              
  Lines        3984     3987       +3     
  Branches      958      971      +13     
==========================================
+ Hits         3179     3180       +1     
+ Misses        770      768       -2     
- Partials       35       39       +4
```


| [Impacted Files](https://codecov.io/gh/Yoctol/bottender/pull/595?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...es/bottender/src/session/CacheBasedSessionStore.ts](https://codecov.io/gh/Yoctol/bottender/pull/595/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zZXNzaW9uL0NhY2hlQmFzZWRTZXNzaW9uU3RvcmUudHM=) | `100% <100%> (ø)` | :arrow_up: |
| [...ckages/bottender/src/session/MemorySessionStore.ts](https://codecov.io/gh/Yoctol/bottender/pull/595/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zZXNzaW9uL01lbW9yeVNlc3Npb25TdG9yZS50cw==) | `100% <100%> (ø)` | :arrow_up: |
| [...ackages/bottender/src/session/RedisSessionStore.ts](https://codecov.io/gh/Yoctol/bottender/pull/595/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zZXNzaW9uL1JlZGlzU2Vzc2lvblN0b3JlLnRz) | `100% <100%> (ø)` | :arrow_up: |
| [packages/bottender/src/cache/MemoryCacheStore.ts](https://codecov.io/gh/Yoctol/bottender/pull/595/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9jYWNoZS9NZW1vcnlDYWNoZVN0b3JlLnRz) | `100% <100%> (ø)` | :arrow_up: |
| [packages/bottender/src/cache/RedisCacheStore.ts](https://codecov.io/gh/Yoctol/bottender/pull/595/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9jYWNoZS9SZWRpc0NhY2hlU3RvcmUudHM=) | `100% <100%> (ø)` | :arrow_up: |
| [...ackages/bottender/src/session/MongoSessionStore.ts](https://codecov.io/gh/Yoctol/bottender/pull/595/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zZXNzaW9uL01vbmdvU2Vzc2lvblN0b3JlLnRz) | `97.29% <66.66%> (-2.71%)` | :arrow_down: |
| [packages/bottender/src/session/FileSessionStore.ts](https://codecov.io/gh/Yoctol/bottender/pull/595/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zZXNzaW9uL0ZpbGVTZXNzaW9uU3RvcmUudHM=) | `91.66% <66.66%> (-1.96%)` | :arrow_down: |
| [packages/create-bottender-app/src/shared/log.ts](https://codecov.io/gh/Yoctol/bottender/pull/595/diff?src=pr&el=tree#diff-cGFja2FnZXMvY3JlYXRlLWJvdHRlbmRlci1hcHAvc3JjL3NoYXJlZC9sb2cudHM=) | `0% <0%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Yoctol/bottender/pull/595?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Yoctol/bottender/pull/595?src=pr&el=footer). Last update [c3147ae...129fa7b](https://codecov.io/gh/Yoctol/bottender/pull/595?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/channel-messenger-handling-events.md b/docs/channel-messenger-handling-events.md
index b539c123..9229ec67 100644
--- a/docs/channel-messenger-handling-events.md
+++ b/docs/channel-messenger-handling-events.md
@@ -3,10 +3,10 @@ id: channel-messenger-handling-events
 title: Handling Messenger Events
 ---
 
-For a Messenger bot, the two most frequent LINE events are `Text Event` and `Payload Event.`
+For a Messenger bot, the two most frequent Messenger events are `Text Event` and `Payload Event.`
 
 - `Text Event` is triggered when a user inputs text.
-- `Payload Event` can be triggered by postback buttons on template, imagemap, flex messages and rich menus, or quick replies.
+- `Payload Event` can be triggered by postback buttons on all kinds of templates, buttons, persistent menu, or quick replies.
 
 Apart from the above events, Messenger also supports advanced events for better user experience. For example, [`Media Related Events`](#media-related-events) offer a bunch of events when users send a piece of rich media to a bot, for instance, image, video, audio, and file. [Delivery/Read Event](#deliveryread-events) is handy while calculating how many users received a broadcast message. A humor response of [Sticker Event](#sticker-events), makes your bot more human-like.
 
@@ -85,9 +85,9 @@ async function App(context) {
 
 ### Media Related Events
 
-In our current experience, image and location is comparatively accessible. For example, an `Image Event` could be a starting point for image-based search engine to find relevant products. And `Location Event` is often used under the context that when your bot tries to recommendation your user nearby shops or branches.
+In our current experience, image and location are comparatively accessible. For example, an `Image Event` could be a starting point for a image-based search engine to find relevant products. And `Location Event` is often used under the context that when your bot tries to recommendation your user nearby shops or branches.
 
-In the following example, you can see a bunch of `Media Related Events,` from image, audio, video, file, and location.
+In the following example, you can see a bunch of `Media Related Events` from image, audio, video, file, and location.
 
 ```js
 async function HandleImage(context) {
@@ -161,7 +161,7 @@ async function App(context) {
 
 ### Sticker Events
 
-A proper response of `Sticker Event` would make your bot more human-like. For example, you can define a set of stickers that your bots understand as a shortcut (or sercret code). It offers an alternative way for your user to interact with your bot.
+A proper response of `Sticker Event` would make your bot more human-like. For example, you can define a set of stickers that your bots understand as a shortcut (or secret code). It offers an alternative way for your user to interact with your bot.
 
 In the following example, you can see an example of the famous `Like Sticker` (a growing big thumb up).
 
diff --git a/docs/channel-messenger-profile.md b/docs/channel-messenger-profile.md
index 26085fef..05dcaa25 100644
--- a/docs/channel-messenger-profile.md
+++ b/docs/channel-messenger-profile.md
@@ -42,7 +42,7 @@ After setting environment variables correctly (if not, please check [this](chann
 npx bottender messenger profile set
 ```
 
-> **Note:** Calls to the Messenger Profile API are limited to **10 API calls per 10 minute** interval. This rate limit is enforced per Page. You can retry it a few minutes later if rate limit exceeded.
+> **Note:** Calls to the Messenger Profile API are limited to **10 API calls per 10 minute** interval. This rate limit is enforced per Page. You could retry it a few minutes later if the rate limit exceeded.
 
 To view all set messenger profile, you may use the `messenger profile get` command:
 
@@ -219,7 +219,7 @@ module.exports = {
 
 `question` will be posted on the thread as the user asking the question and `payload` will be returned as a postback webhook event.
 
-> **Note:** Some of the profile elements like Ice Breakers and Get Started button are incompatible with each other. So when both are set one will take precedence over the other. Here is the priority from highest to lowest:
+> **Note:** Some of the profile elements like Ice Breakers and Get Started button are incompatible with each other. So when both are set, one will take precedence over the other. Here is the priority from highest to lowest:
 >
 > - API Ice Breakers
 > - Get Started button
diff --git a/docs/channel-messenger-sending-messages.md b/docs/channel-messenger-sending-messages.md
index 0517e6e6..c64b887a 100644
--- a/docs/channel-messenger-sending-messages.md
+++ b/docs/channel-messenger-sending-messages.md
@@ -9,12 +9,12 @@ Although the document is about "sending messages," in most of the cases, Messeng
 
 > **Note:**
 >
-> - If you want to know better about when your bots can send message proactively, please refer to Messenger's guide link about [Messenger Platform Policy Overview](https://developers.facebook.com/docs/messenger-platform/policy/policy-overview#standard_messaging)
-> - We have a separate document to introduce user event handling, please refer to [Handling Messenger Events](./channel-messenger-handling-events.md)
+> - If you want to know better about when your bots can send message proactively, please refer to Messenger's guide link about [Messenger Platform Policy Overview](https://developers.facebook.com/docs/messenger-platform/policy/policy-overview#standard_messaging).
+> - We have a separate document to introduce user event handling, please refer to [Handling Messenger Events](./channel-messenger-handling-events.md).
 
 If you are not familiar with Messenger messages, we would like to recommend a short happy path.
 
-To begin with, please try the basis of communication, [`Text Messages`](#sending-text-messages). Secondly, try [`Generic Template Messages`](#sending-template-messages) to help you display a collection of items (e.g., recommended restaurants, songs, or books). Finally, - [`Quick Replies`](#sending-with-quick-reply), which continuously guide your users for the next possible actions.
+To begin with, please try the basis of communication, [`Text Messages`](#sending-text-messages). Secondly, try [`Generic Template Messages`](#sending-template-messages) to help you display a collection of items (e.g., recommended restaurants, songs, or books). Finally, - [`Quick Replies`](#sending-with-quick-reply), which continuously guides your users for the next possible actions.
 
 If you have experienced with Messenger messages, don't miss [`Rich Media Messages`](#sending-rich-media-messages) to show the personality of your bots. [`Rich Media Messages`](#sending-rich-media-messages) and [`Media Template Messages`](#media-template) are necessary if you are building a media-driven, storytelling bot. Take a look at [`Persona`](#sending-with-persona), if you offer bot auto-reply and human agent customer support at the same time. Finally, if you are making a chatbot for campaign purposes, please dive into [`Rate Limits`](#rate-limits) to ensure your bot is ready for high traffic.
 
@@ -59,7 +59,7 @@ If you want to benefit from Facebook's cache, i.e., a much pleasant loading spee
 
 Firstly, you need to get a page-scoped `Attachment Id` by [Attachment Upload API](https://developers.facebook.com/docs/messenger-platform/send-messages/saving-assets#attachment_upload_api).
 
-Once you have get your `Attachment Id`, you can send rich media messages with the following code.
+Once you get your `Attachment Id`, you can send rich media messages with the following code.
 
 ```js
 await context.sendImage({ attachmentId: '<ATTACHMENT_ID>' });
@@ -319,7 +319,7 @@ The `User Email Quick Reply` can be treated as an agree button to collect the us
 Messenger automatically pre-fill the displayed quick reply with the Email from the user's profile information. Since many CRM (Customer Relationship Management) use the Email as a unique id, it is handy for future user mapping.
 
 > **Note:**
-> If the user's profile does not have a Email, the quick reply doesn't show up. Also, the bot only receives the Email until the user clicks the quick reply.
+> If the user's profile does not have an Email, the quick reply doesn't show up. Also, the bot only receives the Email until the user clicks the quick reply.
 
 ```js
 await context.sendText('Hi!', {
@@ -343,7 +343,7 @@ await context.sendText('Hi!', {
 
 ## Rate Limits
 
-If you are making a bot with a sudden high traffic, e.g., a campaign bot for Black Friday, you should deal with `Rate Limits` before you receive [error code 613](https://developers.facebook.com/docs/messenger-platform/reference/send-api/error-codes).
+If you are making a bot with sudden high traffic, e.g., a campaign bot for Black Friday, you should deal with `Rate Limits` before you receive [error code 613](https://developers.facebook.com/docs/messenger-platform/reference/send-api/error-codes).
 
 Page Rate limits are in place to prevent malicious behavior and poor user experiences. For Pages with large audiences, Messenger recommends a send rate of 250 requests per second.
 
diff --git a/docs/channel-messenger-setup.md b/docs/channel-messenger-setup.md
index 620ba1a8..1060e92c 100644
--- a/docs/channel-messenger-setup.md
+++ b/docs/channel-messenger-setup.md
@@ -19,7 +19,7 @@ Make sure that you checked the `messenger` option:
 
 ![](https://user-images.githubusercontent.com/3382565/67851223-f2b7f200-fb44-11e9-960a-4f58d68ab37d.png)
 
-After finishing `Create Bottender App` process, `bottender.config.js`, a config file, will be generated automatically for furthur channel settings.
+After finishing `Create Bottender App` process, `bottender.config.js`, a config file, will be generated automatically for further channel settings.
 
 ### Enable Messenger Channel for Existing Apps
 
@@ -56,7 +56,7 @@ To make a Messenger bot work, you have to setup the following values:
 
 ### Messenger Page Id, Messenger Access Token, Messenger App Id, Messenger App Secret, and Messenger Verify Token
 
-These fields could be found on your Facebook App page, which you can accessed from your [Facebook App Dashboard](https://developers.facebook.com/apps). Then, you need to fill in corresponding fields in `.env` for loading those values correctly into `bottender.config.js`:
+These fields could be found on your Facebook App page, which you can access from your [Facebook App Dashboard](https://developers.facebook.com/apps). Then, you need to fill in corresponding fields in `.env` for loading those values correctly into `bottender.config.js`:
 
 ```
 MESSENGER_PAGE_ID=
@@ -72,7 +72,7 @@ MESSENGER_VERIFY_TOKEN=
 
 ### Webhook
 
-After finishing above settings, you can start your server with Messenger webhook event listening using following commands:
+After finishing the above settings, you can start your server with Messenger webhook event listening using the following commands:
 
 ```sh
 # in production mode
@@ -92,4 +92,4 @@ Now you are ready to interact with your bot on Messenger :)
 
 > **Note:**
 >
-> - Before you release your bot to the public, you have to submit your App to Facebook to get relevant permissions, e.g. `pages_message`. See Facebook's official document, [Submitting Your Messenger App](https://developers.facebook.com/docs/messenger-platform/app-review/), for more information.
+> - Before you release your bot to the public, you have to submit your App to Facebook to get relevant permissions, e.g., `pages_message`. See Facebook's official document, [Submitting Your Messenger App](https://developers.facebook.com/docs/messenger-platform/app-review/), for more information.
diff --git a/packages/bottender/src/cache/MemoryCacheStore.ts b/packages/bottender/src/cache/MemoryCacheStore.ts
index 4a556770..4d82a1b5 100644
--- a/packages/bottender/src/cache/MemoryCacheStore.ts
+++ b/packages/bottender/src/cache/MemoryCacheStore.ts
@@ -28,7 +28,11 @@ export default class MemoryCacheStore implements CacheStore {
     // cloneDeep: To make sure save as writable object
     const val = value && typeof value === 'object' ? cloneDeep(value) : value;
 
-    this._lru.set(key, val, minutes * 60 * 1000);
+    if (minutes) {
+      this._lru.set(key, val, minutes * 60 * 1000);
+    } else {
+      this._lru.set(key, val);
+    }
   }
 
   async forget(key: string): Promise<void> {
diff --git a/packages/bottender/src/cache/RedisCacheStore.ts b/packages/bottender/src/cache/RedisCacheStore.ts
index 7883b89b..81c10ae2 100644
--- a/packages/bottender/src/cache/RedisCacheStore.ts
+++ b/packages/bottender/src/cache/RedisCacheStore.ts
@@ -50,11 +50,15 @@ export default class RedisCacheStore implements CacheStore {
   }
 
   async put(key: string, value: CacheValue, minutes: number): Promise<void> {
-    await this._redis.setex(
-      `${this._prefix}${key}`,
-      minutes * 60,
-      this._serialize(value)
-    );
+    if (minutes) {
+      await this._redis.setex(
+        `${this._prefix}${key}`,
+        minutes * 60,
+        this._serialize(value)
+      );
+    } else {
+      await this._redis.set(`${this._prefix}${key}`, this._serialize(value));
+    }
   }
 
   async forget(key: string): Promise<void> {
diff --git a/packages/bottender/src/session/CacheBasedSessionStore.ts b/packages/bottender/src/session/CacheBasedSessionStore.ts
index 581ee1e8..f87db6ee 100644
--- a/packages/bottender/src/session/CacheBasedSessionStore.ts
+++ b/packages/bottender/src/session/CacheBasedSessionStore.ts
@@ -3,17 +3,15 @@ import CacheStore from '../cache/CacheStore';
 import Session from './Session';
 import SessionStore from './SessionStore';
 
-const MINUTES_IN_ONE_YEAR = 365 * 24 * 60;
-
 export default class CacheBasedSessionStore implements SessionStore {
   _cache: CacheStore;
 
   // The number of minutes to store the data in the session.
   _expiresIn: number;
 
-  constructor(cache: CacheStore, expiresIn: number) {
+  constructor(cache: CacheStore, expiresIn?: number) {
     this._cache = cache;
-    this._expiresIn = expiresIn || MINUTES_IN_ONE_YEAR;
+    this._expiresIn = expiresIn || 0;
   }
 
   async init(): Promise<CacheBasedSessionStore> {
diff --git a/packages/bottender/src/session/FileSessionStore.ts b/packages/bottender/src/session/FileSessionStore.ts
index 3e161396..12a4a067 100644
--- a/packages/bottender/src/session/FileSessionStore.ts
+++ b/packages/bottender/src/session/FileSessionStore.ts
@@ -7,8 +7,6 @@ import subMinutes from 'date-fns/subMinutes';
 import Session from './Session';
 import SessionStore from './SessionStore';
 
-const MINUTES_IN_ONE_YEAR = 365 * 24 * 60;
-
 type FileOption =
   | string
   | {
@@ -32,7 +30,7 @@ export default class FileSessionStore implements SessionStore {
   _expiresIn: number;
 
   constructor(arg: FileOption, expiresIn?: number) {
-    this._expiresIn = expiresIn || MINUTES_IN_ONE_YEAR;
+    this._expiresIn = expiresIn || 0;
 
     const dirname = getDirname(arg) || '.sessions';
 
@@ -116,6 +114,10 @@ export default class FileSessionStore implements SessionStore {
   }
 
   _expired(sess: Session): boolean {
+    if (!this._expiresIn) {
+      return false;
+    }
+
     return (
       sess.lastActivity !== undefined &&
       isBefore(sess.lastActivity, subMinutes(Date.now(), this._expiresIn))
diff --git a/packages/bottender/src/session/MemorySessionStore.ts b/packages/bottender/src/session/MemorySessionStore.ts
index 9df6c6e4..e64f6412 100644
--- a/packages/bottender/src/session/MemorySessionStore.ts
+++ b/packages/bottender/src/session/MemorySessionStore.ts
@@ -3,8 +3,6 @@ import MemoryCacheStore from '../cache/MemoryCacheStore';
 import CacheBasedSessionStore from './CacheBasedSessionStore';
 import SessionStore from './SessionStore';
 
-const MINUTES_IN_ONE_YEAR = 365 * 24 * 60;
-
 type MemoryOption =
   | number
   | {
@@ -31,6 +29,6 @@ export default class MemorySessionStore extends CacheBasedSessionStore
 
     const cache = new MemoryCacheStore(maxSize);
 
-    super(cache, expiresIn || MINUTES_IN_ONE_YEAR);
+    super(cache, expiresIn);
   }
 }
diff --git a/packages/bottender/src/session/MongoSessionStore.ts b/packages/bottender/src/session/MongoSessionStore.ts
index dd001652..f12bd6be 100644
--- a/packages/bottender/src/session/MongoSessionStore.ts
+++ b/packages/bottender/src/session/MongoSessionStore.ts
@@ -5,8 +5,6 @@ import { Collection, Db, MongoClient } from 'mongodb';
 import Session from './Session';
 import SessionStore from './SessionStore';
 
-const MINUTES_IN_ONE_YEAR = 365 * 24 * 60;
-
 type MongoOption =
   | string
   | {
@@ -32,7 +30,7 @@ export default class MongoSessionStore implements SessionStore {
       this._url = options.url;
       this._collectionName = options.collectionName || 'sessions';
     }
-    this._expiresIn = expiresIn || MINUTES_IN_ONE_YEAR;
+    this._expiresIn = expiresIn || 0;
   }
 
   async init(): Promise<MongoSessionStore> {
@@ -89,6 +87,10 @@ export default class MongoSessionStore implements SessionStore {
   }
 
   _expired(sess: Session): boolean {
+    if (!this._expiresIn) {
+      return false;
+    }
+
     return (
       sess.lastActivity !== undefined &&
       isBefore(sess.lastActivity, subMinutes(Date.now(), this._expiresIn))
diff --git a/packages/bottender/src/session/RedisSessionStore.ts b/packages/bottender/src/session/RedisSessionStore.ts
index deacc5e0..32dc6e00 100644
--- a/packages/bottender/src/session/RedisSessionStore.ts
+++ b/packages/bottender/src/session/RedisSessionStore.ts
@@ -3,8 +3,6 @@ import RedisCacheStore from '../cache/RedisCacheStore';
 import CacheBasedSessionStore from './CacheBasedSessionStore';
 import SessionStore from './SessionStore';
 
-const MINUTES_IN_ONE_YEAR = 365 * 24 * 60;
-
 type RedisOption =
   | number
   | string
@@ -20,6 +18,6 @@ export default class RedisSessionStore extends CacheBasedSessionStore
   implements SessionStore {
   constructor(arg: RedisOption, expiresIn?: number) {
     const cache = new RedisCacheStore(arg);
-    super(cache, expiresIn || MINUTES_IN_ONE_YEAR);
+    super(cache, expiresIn);
   }
 }


Test patch:
diff --git a/packages/bottender/src/cache/__tests__/MemoryCacheStore.spec.ts b/packages/bottender/src/cache/__tests__/MemoryCacheStore.spec.ts
index 836509ec..8f5d920a 100644
--- a/packages/bottender/src/cache/__tests__/MemoryCacheStore.spec.ts
+++ b/packages/bottender/src/cache/__tests__/MemoryCacheStore.spec.ts
@@ -5,6 +5,7 @@ describe('#get', () => {
     const store = new MemoryCacheStore(5);
 
     await store.put('x', 'abc', 5);
+
     expect(await store.get('x')).toBe('abc');
   });
 
@@ -65,6 +66,22 @@ describe('#put', () => {
     Date.now = _now;
   });
 
+  it('should store cache item until it has been removed by LRU', async () => {
+    const _now = Date.now;
+    Date.now = jest.fn(() => 1234567891011);
+
+    const store = new MemoryCacheStore(5);
+
+    await store.put('x', 1, 0);
+    expect(await store.get('x')).toBe(1);
+
+    const now = Date.now();
+    Date.now = jest.fn(() => now + 6 * 60 * 1000);
+
+    expect(await store.get('x')).toBe(1);
+    Date.now = _now;
+  });
+
   it('can store mixed data types', async () => {
     const store = new MemoryCacheStore(5);
 
diff --git a/packages/bottender/src/cache/__tests__/RedisCacheStore.spec.ts b/packages/bottender/src/cache/__tests__/RedisCacheStore.spec.ts
index d15aae8c..70249928 100644
--- a/packages/bottender/src/cache/__tests__/RedisCacheStore.spec.ts
+++ b/packages/bottender/src/cache/__tests__/RedisCacheStore.spec.ts
@@ -114,6 +114,15 @@ describe('#put', () => {
     expect(redis.setex).toBeCalledWith('123', 300, '"xyz"');
   });
 
+  it('should store cache item', async () => {
+    const store = new RedisCacheStore();
+    const redis = store.getRedis();
+
+    await store.put('123', 'xyz', 0);
+
+    expect(redis.set).toBeCalledWith('123', '"xyz"');
+  });
+
   it('can store mixed data types', async () => {
     const store = new RedisCacheStore();
     const redis = store.getRedis();