Open22

LINE Messaging APIを使ってGoogleカレンダーの予定をリマインドする

Yuma ItoYuma Ito

LINE Messaging APIの利用を開始する

LINE Messaging APIを利用するためにまずは公式アカウントを作成する必要がある。

  1. https://account.line.biz/signup?redirectUri=https://entry.line.biz/form/entry/unverified にアクセスしてLINE Business IDに登録する(未登録の方のみ)
  2. フォームに入力してLINE公式アカウントを作成する。
  3. LINE Official Account Manager にログインすると、作成した公式アカウントが表示されている。
  4. アカウントをクリックすると以下のような管理画面が表示される。
  5. 「設定>Messaging API」からMessaging APIの利用を開始する
  6. 今度はLINE Developers Consoleにログインして、プロバイダーを選択すると、新しく追加した公式アカウントのチャネルが表示されている。

(補足)
以前はDevelopers Consoleからチャネル(Botのこと)を作成可能であったができなくなってしまった。(参考:2024年9月4日をもってLINE DevelopersコンソールからMessaging APIチャネルを直接作成することはできなくなりました | LINE Developers

Yuma ItoYuma Ito

チャネルアクセストークンv2.1の発行

環境情報は以下。

  • Node.js v22.11.0
  • Bun: 1.1.34

前回と同様にアサーション署名キーのキーペアはブラウザのコンソールで作成した。
参考:ブラウザでキーペアを生成する - チャネルアクセストークンv2.1を発行する | LINE Developers

次に環境変数を設定するための .envファイルを作り、以下のように記載する。

.env
CHANNEL_ID=xxxxx
KID=xxxxxx
  • CHANNEL_ID:「チャネル基本設定>チャネルID」に記載されているID
  • KID:アサーション署名キーの公開鍵をDevelopers Console経由で登録した際に発行されるアクセスキー。「チャネル基本設定>アサーション署名キー」から取得できる。

そして、チャネルアクセストークンを生成するためのスクリプトmake_token.jsを以下のように用意する。

script/make_token.js
import * as jose from "jose";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const makeJWT = async () => {
  const privateKey = JSON.parse(
    fs.readFileSync(path.join(__dirname, "assertion-private.key"))
  );

  const header = {
    alg: "RS256",
    typ: "JWT",
    kid: process.env.KID, // チャネル基本設定>アサーション署名キー
  };

  const payload = {
    iss: process.env.CHANNEL_ID, // チャネルID
    sub: process.env.CHANNEL_ID, // チャネルID
    aud: "https://api.line.me/",
    exp: Math.floor(new Date().getTime() / 1000) + 60 * 25, // JWTの有効期間(UNIX時間)
    token_exp: 60 * 60 * 24 * 30, // チャネルアクセストークンの有効期間
  };

  // JWTの生成
  return new jose.SignJWT(payload).setProtectedHeader(header).sign(privateKey);
};

const createToken = async (jwt) => {
  const accessTokenUrl = "https://api.line.me/oauth2/v2.1/token";
  const response = await fetch(accessTokenUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_assertion_type:
        "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      client_assertion: jwt,
    }).toString(),
  });

  return response;
};

(async () => {
  const jwt = await makeJWT();
  const accessTokenResponse = await createToken(jwt);
  console.log(await accessTokenResponse.json());
})();

準備が整ったら以下のコマンドでチャネルアクセストークンを発行する。

(Bunで実行する場合)

❯ bun run scripts/make_token.js
{
  access_token: 'eyJhbGxxxxxxx',
  token_type: 'Bearer',
  expires_in: 2592000,
  key_id: 'zFHuYHxxxxxxx'
}

(Node.jsで実行する場合)

❯ node --env-file=.env scripts/make_token.js
{
  access_token: 'eyJhbGxxxxxxx',
  token_type: 'Bearer',
  expires_in: 2592000,
  key_id: 'zFHuYHxxxxxxx'
}

前回からの変更点

作業としては以上だが、JWTを生成する際に前回の方法からいくつか変更した。
(前回の方法:JWTを生成する - LINE Messaging APIを使ってオウム返しbotを作成する (Cloud Functions for Firebase 環境)

変更点1:dotenvに依存しない

ではなく、Node.jsの実行時に--env-fileオプションを指定する

前回はdotenvパッケージを利用して環境変数を読み込んでいた。
しかし今回はランタイム環境をBunに変更したため、自動的に環境変数の読み込みが可能になった。そのため、dotenvは除外することができる。
よって、以下のように実行すれば良い

bun run scripts/make_token.js 

もしくは、Node.js環境においても--env-fileオプションをつければ環境変数を読み込むことができる(Node.js v20.6.0以上の場合)。
参考:Node.js の進化に伴い不要となったかもしれないパッケージたち

以下のように実行する。

node --env-file=.env scripts/make_token.js    

変更点2:JWT生成パッケージをnode-joseからjoseに変更

前回はドキュメントに従ってnode-joseを利用してJWTを生成していた。

今回はESModule形式のスクリプトファイルに変更したが、node-joseはESModuleに対応していなかった。
そこで、ESModuleに対応しているjoseに変更した。
JWT生成のインターフェースも変更された。

new jose.SignJWT(payload).setProtectedHeader(header).sign(privateKey);

スクリプトファイルをESModuleに変更した理由は、パッケージマネージャーをBunに変更しbun initをした際にtype: moduleが指定されていたため。

Yuma ItoYuma Ito

AWS SAM の構築

今回はバックエンドサーバーにAWS Lambdaを用いることにする。
AWS Lambdaの構築には、AWS SAM (AWS Serveless Application Model) で構築する。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html

Yuma ItoYuma Ito

SAMの事前準備

SAMを使い始めるためにいくつかの設定が必要である。
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/prerequisites.html

IAM Identity Center管理下のユーザー

SAM CLIを使ってAWSのリソースをデプロイするためには、IAMのクレデンシャルが必要になる。
AWSが推奨している方法はIAM Identity CenterというIAMを管理できるサービスである。

従来方法であるIAMユーザーが発行したクレデンシャルを用いる方法は非推奨となっている。アクセスキーなどのクレデンシャルが長期的に有効となってしまうからである。

公式ドキュメントや下記のブログを参考にしてIAM Identity Center経由でログインするユーザーを作成する。
https://zenn.dev/murakami_koki/articles/79ac2456564b36

AWS CLIの設定

AWS CLIはすでにインストールしていたが、アップデートした。

aws --version
aws-cli/2.22.26 Python/3.12.6 Darwin/23.6.0 exe/x86_64

下記のドキュメントの通りにCLIを利用できるように設定する。
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html

AWS SAM CLIの設定

下記に従ってAWS SAM CLIをインストールする。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/install-sam-cli.html

sam --version
SAM CLI, version 1.132.0
Yuma ItoYuma Ito

AWS SAMアプリケーションの作成

アプリケーションの初期化

sam initコマンドを実行して、ダイアログに沿ってAWS SAMアプリケーションを作成する。

sam init
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Data processing
        3 - Hello World Example with Powertools for AWS Lambda
        4 - Multi-step workflow
        5 - Scheduled task
        6 - Standalone function
        7 - Serverless API
        8 - Infrastructure event management
        9 - Lambda Response Streaming
        10 - GraphQLApi Hello World Example
        11 - Full Stack
        12 - Lambda EFS example
        13 - Serverless Connector Hello World Example
        14 - Multi-step workflow with Connectors
        15 - DynamoDB Example
        16 - Machine Learning
Template: 5

Which runtime would you like to use?
        1 - dotnet8
        2 - dotnet6
        3 - nodejs22.x
        4 - nodejs20.x
        5 - nodejs18.x
Runtime: 3

Based on your selections, the only Package type available is Zip.
We will proceed to selecting the Package type as Zip.

Based on your selections, the only dependency manager available is npm.
We will proceed copying the template using npm.

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: N

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: y
AppInsights monitoring may incur additional cost. View https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/appinsights-what-is.html#appinsights-pricing for more details

Would you like to set Structured Logging in JSON format on your Lambda functions?  [y/N]: y
Structured Logging in JSON format might incur an additional cost. View https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-pricing for more details

Project name [sam-app]: schedule-line-reminder
                                                                                                                       
Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)                              

    -----------------------
    Generating application:
    -----------------------
    Name: schedule-line-reminder
    Runtime: nodejs22.x
    Architectures: x86_64
    Dependency Manager: npm
    Application Template: quick-start-cloudwatch-events
    Output Directory: .
    Configuration file: schedule-line-reminder/samconfig.toml
    
    Next steps can be found in the README file at schedule-line-reminder/README.md
        

Commands you can use next
=========================
[*] Create pipeline: cd schedule-line-reminder && sam pipeline init --bootstrap
[*] Validate SAM template: cd schedule-line-reminder && sam validate
[*] Test Function in the Cloud: cd schedule-line-reminder && sam sync --stack-name {stack-name} --watch

プロジェクト直下で実行したら、schedule-line-reminder/schedule-line-reminderのようになってしまったので、生成されたファイルをプロジェクト直下に移動した。

ビルド

下記コマンドを実行してビルドする。

sam build

.aws-sam/build配下にビルドアーティファクトが生成されればOK。

Yuma ItoYuma Ito

アプリケーションのデプロイ

ビルドが完了したら下記コマンドでアプリケーションをデプロイする。
sam deployコマンドではtemplate.ymlをCloudFormationの形式に変換し、AWSリソースをデプロイする。

sam deploy --guided --profile <profile-name>
  • sam deploy:AWS SAMアプリケーションをデプロイする。
  • --guided:各種設定値をインタラクティブに設定する。
  • --profile:AWS CLIで設定したプロファイル。(~/.aws/configに定義されている)

なお、途中に出てくるSave arguments to configuration fileという設定をY(es)にすると、samconfig.tomlに設定値が反映されるため、次回からは以下のコマンドでデプロイできる。

sam deploy

下記のように設定してデプロイした。

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Found
        Reading default arguments  :  Success

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [schedule-line-reminder]: 
        AWS Region [ap-northeast-1]: 
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [Y/n]: Y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: Y
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]: N
        Save arguments to configuration file [Y/n]: Y
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: 

        Looking for resources needed for deployment:
        Creating the required resources...
        Successfully created!

        Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xxxxxxx
        A different default S3 bucket can be set in samconfig.toml and auto resolution of buckets turned off by setting resolve_s3=False
                                                                                                                       
        Parameter "stack_name=schedule-line-reminder" in [default.deploy.parameters] is defined as a global parameter  
[default.global.parameters].                                                                                           
        This parameter will be only saved under [default.global.parameters] in                                         
/Users/yuma.ito/repositories/personal/schedule-line-reminder/samconfig.toml.                                           

        Saved arguments to config file
        Running 'sam deploy' for future deployments will use the parameters saved above.
        The above parameters can be changed by modifying samconfig.toml
        Learn more about samconfig.toml syntax at 
        https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

        Uploading to schedule-line-reminder/xxxxxxxxxx  4599410 / 4599410  (100.00%)

        Deploying with following values
        ===============================
        Stack name                   : schedule-line-reminder
        Region                       : ap-northeast-1
        Confirm changeset            : True
        Disable rollback             : False
        Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-xxxxxxx
        Capabilities                 : ["CAPABILITY_IAM"]
        Parameter overrides          : {}
        Signing Profiles             : {}

Initiating deployment
=====================

        Uploading to schedule-line-reminder/xxxxxxxxxx.template  1412 / 1412  (100.00%)


Waiting for changeset to be created..

CloudFormation stack changeset
-----------------------------------------------------------------------------------------------------------------
Operation                    LogicalResourceId            ResourceType                 Replacement                
-----------------------------------------------------------------------------------------------------------------
+ Add                        ApplicationInsightsMonitor   AWS::ApplicationInsights::   N/A                        
                             ing                          Application                                             
+ Add                        ApplicationResourceGroup     AWS::ResourceGroups::Group   N/A                        
+ Add                        ScheduledEventLoggerCloudW   AWS::Lambda::Permission      N/A                        
                             atchEventPermission                                                                  
+ Add                        ScheduledEventLoggerCloudW   AWS::Events::Rule            N/A                        
                             atchEvent                                                                            
+ Add                        ScheduledEventLoggerRole     AWS::IAM::Role               N/A                        
+ Add                        ScheduledEventLogger         AWS::Lambda::Function        N/A                        
-----------------------------------------------------------------------------------------------------------------


Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:xxxxxxxxx:changeSet/samcli-deployxxxxxxx/a9324f2b-xxxxxxxxxxxx


Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

2025-01-01 19:40:10 - Waiting for stack create/update to complete

CloudFormation events from stack operations (refresh every 5.0 seconds)
-----------------------------------------------------------------------------------------------------------------
ResourceStatus               ResourceType                 LogicalResourceId            ResourceStatusReason       
-----------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS           AWS::CloudFormation::Stack   schedule-line-reminder       User Initiated             
CREATE_IN_PROGRESS           AWS::IAM::Role               ScheduledEventLoggerRole     -                          
CREATE_IN_PROGRESS           AWS::ResourceGroups::Group   ApplicationResourceGroup     -                          
CREATE_IN_PROGRESS           AWS::ResourceGroups::Group   ApplicationResourceGroup     Resource creation          
                                                                                       Initiated                  
CREATE_IN_PROGRESS           AWS::IAM::Role               ScheduledEventLoggerRole     Resource creation          
                                                                                       Initiated                  
CREATE_COMPLETE              AWS::ResourceGroups::Group   ApplicationResourceGroup     -                          
CREATE_IN_PROGRESS           AWS::ApplicationInsights::   ApplicationInsightsMonitor   -                          
                             Application                  ing                                                     
CREATE_IN_PROGRESS           AWS::ApplicationInsights::   ApplicationInsightsMonitor   Resource creation          
                             Application                  ing                          Initiated                  
CREATE_COMPLETE              AWS::IAM::Role               ScheduledEventLoggerRole     -                          
CREATE_IN_PROGRESS           AWS::Lambda::Function        ScheduledEventLogger         -                          
CREATE_IN_PROGRESS           AWS::Lambda::Function        ScheduledEventLogger         Resource creation          
                                                                                       Initiated                  
CREATE_IN_PROGRESS -         AWS::Lambda::Function        ScheduledEventLogger         Eventual consistency check 
CONFIGURATION_COMPLETE                                                                 initiated                  
CREATE_IN_PROGRESS           AWS::Events::Rule            ScheduledEventLoggerCloudW   -                          
                                                          atchEvent                                               
CREATE_IN_PROGRESS           AWS::Events::Rule            ScheduledEventLoggerCloudW   Resource creation          
                                                          atchEvent                    Initiated                  
CREATE_IN_PROGRESS -         AWS::ApplicationInsights::   ApplicationInsightsMonitor   Eventual consistency check 
CONFIGURATION_COMPLETE       Application                  ing                          initiated                  
CREATE_COMPLETE              AWS::Lambda::Function        ScheduledEventLogger         -                          
CREATE_COMPLETE              AWS::ApplicationInsights::   ApplicationInsightsMonitor   -                          
                             Application                  ing                                                     
CREATE_IN_PROGRESS -         AWS::Events::Rule            ScheduledEventLoggerCloudW   Eventual consistency check 
CONFIGURATION_COMPLETE                                    atchEvent                    initiated                  
CREATE_IN_PROGRESS           AWS::Lambda::Permission      ScheduledEventLoggerCloudW   -                          
                                                          atchEventPermission                                     
CREATE_IN_PROGRESS           AWS::Lambda::Permission      ScheduledEventLoggerCloudW   Resource creation          
                                                          atchEventPermission          Initiated                  
CREATE_COMPLETE              AWS::Lambda::Permission      ScheduledEventLoggerCloudW   -                          
                                                          atchEventPermission                                     
CREATE_COMPLETE              AWS::Events::Rule            ScheduledEventLoggerCloudW   -                          
                                                          atchEvent                                               
CREATE_COMPLETE              AWS::CloudFormation::Stack   schedule-line-reminder       -                          
-----------------------------------------------------------------------------------------------------------------


Successfully created/updated stack - schedule-line-reminder in ap-northeast-1
Yuma ItoYuma Ito

トラブルシューティング

トークンの有効期限切れ

samコマンド実行時に以下のエラーになった。

Error: Failed to complete an API call to fetch stack information for schedule-line-reminder: Error when retrieving token from sso: Token has expired and refresh failed

その場合、SSOで再認証すれば良い。

aws sso login --profile <profile-name>
Yuma ItoYuma Ito

Lambda関数でパラメータストアを利用する

Google APIのクライアントシークレット情報は秘匿情報のため安全にで管理したい。
そのため、秘匿情報はAWS SSMのパラメータストアに保存し、Lambda関数でパラメータストアから秘匿情報を取得できるようにする。

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/ps-integration-lambda-extensions.html
https://zenn.dev/k_saito7/articles/aws-lambda-ssm-layer

Yuma ItoYuma Ito

Google Calendar API

Googleカレンダーの情報を取得するためにはGoogle Calendar APIを利用する。

https://developers.google.com/calendar/api/guides/overview?hl=ja

Yuma ItoYuma Ito

APIの認証

Google Calendar APIを利用する際にOAuth 2.0によってAPIの利用を許可する。

https://developers.google.com/workspace/guides/auth-overview?hl=ja

OAuth 2.0の利用設定

  1. Google Cloudプロジェクトの作成
  2. Google Workspace API の有効化
  3. 「OAuth同意画面」に遷移する
    • 同意画面が変更される表示がされていたので、リンク先に遷移する
  4. Google Auth Platformの画面に遷移した
  5. 「開始」をクリックして画面に従って設定
    • アプリ名:Schedule LINE Reminder
    • ユーザーの種類:外部
      • テストユーザー:自分のメールアドレス
    • ユーザーサポートメール:自分のメールアドレス
  6. 「データアクセス」画面からスコープを設定する
  7. 「クライアント」画面からOAuth 2.0クライアントを作成
  8. 生成したOAuth 2.0クライアントのクライアントシークレット(JSONファイル)をダウンロード
    • 絶対にGitリポジトリにコミットしないこと!
Yuma ItoYuma Ito

ライブラリの選定

Google Calendar APIを利用するためのNode.js用認証ライブラリはいくつか公開されている模様。

今回はGoogle Calendar APIを呼び出すため、googleapis/google-api-nodejs-clientを利用する。

インストール

Google Calendar API用のライブラリのみ追加する。

bun install @googleapis/calendar

Google API ClientのREADME (OAuth2 client の節) やsamples/oauth2.jsを見ながらOAuthでアクセス許可するスクリプトを書く。

https://github.com/yuma-ito-bd/schedule-line-reminder/blob/1f42677163cb111941d571cd1eae8811e005d3d0/scripts/google-authenticate.ts

Yuma ItoYuma Ito

アクセストークンとリフレッシュトークンの取得

作成したスクリプトを実行する。

bun run scripts/google-authenticate.ts  

実行すると認可用のURLがコンソールに出力されるのでブラウザでアクセスする。

Authorize this app by visiting this URL: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.calendarlist.readonly&include_granted_scopes=true&response_type=code&client_id=xxxxxxxxx.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth2callback

まだ開発中のアプリケーションなので下記のような警告が表示される。

「続行」をクリックすると、アクセス許可の画面が表示される。

許可すると以下のようにサーバーからのレスポンスが表示される。

コンソールにはアクセストークンとリフレッシュトークンが出力される。

{
  access_token: "xxxxxxxxx",
  refresh_token: "xxxxxxxxx",
}

これらのアクセストークンとリフレッシュトークンを使うことでGoogle APIにアクセスすることができる。

Yuma ItoYuma Ito

Dynamic require of "child_process" is not supportedエラー

Lambdaにデプロイして実行してみると以下のエラーになった。

{
  "errorType": "Error",
  "errorMessage": "Dynamic require of \"child_process\" is not supported",
  "trace": [
    "Error: Dynamic require of \"child_process\" is not supported",
    "    at file:///var/task/calendar-events-handler.mjs:11:9",
    "    at node_modules/google-auth-library/build/src/auth/googleauth.js (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/node_modules/google-auth-library/build/src/auth/googleauth.js:29:25)",
    "    at __require2 (file:///var/task/calendar-events-handler.mjs:17:50)",
    "    at node_modules/google-auth-library/build/src/index.js (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/node_modules/google-auth-library/build/src/index.js:17:22)",
    "    at __require2 (file:///var/task/calendar-events-handler.mjs:17:50)",
    "    at node_modules/googleapis-common/build/src/index.js (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/node_modules/googleapis-common/build/src/index.js:16:29)",
    "    at __require2 (file:///var/task/calendar-events-handler.mjs:17:50)",
    "    at node_modules/@googleapis/calendar/build/index.js (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/node_modules/@googleapis/calendar/index.ts:16:1)",
    "    at __require2 (file:///var/task/calendar-events-handler.mjs:17:50)",
    "    at <anonymous> (/private/var/folders/65/dbkvg0pd4kvc6jsv_fb8cmw80000gp/T/tmpixckb84_/src/google-calendar-api-adapter.ts:1:39)"
  ]
}
Yuma ItoYuma Ito

原因

どうやらCommonJSのパッケージとESModuleのパッケージが混ざっているとesbuildでうまくトランスパイルできないらしい。

https://dev.to/marcogrcr/nodejs-and-esbuild-beware-of-mixing-cjs-and-esm-493n
https://github.com/evanw/esbuild/issues/1921

解決方法

以下のようにBannerというプロパティを追加して、ESModuleに対応できるようにしたらうまくいった。

template.yml
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Format: esm
        Minify: false
        OutExtension:
          - .js=.mjs
        Target: "es2020"
        Sourcemap: true
        EntryPoints:
          - src/handlers/calendar-events-handler.ts
+       Banner:
+         - js=import { createRequire } from 'module'; const require = createRequire(import.meta.url);

なお、Bannerというプロパティはドキュメントには記載されていない。

https://stackoverflow.com/questions/77260658/sam-with-esbuild-mjs-file-failing-with-dynamic-require-of-net-is-not-supported

Yuma ItoYuma Ito

mock.moduleの設定が他のテストに影響する

Bunでは mock.moduleによって他のモジュールをモックすることができる。

https://bun.sh/docs/test/mocks#module-mocks-with-mock-module

しかし、この設定を行うと他のテストファイルの実行に影響してしまった。

例えば、a.tsからb.tsをimportしていたとする。
b.tsでは文字列"b"を返却する関数 b をexportしており、a.tsではb関数を呼び出して値を返却する関数aをexportしている。

b.ts
export const b = () => {
  return "b";
};
a.ts
import { b } from "./b";

export const a = () => {
  return b();
};

ここで2種類のテストファイルを用意する。

モックの設定はせず、b関数の値が返却されるか確認する a1.test.tsファイル

a1.test.ts
import { describe, it, expect } from "bun:test";
import { a } from "./a";

describe("a1", () => {
  it("should return b()", () => {
    expect(a()).toBe("b");
  });
});

mock.moduleを使って、b.tsからexportされる関数をモック化して値が返却されるか確認するテストファイル

a2.test.ts
import { describe, it, expect, mock } from "bun:test";
import { a } from "./a";

describe("a2", () => {
  it("should return b()", () => {
    mock.module("./b", () => {
      return {
        b: () => "dummy",
      };
    });

    expect(a()).toBe("dummy");
  });
});

ここで、テストを走らせると失敗してしまう。

$ bun test
bun test v1.1.34 (5e5e7c60)

a2.test.ts:
✓ a2 > should return b() [0.03ms]

a1.test.ts:
1 | import { describe, it, expect, mock } from "bun:test";
2 | import { a } from "./a";
3 | 
4 | describe("a1", () => {
5 |   it("should return b()", () => {
6 |     expect(a()).toBe("b");
                    ^
error: expect(received).toBe(expected)

Expected: "b"
Received: "dummy"

      at /Users/***/schedule-line-reminder/a1.test.ts:6:17
✗ a1 > should return b() [0.33ms]

 1 pass
 1 fail
 2 expect() calls
Ran 2 tests across 2 files. [12.00ms]

a1.test.tsの実行で"b"を期待しているのに"dummy"が返却されてしまっている。
この原因は、先に a2.test.tsが実行されmock.moduleが実行される(=dummyが返却される関数になる)と、後続のテストa1.test.tsにもその設定が引き継がれてしまうからである。

ちなみに、個別に実行するとテストは通る。

$ bun test a1.test.ts
bun test v1.1.34 (5e5e7c60)

a1.test.ts:
✓ a1 > should return b() [0.55ms]

 1 pass
 0 fail
 1 expect() calls
Yuma ItoYuma Ito

この問題は下記のissueにも挙げられている。

https://github.com/oven-sh/bun/issues/7823

このissueでは以下の方法が紹介されていた。

Yuma ItoYuma Ito

自分が行った解決方法は、依存している関数(クラス)を外部から注入することでmock.moduleを利用しないように変更した。
↓コミット
https://github.com/yuma-ito-bd/schedule-line-reminder/pull/5/commits/2092edf7d612a5273cbcc6aee255248047d40d81?diff=split&w=0

先程の例で説明すると、a.tsを以下のように書き換える。

a_new.ts
export const a = (func: () => string) => {
  return func();
};
a_new.test.ts
import { describe, it, expect } from "bun:test";
import { a } from "./a_new";

describe("a", () => {
  it("should return func()", () => {
    const dummyFunction = () => "dummy";

    const result = a(dummyFunction);
    expect(result).toBe("dummy");
  });
});
Yuma ItoYuma Ito

テストが書きづらいときは設計を見直した方が良いですね。