我正在尝试用茉莉花为我拥有的Angular服务编写规范。该服务包装了@azure/msal-angular
,还使用Microsoft Graph API来获取登录用户的个人资料图片。我无法成功通过测试。我要尝试做的就是验证当要求图片时Graph API错误(即存在404错误)时,是否捕获了该错误,并返回了我的Assets文件夹中的默认图像。
下面是我正在接受测试的服务以及测试本身。任何帮助都将不胜感激!
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { BroadcastService, MsalService } from '@azure/msal-angular';
import { Account, AuthError, AuthResponse, ClientAuthError } from 'msal';
import { Observable, ReplaySubject, throwError } from 'rxjs';
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
import { GraphUser } from '../../models';
import { LocalBlobService } from '../../services';
import { environment } from './../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class AuthService {
isLoggedIn: boolean;
/** TODO: Cache locally
* - clear on logout
* - how often to refresh?
*/
graphUser$: Observable<GraphUser>;
/** TODO: Cache locally
* - clear on logout
* - how often to refresh?
*/
graphPicture$: Observable<SafeUrl>;
get loginFailure$(): Observable<any> {
return this._loginFailure$.asObservable();
}
get loginSuccess$(): Observable<any> {
return this._loginSuccess$.asObservable();
}
get user(): Account {
const user = this._msal.getAccount();
this.isLoggedIn = !!user;
return user;
}
private _loginFailure$ = new ReplaySubject<any>();
private _loginSuccess$ = new ReplaySubject<any>();
constructor(
private readonly _http: HttpClient,
private readonly _msal: MsalService,
private readonly _broadcasts: BroadcastService,
private readonly _sanitizer: DomSanitizer,
private readonly _localBlob: LocalBlobService
) {
this._msal.handleRedirectCallback(this.redirectCallback);
this.getGraphUserInfo();
this._broadcasts.subscribe(
'msal:loginFailure',
this.loginFailureCallback
);
this._broadcasts.subscribe(
'msal:loginSuccess',
this.loginSuccessCallback
);
}
updateUserProfilePhoto(file) {
return this._http
.put(environment.AD.pictureUrl, file)
.pipe(
catchError((error, caught) => this.handlePictureError(error))
);
}
private getGraphUserInfo() {
if (this.user) {
this.graphUser$ = this.getGraphUser();
this.graphPicture$ = this.getGraphPicture();
}
}
private getGraphPicture(): Observable<SafeUrl> {
return this._http
.get(environment.AD.pictureUrl, {
responseType: 'blob',
})
.pipe(
catchError((error, caught) => this.handlePictureError(error)),
switchMap(blob => this._localBlob.readAsDataURL(blob)),
map(picture => this._sanitizer.bypassSecurityTrustUrl(picture)),
shareReplay(1)
);
}
private getGraphUser(): Observable<GraphUser> {
return this._http.get<GraphUser>(environment.AD.graphUrl).pipe(
catchError((error, caught) => throwError(error)),
shareReplay()
);
}
private loginSuccessCallback = payload => {
this.isLoggedIn = true;
this._loginSuccess$.next();
this.getGraphUserInfo();
};
private loginFailureCallback = payload => {
this.isLoggedIn = false;
this._loginFailure$.next();
};
private redirectCallback = (
redirectError: AuthError,
redirectResponse: AuthResponse
) => {
if (redirectError) {
console.error(redirectError);
return;
}
console.log(redirectResponse);
};
private handlePictureError(error: ClientAuthError): Observable<Blob> {
console.log(error);
return this._http.get('/assets/images/defaultAvatarSmall.png', {
responseType: 'blob',
});
}
}
it('should return default image if graph API picture fails', (done: DoneFn) => {
// Arrange
const spy = spyOn(TestBed.inject(HttpClient), 'get')
.withArgs(environment.AD.graphUrl)
.and.callThrough()
.withArgs(environment.AD.pictureUrl, {
responseType: 'blob',
} as any)
.and.returnValue(throwError(new ClientAuthError('')))
.withArgs('/assets/images/defaultAvatarSmall.png', {
responseType: 'blob',
} as any)
.and.callThrough();
// Act
const service: AuthService = TestBed.inject(AuthService);
service.graphPicture$.subscribe(
() => {
// Assert
expect(spy.calls.allArgs()).toEqual([
[environment.AD.graphUrl],
[
environment.AD.pictureUrl,
{ responseType: 'blob' as 'json' },
],
[
'/assets/images/defaultAvatarSmall.png',
{ responseType: 'blob' as 'json' },
],
]);
done();
},
error => {
expect(1).toEqual(1);
done();
}
);
});
我终于明白了。由于这一天,今天真是漫长的一天...
我将getGraphPicture
中的AuthService
更改为如下所示:
private getGraphPicture(): Observable<SafeUrl> {
// Observable stream of image requests where errors are not emitted.
return onErrorResumeNext<Blob>(
// Try the logged in user's pictureUrl first
this._http.get<Blob>(environment.AD.pictureUrl, {
responseType: 'blob' as 'json',
}),
// Try the default avatar picture in assets next
this._http.get<Blob>('/assets/images/defaultAvatarSmall.png', {
responseType: 'blob' as 'json',
})
).pipe(
first(), // Only grab the first successful emission
switchMap(blob => this._localBlob.readAsDataURL(blob)),
map(picture => this._sanitizer.bypassSecurityTrustUrl(picture)),
shareReplay(1)
);
}
然后我像这样写了我的测试:
it('should return default image if graph API picture fails', fakeAsync(() => {
// Arrange
const spy = spyOn(TestBed.inject(HttpClient), 'get')
.withArgs(environment.AD.graphUrl)
.and.callThrough()
.withArgs(environment.AD.pictureUrl, {
responseType: 'blob' as 'json',
})
.and.returnValue(throwError(new ClientAuthError('')))
.withArgs('/assets/images/defaultAvatarSmall.png', {
responseType: 'blob' as 'json',
})
.and.callThrough();
// Act
const service: AuthService = TestBed.inject(AuthService);
tick();
expect(spy.calls.allArgs()).toEqual([
[environment.AD.graphUrl],
[environment.AD.pictureUrl, { responseType: 'blob' as 'json' }],
[
'/assets/images/defaultAvatarSmall.png',
{ responseType: 'blob' as 'json' },
],
]);
}));