- 2023.07.31
- 勤怠管理システム『きんたくん』の単体テストとGitHub Actionsでの機密情報の扱い
きんたくん is 何?
おはようございます~!㉔です!
先日の帰社日では漫才風の技術勉強会を実施し、好評を頂きました^^
これからも学びやすい勉強会を企画していきたいと思います!きんたくんはAz One自社開発の勤怠管理システムです。詳しくは以前の記事で紹介しているので前回の技術ブログをご参照ください。
きんたくんのテスト環境
テストは堅牢なシステムを実装する上で欠かせません。テストの必要性、粒度に関する議論が尽きることはありませんが、少なくとも自動テストの無い開発環境は避けたいところです。
単体テストツール
今回はVitestを採用しています。熟成されてエコシステムの揃っているJestと迷いましたが、きんたくんは社内システムであり新規技術の採用に向いているPJですので知見の少ないVitestを採用しました。
テストのトリガー
単体テストはリポジトリのpushをトリガーとしてGitHub Actionsを利用して実施しています。きんたくんは当初3人で開発を実施していましたが(現在1人で保守しています)、Pull Requestはテストをパスしないとマージできないように制御していたため、複数人の開発でもデグレもなく安定した開発となりました。
テストの方針
今回の単体テストの方針は以下としました。
- DiscordのAPIに依存する部分はテストしない
- ビジネスロジックに関する部分を単体テストを実装する
メッセージの送信等、
Discord API
を利用する部分をテストするならMock Service Workerを使用してリクエストをインターセプトすることになりますが、きんたくんはDiscord APIのレスポンスはロジックに関わる部分がほぼない状況です。そのため今回はビジネスロジックのみで単体テストを実装するようにしました。Google Apiのテスト
前回の記事で触れましたが、きんたくんはデータの永続化にGoogle Sheetを使用しています。そのためCRUDにあたる機能を自前で実装しています。(辛い)データの書き込みやシートの新規作成などはデータの安全な管理に直結するためテスト対象としたいところです。
機密情報の扱い
Google APIのテストを実行する場合、クライアントIDとクライアントシークレットが保存されたOAuth認証用のファイルを利用することでGitHub Actionsのような
ブラウザを持たない仮想マシン
でのAPIテストを実施できます。ただし、当然リポジトリに機密情報となるクライアントIDとクライアントシークレットを含めることはできないため、GitHub Actions内でこれら機密情報を安全に利用する方法が必要です。GitHub Actionsのお作法
単純に考えればリポジトリの
Secrets
にクライアントIDとクライアントシークレットを保存し、下記のようにファイルを生成してしまえばいいように思います。echo -n "${PASSWORD}" > password.txt
ただ、これは公式のお墨付きで避けるべき方法の一つです。
Avoid passing secrets between processes from the command line, whenever possible. Command-line processes may be visible to other users (using the
ps
command) or captured by security audit events. To help protect secrets, consider using environment variables,STDIN
, or other mechanisms supported by the target process.引数に
"${PASSWORD}"
などを使うとシェルが展開してからプロセスを開始するので、ps
などで見えてしまうからというのが理由のようです。$ echo "${PASSWORD}" # SDf%@m38x"Tc5N4r $ echo "sleep 10" > long.sh $ bash ./long.sh "${PASSWORD}" & # [1] 2364315 $ pgrep -af long # 2364315 bash ./long.sh SDf%@m38x"Tc5N4r
GitHub Actionsでの機密情報に関するあれこれはこの記事で詳しく検証されていました。
Actionを利用する
今回のようなSecretsに保存した変数を利用して認証用のファイルを生成するために利用できるActionが提供されています。
この用に指定することでプロセス内でSecretsに登録したデータを安全に任意のファイル形式に変換することが可能です。
# .github/workflows/var-substitution.yml on: [push] name: variable substitution in json, xml, and yml files jobs: build: runs-on: windows-latest steps: - uses: actions/checkout@v3 - uses: keeroll/variable-substitution@v2.0.2 with: files: 'Application/*.json, Application/*.yaml, ./Application/SampleWebApplication/We*.config' env: Var1: "value1" Var2.key1: "value2" SECRET: ${{ secrets.SOME_SECRET }}
実際のテスト
Google APIのテストのサンプルとしては以下のような感じになります。
beforeAll
、afterAll
で任意のテストシートの作成、破棄をすることで不要なファイルをDriveに残すことなくテストを実施可能です。// テスト用シート生成 beforeAll(async () => { googleAuth = (await authorize()) as OAuth2Client spreadSheetId = await createNewSpreadSheet(googleAuth, 'sample') }) // テスト用シート削除 afterAll(async () => { if (!spreadSheetId) return await deleteFileOrFolder(googleAuth, spreadSheetId) }) describe('シート操作', async () => { it('データ更新', async () => { expect(googleAuth).not.toBeUndefined() expect(spreadSheetId).not.toBeUndefined() if (!googleAuth || !spreadSheetId) return await updateValues(googleAuth, spreadSheetId, 'A1', [['test-data']], 'RAW') const values = await collectValues(googleAuth, spreadSheetId, 'A1') assert.notEqual(values, undefined) assert.notEqual(values, null) if (!values) return assert.equal(values.length, 1) assert.equal(values[0].length, 1) assert.equal(values[0][0], 'test-data') }) })
最後に
今回はテストについて紹介しました。きんたくんでは自動でのe2eテストは実施していません。この記事を書くに当たって多少調査しましたが、Discord Botに関しては正直メジャーなものは無い気がします。きんたくんはSapphireというNode.js環境のFWを採用しているため、採用できる候補としてはcordeというOSSがあるようです。
次回はお待ちかね?のCopilotについてです。有名なTech系の企業の方がまとめている資料などを交えてきんたくんの実装で利用した所感をお伝えしたいと思います。