msal.js v2.0.1 をAngularで使用する
はじめに
今回の記事で使用するコードは下記の構成で実装しています。
記事を参照されるタイミングによっては動作しない可能性がありますのでご留意ください。
- @angular/cli:10.0.5
- @azure/msal-browser:2.0.1
msal.js v2.0.0がリリース
先月の末頃(2020年7月21日)にmsal.jsのv2.0.0 がリリースされました。
(色々試している間にv2.0.1がリリースされちゃいましたが…
PKCEの対応が行われ、よりかんたんにセキュアに使用することができるようになります。
詳細なお話は公式のドキュメントや、今月の.NETラボを見ていただくとして…
ひとまず、僕がメインで使用しているAngularでの使用方法を確認していこうと思います。
@azure/msal-angular
msal.jsには派生ライブラリとして、Angular対応された@azure/msal-angular
というライブラリが存在します。
ServiceやInterceptorが提供され、Angularで使用するのに便利なライブラリとなっています。
👆microsoft-authentication-library-for-jsより抜粋
上図の通り、@azure/msal-angularもv2.0がPKCE対応されるはずなのですが、まだリリースされていないようです。
Roadmapにも特に予定は明記されていないようですが
何はともあれ使用したいので、プレーンな@azure/msal-browser
をAngularで使用してみましょう。
@azure/msal-browserをAngularで使用する。
基本的な使い方は@azure/msal-browserのドキュメントの通りで問題ありません。
ライブラリのインポート
npm install @azure/msal-browser
msalアプリケーションインスタンスの生成
msalのv1からmsalの処理のエントリークラスがUserAgentApplication
からPublicClientApplication
に変更されています。
インスタンスの生成にADアプリなどの設定のConfiguration
を与えるあたりの使い方は変わらないようです。
Configurationの構成自体も大きく変わっていないようで、clientId
やauthority
などしか項目を使っていない場合は設定記述部分は変更する必要はありません。
handleRedirectCallback
loginRedirect
などを行った場合に引っ掛けるhandleRedirectCallback
は大きく変更されています。
Promise
で実行されるようになっておりそれに合わせた変更が必要です。
const res = await this.clientApp.handleRedirectPromise().catch(err => { console.log({err}); });
自分は検証でエラーをトラップしたかったので雑に👆の実装にしていますが、resがvoid | AuthenticationResult
となるのでthen
を使うのもありです。
handleRedirectPromise
の内部処理実行前にloginRedirect
処理などが走るとエラーが発生するので
handleRedirectPromise
の処理終了を待機して、処理終了後にログイン処理を行うのがベターです。
login
loginを行う処理のインターフェースは変わっていません。loginRedirect
、loginPopup
でログインを実行します。
Redirectは前述するCallbackで、PopupはPromiseで結果を取得します。
ユーザー情報の取得
msal.jsのv1ではユーザー情報を取得するインタフェースがClientApp上に用意されていましたが、v2でなくなりました。
msal.jsのv2では、ログインやトークン取得時の結果としてアカウント情報が含まれた状態で返却されるので
そこからデータを取得することになります。
リダイレクトの場合は下記のようにhandleRedirectPromise
で取得できたレスポンスから抽出することになります。
const res = await this.clientApp.handleRedirectPromise().catch(err => { console.log({err}); }); this.account = (res as AuthenticationResult).account;
PopupはloginPopup
の返却値です。
アクセストークンの取得
acquireTokenSilent
で必要な引数にアカウント情報のパラメータが追加されています。
const res = await this.clientApp.acquireTokenSilent({ scopes: this.scopes, account: this.account }); return res.accessToken;
ログイン時などにアカウント情報は取得し、プライベートな変数なりに格納しておいたほうが良さそうです。
ここまでの実装はAngular特有というものではなく、普通の@azure/msal-browserの使用の仕方ですね。
本家のmsalServiceよろしく、一つのServiceクラスに👆までの処理を実装しておくと使いやすいと思います。
Interceptorの作成
Angularのサービスらしく。ということでInterceptorを作成し、WebAPIへの通信時に自動的にヘッダにトークンを付与させます。
ここは@azure/msal-angularのInterceptorをちょっとだけ参考にします。
[ { endPoint: string, scopes: string[] } ]
上記の構造の設定を使用して、アクセスするエンドポイントによって
使用するスコープを切り替える機能を実装していきます。
(👇本家と比べるとかなり雑ですが…!
@Injectable() export class MsalInterceptor implements HttpInterceptor { constructor( private auth: MsalService ) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const target = environment.endPointTargetScope.filter(f => req.url.includes(f.endPoint)); let scopes = []; if (target.length > 0) { scopes = target[0].scopes; } if (!scopes || scopes === []) { return next.handle(req); } return from( this.auth.acquireTokenSilent(scopes) .then((token: string) => { const authHeader = `Bearer ${token}`; return req.clone({ setHeaders: { Authorization: authHeader, } }); }) ) .pipe( mergeMap(nextReq => next.handle(nextReq)) ); } }
ひとまずこれで、ログイン👉アクセストークン取得👉WebAPI通信の必要最低限な動きは確認できるようになります。
まとめ
Angularでのmsal.js v2.0.xの使用の仕方を見てきました。
@azure/msal-angularのv2がリリースされる可能性はありますが
@azure/msal-browser準拠のインターフェースや使い方になりそうな気はしているので
これを使用して、変わりそうな箇所を抑えておくのも良いかもしれません。
今回検証で使用したソースコードは下記にのごった煮レポジトリ格納しています。
AngularのRouterでuseHashをtrueにしているときにmsalで`Error: Cannot match any routes. URL Segment: 'access_token'`が出るときの対処
はじめに
2020年7月時点の記事となります。
msal.jsのv2が出たのですが、書きかけだった記事の供養となります。。ご留意ください。
Angular・msal.js・@azure/msal-angularは下記のバージョンとなっています。
- Angular: 10.0.1
- msal.js: 1.3.2
- @azure/msal-angular: 1.0.0
検証を行うバージョンによっては動作が異なることがありますのでご留意ください。
何が起きているか
タイトルのとおりですが、AngularのRouterModuleを設定するときにuseHash
をtrue
にすると
Angularのルーティングはhttp://localhost:4200/#/<path>
のような#
付きのパスとなります。
AzureADでimplicit flowでトークンが返却される場合、リダイレクトURLに#access_token=~~
のようにトークン情報が付与されて返却されているようです。
- fragmentでトークンが送られる
- Angularのルーティングで#付きのパス=遷移と誤認し遷移を実行
- 実際のパスは当然存在しないのでエラー
この流れが原因のようです。
queryで返却されるようになれば良かろうかとも思うのですが
msal.jsのConfigをさらっと眺めた感じだと、responce_modeをいじる設定はないように見えるので別の方法を模索してみます。
どのように解消するか
routerのeventをsubscribeするのも良いですがちょっと手間なのでシンプルな解決方法が望ましいですね。
解決方法1:BootstrapModuleの変更
window.parent
かどうか判定し、IFrame内部であればRouterを持たないダミーのモジュールを流し込めば良いはずです。
「MSAL.js を使用してトークンを自動的に取得、更新するときにページのリロードを回避する」が参考になるかと思います。
別の問題の解消方法ですが、これに似た方法で対応が可能となります。
ただ、上記のサイトに上げた方法だと、useHash=true
にしたRouterModuleをImportすることになるので、問題となっていた現象を解決できません。
そこで、window.parent
による分岐をAppModule下ではなく
もう少し上のレイヤーで実行し、BootstrapするModuleをRouterを持たないダミーのModuleに変更する。といった対応を行います。
処理の分岐を下記のようにmain.ts
で実行します。
if (window !== window.parent && !window.opener) { // IFrame内で実行されるダミーモジュール platformBrowserDynamic().bootstrapModule(RedirectDummyModule) .catch(err => console.error(err)); } else { // 通常のモジュール platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.error(err)); }
解決方法2:リダイレクト先の変更
そもそもAngularを使用していないプレーンなダミーサイトにリダイレクト先を変更しちゃおうという手です。
「ADAL Wiki - FAQ - Q1. My app is re-loading every time adal renews a token.」が参考になります。
これはadalの記事ですがmsal、@azure/msal-angularでも同様の実装が可能となります。
リダイレクト先のHTMLとして、下記のようなHTMLを用意しておきます。
リダイレクト時点でmsalのUserAgentApplicationインスタンスが生成されていないとエラーとなるので
msal.jsの読み込みと、UserAgentApplicationのインスタンス生成を行う必要があります。
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Frame Redirect</title> <script type="text/javascript" src="https://alcdn.msftauth.net/lib/1.3.2/js/msal.min.js" integrity="sha384-4cH4i4aLXsdIJJz5DF27S+GxZXRSqBORS4NN7kc7NyZBBIugxFyt6hQt5FiR/DuZ" crossorigin="anonymous"></script> </head> <body> dummy <script> var msalConfig = { clientId: "<Azure AD App Client ID>" }; var authContext = new Msal.UserAgentApplication(msalConfig); authContext.handleWindowCallback((err, res) => { console.log({ err, res }); }); if (window === window.parent) { window.location.replace(location.origin + location.hash); } </script> </body> </html>
仮に、このHTMLがredirect-page.html
とすると
アクセストークン取得時などにリダイレクトURLとしてこのURLを指定します。
// 👇msal-angularのMsalService.acquireTokenSilent() // 👇msal.jsのUserAgentApplication.acquireTokenSilent() const res = await this.msalService.acquireTokenSilent({ scopes: [ 'スコープ'], redirectUri: 'http://localhost:4200/redirect-page.html' // 👈こんな感じ });
デプロイする時も、今回作成したredirect-page.html
が必要になるのででangular.json
でデプロイ対象に指定しておきましょう。
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", .... "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { ... "assets": [ ... "src/redirect-page.html" 👈追加 ], ...
まとめ
そもそも別問題の対応のために、解決方法1や2を行っていたので今更見つけた感じになります。
無限リダイレクト問題とかにぶつかって、解決方法1,2とかに行き当たる方は多いと思うので知らん間に対応している。といったことが多そうです。
useHash:trueで且つ、そこらの対応がされていないとかのすこしニッチな状況のものになりますね。
msalのv2でこの記事が無意味なものに変わってくれるといいなーと若干期待しています。
今回検証で使用したリポジトリは下記となります。
Microsoft Graphのpresenceについて
修正
2020/7/19 : presenceのβ公開時期を2020年6月から2019年12月に修正しました。
(@karamem0さんご指摘ありがとうございました!)
2020/8/2:presenceのSubscriptionのドキュメントが公開されたので反映しました。
はじめに
2020年7月時点のMicrosoft Graphを使用した記事となります。
また記事で扱うpresenceは2020年7月段階でβ状態なので
参照されるタイミングによっては仕様が変更されている可能性がありますのでご留意ください。
presence
2019年12月に取得WebAPIがβ公開され、2020年7月に変更通知がサポートされました。
presenceとは具体的になんなのでしょうか。
めちゃくちゃかんたんに説明すると👇コレです。
presenceの取得
Graphの変更履歴の7月からのリンクは404なのですがドキュメントは確認できます。
presenceのデータを取得するのは下記のWebAPIで取得します。
※今はβしか公開されていないのでhttps://graph.microsoft.com/beta/
エンドポイントからしかアクセスできません。
GET /me/presence GET /users/{id}/presence GET /communications/presences
GET /communications/presences
と記載されていますが素直に使用しても複数人取得できず404になりました。
ユーザーid指定が必須のようなので、/communications/presences/{id}
が正しいと思います。
複数人のpresenceの取得はgetPresencesByUserIdです。
POSTで取得したいユーザーのidをBodyに指定するので、ちょっと違和感がありますね。
POST communications/getPresencesByUserId { "ids": ["<UserId>", "<UserId>"] }
presenceで取得するプロパティについて
取得するプロパティは少なく、id
、activity
、availability
の3種だけです。
👆のドキュメントはめったくそややこしいですが、動作させて確認したところこれで正しいようです。
(user's availabilityがavailibityじゃないんだ…的な)
Presenceの状態については、Teams でのユーザーのプレゼンスで確認できます。
availabilityとactivityの対応は下図の通りの認識です。
presenceの変更通知
2020年7月に変更通知に対応されました。
CreateSubscriptionにはリソースの明記はありませんが、OverViewによると/communications/presences
のようです。
presences
の取得から考えると単一ユーザー指定の/communications/presences/{id}
の指定になるようです。
IDなしやカンマ区切りの複数指定は400になりました。
また、Subscriptionの作成では有効期限を指定する必要があります。
ドキュメントに記載の通り、1時間のようです。
サブスクリプションの作成は他のリソースのときと同様、下記のようなJSONをPOSTすることで作成できます。
{ "changeType": "updated", "notificationUrl": "<エンドポイントURL>", "resource": "communications/presences/<user id>", "expirationDateTime": "<有効期限>" }
複数指定する場合は$filter
を使用して下記のように指定するようです。
"resource": "communications/presences$filter=id in ('<userid>', '<userid>'...)",
他のリソースのSubscriptionを作成する時とそこまで大きく変わるところはありませんね。
変更通知で通知される内容
Subscriptionを作成して内容を確認した結果、下記のようなデータが取得できました。
{ "value": [ { "subscriptionId": "cc87f8f1-45aa-4b33-9dd3-660bbaf3ed57", "clientState": null, "changeType": "updated", "resource": "communications/presences('<userid>')", "subscriptionExpirationDateTime": "2020-07-18T23:03:50.4305891-07:00", "resourceData": { "@odata.type": "#Microsoft.Graph.presence", "@odata.id": "communications/presences('<userid>')", "id": null, "activity": "InACall", "availability": "Busy" }, "tenantId": "<tenantid>" } ] }
activity
もavailability
も取得できているので、TeamsやEventのように別途WebAPIでリソースデータの取得などをする必要はなさそうです。
おわりに
β公開中のpresence
の取得と変更通知の使い方を見ていきました。
第 1 回 Japan M365 Dev User Group 勉強会ではこれをネタにLTする予定です。
よろしくおねがいします。
Angular Elementsで作成したWeb Componentsの設定情報をプレーンなJSから変更する
はじめに
この記事内のアプリケーションは下記バージョンで構成しています。
- Angular CLI: 10.0.1
- Angular: 10.0.2
この記事を参照されるタイミングによっては
サンプルコードが動作しなかったりする可能性がありますのでご留意くださいませ。
そもそもなにをしたいのか
Microsoft GraphはGraph ToolkitというWeb Componentsを公開していまして
オリジナルでそれを作ってみたいなー。というのが発端です。
今回の記事はその前哨戦。
Web Componentsを作ってみてハマった箇所の備忘録。といった感じですね。
Angular Elements
Angularを使用している場合は、Angularで作成したComponentをWebComponentsでパッケージングしてくれる
Angular Elementsというものが存在します。Angularで諸々なれている自分はこれを使うのが一番手っ取り早そうです。
基本的な使い方などは、👆のドキュメントを参考にすれば良いと思うので割愛します。
最終的に下記のようにAngularを使用していないようなプレーンなHTMLで
作成したWebComponentsのセレクタを指定してAngularで作成したComponentを表示させることが出来ます
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="../../dist/custom-element/styles.css"> <script src="../../dist/custom-element/custom-element.js"></script> </head> <body> <h1>Web Component (Costum Elements)</h1> <scl-custom-button></scl-custom-button> <scl-sample-form-group></scl-sample-form-group> <scl-router-page></scl-router-page> </body> </html>
👇
情報を設定する🤔
さて、Micorosoft Graphにアクセスするということは、ADのアプリケーション情報などが必要になります。
設定情報をWeb Components作成時点でビルドインしても良いのですが、それでは使い回しがしにくいです。
具体的には下図のような構成にしたいところですね。
ビルドされたソースはどうなっている?
ビルドされたパッケージ内のServiceクラスに値を直接外部から突っ込めるでしょうか?
下記のTSがビルドされたソースを整形して見てみます。
export class LibSettingsComponent implements OnInit { ... constructor( private service: LibStateService ) { } ... set setting(setting: { state: string, userId: string, userName: string }) { console.log('emit'); this.service.setting(setting); console.log(this.service.state); this.dummyStSet = 'emit'; } }
Oy), By = ((Iy = function() { function e(t) { _classCallCheck(this, e), this.service = t, this.dummySt = "dd" } return _createClass(e, [{ key: "ngOnInit", value: function() {} }, { ... }, { key: "setting", set: function(e) { console.log("emit"), this.service.setting(e), console.log(this.service.state), this.dummyStSet = "emit" } }]),
this.service
はfunctionの引数のt
で更にそこの呼び出し元の…?
いずれにせよServiceのインスタンスに直接アクションをするのは多大な労力を伴いそうです。
設定用のComponentを用意する
Componentからのアクセスは簡易に行えます。なので設定用のComponentを作成する。というアプローチをとってみます。
中身が空の設定用のComponentを用意します。
設定用のComponentは@Input
でSetterのみを提供し、Setterの実装内で設定情報Serviceを書き換えます。
具体的なソースコードは下記のとおりです。
色々視認やデバッグしやすいようにHTMLテンプレートにものを突っ込んでますが空で問題ないと思います。
import { Component, OnInit, Input } from '@angular/core'; import { LibStateService } from '../../services/lib-state.service'; @Component({ selector: 'scl-lib-settings', template: '<div>{{dummySt}}</div>' }) export class LibSettingsComponent { dummySt = 'bf'; constructor( private service: LibStateService ) { } @Input() set setting(setting: { state: string, userId: string, userName: string }) { // ここで設定情報をServiceに反映する this.service.setting(setting); this.dummySt = 'emit'; } }
これでプレーンなHTMLのタグ上などで、setting
プロパティに値を突っ込むことで設定情報が反映されるようになりました。
設定用のコンポーネントタグを書かなくて良いようにする
さて、設定を外から与えることができるようになりましたが
何も情報を出力しない設定用のコンポーネントをHTML上に書くのはなんともダサ味があります。
なので、スクリプトでも設定情報の反映ができるようにしたいところです。
そこで、
こんな感じの設定スクリプトをAngular Elementsから提供することにします。
設定スクリプトは下記のようなものです。
class LibSettings { setSettings(data) { const settingDom = document.createElement('scl-lib-settings'); document.body.appendChild(settingDom); settingDom.dummySt = 'hoge'; settingDom.setting = data; document.body.removeChild(settingDom); } }
作成した設定スクリプトをWebComponentsをビルドする際に一緒に吐き出したいので、angular.jsonをいじります。
"custom-element": { // Angular Elementsのプロジェクト名 ... "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { ... "scripts": [ // 👇を追加 "projects/custom-element/src/static-scripts/lib-settings.js" ]
これで、WebComponentsをパッケージングしたときに、同一ディレクトリに設定スクリプトも吐かれるようになりました。
使ってみる
設定スクリプトで設定が反映されたサービスのプロパティをアラート表示するボタンComponentを作成しました。
このボタンを押下することで設定情報の反映の確認を行ってみます。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="../../dist/custom-element/styles.css"> <script src="../../dist/custom-element/custom-element.js"></script> </head> <body> <h1>Web Component (Costum Elements)</h1> <scl-custom-button></scl-custom-button> <scl-sample-form-group></scl-sample-form-group> <scl-router-page></scl-router-page> <script src="../../dist/custom-element/scripts.js"></script> <script type="text/javascript"> // 設定スクリプトを使用して設定情報の反映 const cl = new LibSettings(); const setting = { state: 'HTMLから設定したよ!', userId: 'user', userName: 'hogehoge' } cl.setSettings(setting); // </script> </body> </html>
上記の状態で、アラートに「HTMLから設定したよ!」と表示されればOKです。
設定できました。
これで設定情報を外部から与えることができそうです。
おわりに
長い前哨戦が終わったので、次回はようやく目的だったオリジナルのGraph Toolkitの作成に入ってみます。
手を付けれていないのでいつになるかわかりませんが
今回検証を行ったソースコードは下記となります。
色々検証で試行錯誤したので少々とっちらかっているのはご容赦くださいませ…
Microsoft MVPを初受賞しました
タイトルの通りMicrosoft MVPを初受賞しました。
カテゴリはOffice Developmentとなります。
登壇の機会を多くくださった.NETラボの皆様
セッションを聞いてくださった皆様
フィードバックをくださった皆様
個人の活動であるにも関わらず会社でぼくの活動をサポートしてくださった皆様
僕の活動にあらゆる形で関わってくださった皆様にお礼申し上げます。
本当にありがとうございました。
これからも楽しんで技術を追いかけ続けようと思います。
ただ看板を頂いた以上はそれに恥じない動きを心がけたいと思います。
感想
初受賞ということもあり、Twitterで感想おさまらんなー。というわけで感想をポストしています。
Officeの名を冠するカテゴリで受賞させていただいたのですが
僕のブログは「Office」で検索しても1件も引っ掛からないレベルでOfficeに触れていません。
(1件くらいあるやろと思ってたので「マジか」と自分でも思いました。。。)
- よく言えばMicrosoft Graph特化で
- OfficeというよりOfficeのデータに熱中していて
- Webや.NETといったフロント方面から執拗にOfficeのデータにアプローチを行っている
そんな様が審査時に「変な人」的に印象にのこったかもしれないなーとか思ったりしています。
今後の話
受賞したからには継続して受賞していきたいと思いますし
Azureが好きで、Angularが好きで、.NET Coreが好きで、Microsoft Graphが好き。といった状況もまだまだ続くと思います。
そんな自分なので、Microsoft Graphはもちろんですが、Microsoft Graph以外も含め、多くの情報発信ができれば良いな。と考えていますので
今後ともよろしくおねがいいたします🙇
蛇足
受賞メールが届いた日は興奮したのか一睡もできず、かなり久々に完徹して出勤する事になりました。
結果、お昼すぎに睡魔に勝てず途中リタイア。
自分の老いにも確かな手応えを感じてしまったので、体調にも気をつけつつ頑張ろうと思います。。
AzureDevOpsのCIで.NET CoreとAngularのアプリケーションをバージョンアップする
はじめに
この記事内で使用する各フレームは下記バージョンで構成しています。
- Angular: 9.1.8
- .NET Core: 3.1
また、AzureDevOpsは記事作成時点のキャプチャやコマンドが掲載されています。
この記事を参照されるタイミングによっては
動作しなかったり画面が違うといった可能性がありますのでご留意くださいませ。
モチベーション
アプリケーションのバージョンアップ作業って地味で忘れがちです。
そういった作業は自動で行ってしまいたい!!
と、いうわけでAzure DevOpsのCIフローの中でバージョンアップ作業ができないか試してみました。
僕が主に使用しているのは.NET CoreとAngularのアプリケーションですので
その二つのアプリケーションでバージョンアップを実施するCIを組んでみようと思います。
.NETアプリケーション
.NETのアプリケーションのバージョンアップをCIで実現しようと思います。
.NETアプリケーションのバージョン番号はメジャー.マイナー.ビルド番号.リビジョン
で表現されますが
ビルド番号、あるいはリビジョンに*指定で、ビルド時に自動的にバージョンアップをしてくれます。
なので普通に運用する場合は何も考えなくてもバージョンアップはしてくれるのですが
バージョン付番を独自のルールで運用したり、付番したバージョンをデータストアに格納したりとかしたい場合があります。
なので、今回は独自ルールでバージョンを付番してアプリケーションを発行するCIを構築してみようと思います。
バージョンアップルール
今回は下記のルールでバージョンアップしようと思います
- 2020年6月2日からの経過日数をバージョン番号とする
- ビルドを行った日の00:00からの経過分数をリビジョンとする
.NETアプリケーションのビルドコマンドでバージョン番号を指定できるよう準備
MsBuld.exeを使用してビルドする際のコマンドでカスタムプロパティを指定できます。
そこで、ビルドコマンドのArgumentsでバージョンを変更できるようにします。
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> <UseWPF>true</UseWPF> <PublishSingleFile>true</PublishSingleFile> <RuntimeIdentifier>win-x86</RuntimeIdentifier> <!-- バージョン番号のベースの経過日数を受け取る --> <BuildNumber Condition=" '$(BuildNumber)' == '' ">0</BuildNumber> <!-- リビジョンのベースの経過分数を受け取る --> <Revision Condition=" '$(Revision)' == '' ">0</Revision> <!-- バージョンの指定--> <AssemblyVersion>0.0.$(BuildNumber).$(Revision)</AssemblyVersion> <VersionPrefix>0.0.$(BuildNumber).$(Revision)</VersionPrefix> </PropertyGroup> </Project>
これで、MsBuild.exe <プロジェクト> /p:BuildNumber=<適当な数字> /p:Revision=<適当な数字>
でバージョンをコマンド上で指定できるようになりました。
MsBuild.exe <プロジェクト> /p:BuildNumber=1 /p:Revision=2
した結果は下図のとおりです
CIの設定
CIで使用する変数の設定
CIの複数のタスクでバージョン情報を使い回すので、その情報を格納する変数を指定します。
経過日数を格納する$days
と経過分数を格納する$dayinterval
を用意しています。
variables: days: 0 dayinterval: 0
Power Shell
まずはPowerShellでコマンドで指定するバージョン番号とリビジョンを生成しています。
現在日付をゴニョゴニョして、$days
と $dayinterval
変数に格納します。
- 2020年6月2日からの経過日数をバージョン番号とする
- ビルドを行った日の00:00からの経過分数をリビジョンとする
ってことをやっています。
- task: PowerShell@2 displayName: 'Calc Build Version' inputs: targetType: inline script: | $baseDate = [datetime]"06/02/2020" $currentDate = $(Get-Date) $interval = NEW-TIMESPAN -Start $baseDate -End $currentDate $days = $interval.Days Write-Host "##vso[task.setvariable variable=days]$days" echo $days $today = Get-Date -f d $startdate = Get-Date $today $todayinterval = NEW-TIMESPAN -Start $startdate -End $currentDate $dayinterval = [Math]::Truncate($todayinterval.TotalMinutes) Write-Host "##vso[task.setvariable variable=dayinterval]$dayinterval" echo $dayinterval
Write-Host "##vso[task.setvariable variable=<変数名>]<PowerShell内の変数(突っ込みたいValue)>"
で👆で作成した変数に突っ込めます。
このPowershellでは、👆で指定したdays
とdayinterval
に値を突っ込んでいますね。
バージョンの情報を管理しているサービスなんかがある場合は
WebAPIを作成して、Invoke-WebRequest
コマンドを使用して引っ張ってきたり登録したりすればいいかなと思います。
MsBuild
MsBuildで👆で指定したバージョンを指定してビルドを行うコマンドを作成していきます。
- task: MSBuild@1 inputs: solution: '**/<プロジェクト名>/**/*.csproj' configuration: Release msbuildArguments: /t:Publish /p:BuildNumber=$(days) /p:Revision=$(dayinterval)
これはそこまで複雑なことはしていないですね。
PowerShellで設定した変数をMsBuildで使用するカスタムプロパティに指定しています。
実行結果
PowerShellタスクで指定するバージョン情報を出力しています。
Publishされたファイルのバージョンが、Powershellで生成した情報で構成されていることが確認できました。
Angularアプリケーション
普通にWebアプリケーションを作成するだけであれば特に意識する必要はないと思うのですが
ライブラリを作成してPublicなりPrivateなnpmレジストリにPublishしたい場合、バージョンアップは必要な作業です。
基本はnpm version
コマンドでバージョンアップしていけば良いかなと思っています。
バージョンアップルール
今回は下記のルールでバージョンアップしようと思います
master
ブランチのCIはnpm version patch
でパッチバージョンを上げるdevelop
ブランチのCIのときはプレリリースでdev
バージョンとしてリリースするnpm version prepatch --preid=dev
でバージョンを設定する- 参考: npm-version
CIの設定
ちょっとCIの順番と連動していないのですが、行う作業の関連など考慮して順不同で説明しています。
Power Shell
npmコマンドをPowerShellで実行します。
このPowershellコマンドでは下記のことを実行します
npm version
の実行- ライブラリの
package.json
のバージョンを上げたいのでライブラリのディレクトリで行う
- ライブラリの
- バージョン変更をリモートリポジトリにPushする
- そうしないと次回以降も同一のバージョンが生成されてしまうためですね
参考: MS Doc - Run Git commands in a script
参考: MS Doc - Build Azure Repos Git or TFS Git repositories
参考: Stack Overflow - How to increase a version of an npm package using Azure Devops pipeline
先にyamlを掲載すると👇な感じになります。
- task: PowerShell@2 displayName: 'Version Up & Commit' inputs: targetType: inline script: | git config user.name "learn-angular-library-ci Build Service (devtakas-public)" git config user.email "dummy@example.com" $BranchName = "$(Build.SourceBranch)" -replace "refs/heads/" git checkout $BranchName --quiet cd projects\sample-lib npm version $env:VERSIONCOMMAND --preid=dev -m "$env:VERSIONCOMMENT" --force --silent git add . git commit -m "$env:VERSIONCOMMENT" git push --quiet
Script内1~2行目
git config user.name "learn-angular-library-ci Build Service (devtakas-public)" git config user.email "dummy@example.com"
gitにコミットするためにユーザーの情報などを設定しています。
ここは好きな値で問題ないです。
Script内3~4行目
$BranchName = "$(Build.SourceBranch)" -replace "refs/heads/" git checkout $BranchName --quiet
Azure DevOpsのCI実行ログを見ればわかるのですが
ソースコードの操作が行われているブランチは厳密にはCIコマンドで指定したブランチと異なります。
バージョン変更を行ったコミットをPushしたいブランチはCIで指定するブランチなので
環境変数に格納されている作業ブランチをベースに作業ブランチのチェックアウトを行う必要があります。
Script内最後の行まで
cd projects\sample-lib npm version $env:VERSIONCOMMAND --preid=dev -m "$env:VERSIONCOMMENT" --force --silent git add . git commit -m "$env:VERSIONCOMMENT" git push --quiet
作業ディレクトリをライブラリのpackage.jsonがあるところまで移動して諸々作業します。
$env:VERSIONCOMMAND
はAzure DevOpsのCI上で設定する変数です。
masterのCIではpatch
、developのCIではprerelease
を指定しています。
AzureDevOpsのCIでは、rootディレクトリ以外ではnpm version
してもCommitが発生しなかったので
別途 git add .
とgit commit
しています。
警告などでるとAzure DevOpsのCIでエラーを吐いてしまうので
適宜--quiet
や--silent
でエラーにならないように回避しています。
参考: Stack Overflow - How to generate NPM release candidate version
参考: はらへり日記 - npm scriptsでエラーログを表示させたくない話
認証の設定
参考: MS Doc - Run Git commands in a script
AzureDevOpsで管理されているリポジトリに対してPushを行いたいので
ソースを取得する段階で認証情報を設定しちゃいます。
下記で設定可能です。
steps: - checkout: self persistCredentials: true
Trigger
例えばdevelop
ブランチの変更があった場合、develop
ブランチにCommit/Pushが行われます。
つまり、なにも考慮しないと無限ループになります(なった)。
自分は、単純に下記の通り自動変更される対象のファイルをexclude設定にしました。
trigger: branches: include: - develop paths: exclude: - projects/sample-lib/package.json
ここは人により設定が変わりそうなところですね。
ユーザーの設定
CIには実行するユーザーが割り振られています。
なのでそのユーザーがソースコードに対してCommitしたりPushできたりするように設定する必要があります。
参考: MS Doc - Run Git commands in a script
設定場所は下図から確認できます。
Usersの<プロジェクト名> Build Service
という名前に基本なっていると思います。
設定は下記のとおりですね。
Contribute
/Read
- 必須
Create Branch
/Create Tag
- 必要があれば
- リリース時にタグ付けしたいとか、自動コミットじゃなくてブランチとPR作りたいとかの場合かな?と思います
Bypass policies when pushing
- ブランチポリシーで保護しているブランチに対してPushしたい時に必要です。
実行結果
今回はDevOps上のArtifactsにライブラリをPublishしました。
- developブランチのCIの場合
preleaseで出力されていることが確認できます。
- masterブランチのCIの場合
patchで出力されていることが確認できます。
おわりに
今回使用したリポジトリは下記の通りです。
Angularの方はブランチポリシー下の動作検証のためAzure DevOpsでソース管理もしています。
.NET Core
Angular
供養
長く苦しい戦いの記録
Angular MaterialのComponentHarnessを使ってみた(Autocomplete編)
はじめに
この投稿内のAngularは下記のバージョンで構成しています。
この記事を参照されるタイミングによってはサンプルコードが動作しない可能性がありますのでご留意くださいませ。
- Angular CLI: 9.1.6
- @angular/material: 9.2.3
ComponentHarness
先日、ng-japan OnAir #16 を視聴した際にでてきた
Angular Materialで提供されているComponentHarnessを使ってみました。
自分が作るAngularのアプリケーションはMaterialを多用しているものが多く
Componentのテストが壊れにくくなりそうである。ということが魅力的でした。
ComponentHarness含む「CDK Testing API」については
下記の@lacolacoさんのレポートをご参考いただければと思います。
AutocompleteのComponentHarness
割とMaterial全般を使っているので、一個ずつComponentを確認しようかな、と思いまして。
まずは「Autocomplete」に手を付けてみました。
(構成の都合上MatInputもちょいちょい入ってますが…)
超絶雑ですが👇がテストを行うComponentの構成です。
Serviceからリストを受け取ってAutocompleteでリスト表示。
チェックボックスでAutocompleteコントロールのDisbaledを制御できる。みたいな感じです。
Harnessの作成
使用するのは初めてなので、そもそもHarnessの作り方、使い方から確認していきます。
Angular Materialのドキュメントと、ng-japan OnAirで実装されていたソースなどを参考に実装しました。
Componentテスト用のHarnessクラスを作成し、そこに諸々準備していきます。
MaterialのComponentHarnessは、各Materialのtesting内に格納されているそうです。 あとはMatInputと組み合わせて使用するので、MatInputのHarnessも必要そうです。
import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing'; import { MatInputHarness } from '@angular/material/input/testing';
で、AutocompleteとMatInputのHarnessをインポートしておきます。
privateでlocatorを準備して、コントロールにアクセスできるようにします。
今回、プレーンなチェックボックスも使用するので、locatorは👇な感じになりました。
private getCheckBoxLocator = this.locatorFor('[data-testctl="checkbox"]'); private getIncputLocator = this.locatorFor( MatInputHarness.with({ placeholder: 'Test', }) ); private getAutoCompleteLocator = this.locatorFor(MatAutocompleteHarness);
- チェックボックスのコントロールはテスト用の属性を指定して取得
- MatInputは、MatInputHarnessを介して、placeholderが「Test」になっているコントロールを取得
- AutocompleteはComponentに一つしかないので「MatAutocompleteHarness」のみで取得
コントロールの動作を実装する
Harnessクラスの中で公開する、コントロールの動作を実装していきます。
例えば、Dsiableを制御するチェックボックスのトグルは👇な感じですね。
async toggleCheckBox(): Promise<void> { const chkbox = await this.getCheckBoxLocator(); await chkbox.click(); }
Locatorを介して取得したオブジェクトのClickイベントを叩くだけです。かんたん。
Disable状態を変更する動作をしたあとは、実際のコントロールの状態を確認したいです。
async inputControlDisabledState(): Promise<boolean> { const input = await this.getIncputLocator(); return input.isDisabled(); }
こっちはlocatorを介してInputのオブジェクトを取得し、そのオブジェクトのisDisbaledステータスを返却します。
こっちもかんたん。
テストを実装してみる
さて、specファイルでテストを記述していきます。
テストで先程作成したHarnessを使用するためにfixtureを作成したあとに
使用するHarnessをFixtureを関連付けます(fixtureにharness(補助具)を割り当てるという表現になるんですかね?)。
beforeEach(async () => { fixture = TestBed.createComponent(テストするComponent); component = fixture.componentInstance; harness = await TestbedHarnessEnvironment.harnessForFixture( fixture, 作成したHarnessクラス ); fixture.detectChanges(); });
これで準備完了です。
行うテストを実装していきます。
まずは「チェックボックスをクリックしたらAutocompleteのInputが入力不可になること」を実装してみます。
it ('CheckBoxToggle -> Input is Disabled', async () => { await harness.toggleCheckBox(); // Harness上で実行されるクリック処理 const isDisabled = await harness.inputControlDisabledState; // Harness上で取得されるInputのDisabled状態 expect(isDisabled).toBeTruthy(); });
なんぞこれ?ですね。めちゃくちゃ簡単になっています。
Harnessなしの状態だと、Specファイル内でやっていた、ControlをElementから取得してー。とか。
そのコントロールから値を読んでー。といった処理が一掃されています。
そういったコードが抹消されたことにより、テストコードの可読性も上がっていますね。
さて、本題です。
AutocompleteのComponentHarness(2)
Autocompleteのコントロールでは下記3点をテストしてみようと思います。
- Autocompleteのリストの初期状態
- 対応するInputに入力があった場合に絞り込まれた状態となるか
- Autocompleteのリストから選択された場合は対応するInputに文字列が入力されるか
まぁ、Materialで担保している内容もありますが、勉強なので…ということで。
さっそくハマる
リストの状態は、下記で取得できるのかいなと思っていたのですが
結果として帰ってきたのは空配列でした。
async getAutoCompleteList(): Promise<MatOptionHarness[]> { const ctrl = await this.getAutoCompleteLocator(); return await ctrl.getOptions(); }
実装を確認する
ひとまず、困ったときは実装を確認するのみです。
実装の細かい部分はさておき、getOptions()
の動きとしてはMatOptionのオブジェクトを取得してきているようです。
では、表示されるオプションはどうなっているのか確認してみます。
実際に動作させてElementを参照してみるとわかるのですが
AutoCompleteのリストはフォーカスがInputにあたった際に生えてくるもののようです。
なので、オプションの情報を読むためには、「フォーカスをインプットに当てる」というワンアクションが必要であると予測します。
「フォーカスをインプットに当てる」処理を実装する
うまく行かなかった方法
インプットのlocatorはすでに作成したので、こいつが使えないかと思いました。
実装すると👇な感じですね。
async inputFocus(): Promise<void> { const input = await this.getIncputLocator(); await input.focus(); // focusを当てる }
これを、オプション情報の取得前に実行します。
async getAutoCompleteList(): Promise<MatOptionHarness[]> { const ctrl = await this.getAutoCompleteLocator(); this.inputFocus(); return await ctrl.getOptions(); }
結果、うまく行きませんでした。
フォーカスはあたっているが…!って感じですね。Autocompleteのリストが表示されていません。
ただ、デバッグを行っているウィンドウを選択していたらオプションが表示されるのでテストは通るのです。
結果はともかく、方針としては問題ないようです。
先人にならう
そもそも、MaterialのAutocompleteのComponent自体は同じようなテストをしているはずです。
どのようなテストを行っているのか確認してみました。
実装を見てみると、フォーカスをあてる処理としてdispatchFakeEvent(input, 'focusin');
なることをしているようです。
ひとまず、dispatchFakeEvent
周りの実装(この子とこの子とこの子)はそのまま使用させていただこうかと思います。
dispatchFakeEvent
に食わせるElementの取得
dispatchFakeEvent
の引数としてHtmlElementを要求されているので、その情報がほしいです。
locatorからElementの情報を取得できるので、それが利用できないか試してみました。
async getAutoCompleteList(): Promise<MatOptionHarness[]> { const ctrl = await this.getAutoCompleteLocator(); const inputElement: any = await ctrl.host(); // Element取得 dispatchFakeEvent(inputElement, 'focusin'); // focus当てる return await ctrl.getOptions(); }
しかし、host()の情報を利用するのはだめでした。
Elementの情報は取得できるのですが、HtmlElementそのものではないようで、dispatchEventが参照できずに終了します。
ここは本家よろしく、fixtureから直接取得する他なさそうです。
苦肉の策
fixtureから直接取得することにはしましたが、なるたけspecの方に実装したくはありません。
フォーカスを当てるのは、あくまでAutocompleteのリストを表示させてその値を参照したいからであって
そういった値を取得するまでのあれやこれやの処理はHarness側に寄せたいと思ったためです。
なので👇のように実装しました。
async getAutoCompleteList(fixture: ComponentFixture<AutoCompleteComponent>): Promise<MatOptionHarness[]> { const ctrl = await this.getAutoCompleteLocator(); const inputElement = fixture.debugElement.query(By.css('[data-testctl="autcompinput"]')).nativeElement; dispatchFakeEvent(inputElement, 'focusin'); return await ctrl.getOptions(); }
fixtureを引数でもらって、そこからElementを取得する力技です。
ベストプラクティスからは遠いかな?とも思いますが、これでうまくいくかどうか試してみます。
うまくいきました。
テストの実装はどうなったか
では、テスト本体の実装はどうなったのか見てみます。
テスト実行部分だけ抜粋します。
it('should create', () => { expect(component).toBeTruthy(); }); it ('init option list', async () => { const datas = await harness.getAutoCompleteList(fixture); expect(datas.length).toEqual(10); }); it ('filter option list', async () => { await harness.inputText('0'); fixture.detectChanges(); const datas = await harness.getAutoCompleteList(fixture); expect(datas.length).toEqual(1); }); it ('option selection', async () => { await harness.inputText(''); fixture.detectChanges(); await harness.selectOption('0さん', fixture); const iputValue = await harness.inputTextValue(); expect(iputValue).toEqual('0さん'); }); it ('CheckBoxToggle -> Input is Disabled', async () => { await harness.toggleCheckBox(); const isDisabled = await harness.inputControlDisabledState; expect(isDisabled).toBeTruthy(); });
ものすごくスッキリしました。
改めて見て「これがAngularのComponentのテストコードってマジ?」とか思いました。
まとめ
Angular MaterialのAutocompleteのComponentHarnessの使い方を見てきました。
実装した感覚、コード自体は普段と比べて書く量はそこまで変わらないような印象ですが
テストコード本体のスッキリ具合や
意図せずともテストの動作を個別に実装することができるようになるので
ソースコードの再利用性や可読性はぐっと上がるのではないかと思いました。
(ひょっとしたら再利用性のほうが恩恵でかいのでは…?と思ってたりもします。)
MaterialのComponentHarnessは最初から色々とコントロールを動かすためのAPIを用意してくれているので積極的に使っていきたいですね!
今回作成した諸々は下記レポジトリに格納しています。
ちょいちょいMatrialのComponentHarnessで実験して、ハマったら記事に上げようかと思います。。