Skip to content

Commit

Permalink
refactor: api token service improvements (#1522)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Silke <[email protected]>
  • Loading branch information
Eisie96 and SGrueber authored Oct 30, 2023
1 parent fc17144 commit 4b263f1
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 36 deletions.
2 changes: 2 additions & 0 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ kb_sync_latest_only
## From 4.2 to 5.0

We renamed the input parameter 'id' to 'productListingId' for the product listing component to avoid unintentionally having more than one element with the same id in the HTML document.
The api-token.service has been refactored and the class variables `apiToken$` and `cookieVanishes$` have got the private modifier.
Use the public getter/setter methods to access these variables outside the class.

## From 4.1 to 4.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('Icm Identity Provider', () => {

beforeEach(() => {
when(apiTokenService.restore$()).thenReturn(of(true));
when(apiTokenService.cookieVanishes$).thenReturn(new Subject());
when(apiTokenService.getCookieVanishes$()).thenReturn(new Subject());

resetCalls(apiTokenService);
resetCalls(accountFacade);
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/identity-provider/icm.identity-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class ICMIdentityProvider implements IdentityProvider {
init() {
this.apiTokenService.restore$().subscribe(noop);

this.apiTokenService.cookieVanishes$.subscribe(type => {
this.apiTokenService.getCookieVanishes$().subscribe(type => {
this.accountFacade.logoutUser({ revokeApiToken: false });
if (type === 'user') {
this.router.navigate(['/login'], {
Expand Down
10 changes: 5 additions & 5 deletions src/app/core/store/customer/basket/basket.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ import { BasketEffects } from './basket.effects';
describe('Basket Effects', () => {
let actions$: Observable<Action>;
let basketServiceMock: BasketService;
let apiTokenMock: ApiTokenService;
let effects: BasketEffects;
let store: Store;
let router: Router;

beforeEach(() => {
basketServiceMock = mock(BasketService);
apiTokenMock = mock(ApiTokenService);

TestBed.configureTestingModule({
imports: [
Expand All @@ -70,11 +72,7 @@ describe('Basket Effects', () => {
RouterTestingModule.withRoutes([{ path: '**', children: [] }]),
],
providers: [
{
provide: ApiTokenService,
useFactory: () => instance(mock(ApiTokenService)),
useValue: { apiToken$: of({ apiToken: 'apiToken' }) },
},
{ provide: ApiTokenService, useFactory: () => instance(apiTokenMock) },
{ provide: BasketService, useFactory: () => instance(basketServiceMock) },
BasketEffects,
provideMockActions(() => actions$),
Expand All @@ -84,6 +82,8 @@ describe('Basket Effects', () => {
effects = TestBed.inject(BasketEffects);
store = TestBed.inject(Store);
router = TestBed.inject(Router);

when(apiTokenMock.getApiToken$()).thenReturn(of('apiToken'));
});

describe('loadBasket$', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/store/customer/basket/basket.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ export class BasketEffects {
select(getCurrentBasket),
mapToProperty('id'),
// append corresponding apiToken and customer
withLatestFrom(this.apiTokenService.apiToken$, this.store.pipe(select(getLoggedInCustomer))),
withLatestFrom(this.apiTokenService.getApiToken$(), this.store.pipe(select(getLoggedInCustomer))),
// don't emit when there is a customer
filter(([, , customer]) => !customer),
startWith([]),
Expand Down
9 changes: 9 additions & 0 deletions src/app/core/store/customer/customer-store.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,13 @@ export class CustomerStoreModule {
static forTesting(...reducers: (keyof ActionReducerMap<CustomerState>)[]) {
return StoreModule.forFeature('_customer', pick(customerReducers, reducers));
}

/**
* Customer_STORE_CONFIG needs to be provided in test
* @example
* { provide: CUSTOMER_STORE_CONFIG, useClass: CustomerStoreConfig }
*/
static forTestingWithMetaReducer(...reducers: (keyof ActionReducerMap<CustomerState>)[]) {
return StoreModule.forFeature('_customer', pick(customerReducers, reducers), CUSTOMER_STORE_CONFIG);
}
}
231 changes: 231 additions & 0 deletions src/app/core/utils/api-token/api-token.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { TestBed } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { combineLatest, skip } from 'rxjs';
import { anything, instance, mock, verify, when } from 'ts-mockito';

import { Basket } from 'ish-core/models/basket/basket.model';
import { CustomerLoginType } from 'ish-core/models/customer/customer.model';
import { Order } from 'ish-core/models/order/order.model';
import { CoreStoreModule } from 'ish-core/store/core/core-store.module';
import { loadBasketSuccess } from 'ish-core/store/customer/basket';
import {
CUSTOMER_STORE_CONFIG,
CustomerStoreConfig,
CustomerStoreModule,
} from 'ish-core/store/customer/customer-store.module';
import { loadOrderSuccess, selectOrder } from 'ish-core/store/customer/orders';
import { loginUserSuccess, logoutUserSuccess } from 'ish-core/store/customer/user';
import { CookiesService } from 'ish-core/utils/cookies/cookies.service';

import { ApiTokenCookie, ApiTokenService } from './api-token.service';

describe('Api Token Service', () => {
let apiTokenService: ApiTokenService;
let cookieServiceMock: CookiesService;

let store: Store;

const initialApiTokenCookie: ApiTokenCookie = {
apiToken: '123@apiToken',
type: 'user',
};

const injectServices = (cookieServiceMock: CookiesService) => {
TestBed.configureTestingModule({
imports: [
CoreStoreModule.forTesting(),
CustomerStoreModule.forTestingWithMetaReducer('basket', 'user', 'orders'),
],
providers: [
{ provide: CookiesService, useFactory: () => instance(cookieServiceMock) },
{ provide: CUSTOMER_STORE_CONFIG, useClass: CustomerStoreConfig },
],
});

apiTokenService = TestBed.inject(ApiTokenService);
store = TestBed.inject(Store);
};

beforeEach(() => {
cookieServiceMock = mock(CookiesService);
});

it('should be created', () => {
injectServices(cookieServiceMock);
expect(apiTokenService).toBeTruthy();
});

describe('getInternalApiTokenCookieValue$', () => {
beforeEach(() => {
when(cookieServiceMock.get('apiToken')).thenReturn(JSON.stringify({ apiToken: '123' } as ApiTokenCookie));
injectServices(cookieServiceMock);
});

it('should create user apiToken cookie when only user information changes in store', () => {
store.dispatch(loginUserSuccess({ user: { firstName: 'Test', lastName: 'User' } } as CustomerLoginType));

verify(
cookieServiceMock.put(
'apiToken',
JSON.stringify({ apiToken: '123', type: 'user', isAnonymous: false, creator: 'pwa' } as ApiTokenCookie),
anything()
)
).once();
});

it('should create anonymous user apiToken cookie when only basket information changes in store', () => {
store.dispatch(loadBasketSuccess({ basket: { id: 'new-basket' } as Basket }));

verify(
cookieServiceMock.put(
'apiToken',
JSON.stringify({ apiToken: '123', type: 'user', isAnonymous: true, creator: 'pwa' } as ApiTokenCookie),
anything()
)
).once();
});

it('should create order apiToken cookie when only order is selected in store', () => {
store.dispatch(selectOrder({ orderId: 'orderId' }));

verify(
cookieServiceMock.put(
'apiToken',
JSON.stringify({ apiToken: '123', type: 'order', orderId: 'orderId', creator: 'pwa' } as ApiTokenCookie),
anything()
)
).once();
});

it('should update apiToken information in cookie when apiToken changes', () => {
apiTokenService.setApiToken('new-api-token');

verify(
cookieServiceMock.put('apiToken', JSON.stringify({ apiToken: 'new-api-token' } as ApiTokenCookie), anything())
).once();
});
});

describe('logout$', () => {
beforeEach(() => {
when(cookieServiceMock.get('apiToken')).thenReturn(JSON.stringify({ apiToken: '123' } as ApiTokenCookie));
injectServices(cookieServiceMock);

store.dispatch(
loginUserSuccess({ userId: 'user', user: { firstName: 'Test', lastName: 'User' } } as CustomerLoginType)
);
});

it('should remove apiToken on logout', () => {
store.dispatch(logoutUserSuccess());

verify(cookieServiceMock.remove('apiToken', anything())).once();
});
});

describe('cookieVanish$', () => {
beforeEach(() => {
when(cookieServiceMock.get('apiToken')).thenReturn(JSON.stringify(initialApiTokenCookie));
injectServices(cookieServiceMock);
});

it('should vanish apiToken information when cookie is removed unexpectedly from the outside', done => {
when(cookieServiceMock.get('apiToken')).thenReturn(JSON.stringify(initialApiTokenCookie), undefined);
combineLatest([apiTokenService.getApiToken$(), apiTokenService.getCookieVanishes$()]).subscribe(
([apiToken, cookieVanishes]) => {
expect(apiToken).toBeUndefined();
expect(cookieVanishes).toEqual('user');
done();
}
);

setTimeout(() => undefined, 3000);
});
});

describe('tokenCreatedOnAnotherTab$', () => {
beforeEach(() => {
injectServices(cookieServiceMock);
});

it('should set correct apiToken when new apiToken is set unexpectedly from the outside', done => {
when(cookieServiceMock.get('apiToken')).thenReturn(undefined, JSON.stringify(initialApiTokenCookie));
apiTokenService
.getApiToken$()
.pipe(skip(1))
.subscribe(apiToken => {
expect(apiToken).toEqual('123@apiToken');
done();
});

setTimeout(() => undefined, 3000);
});
});

describe('restore$', () => {
describe('user', () => {
it('should react on a loaded user in state for a non anonymous user', done => {
when(cookieServiceMock.get('apiToken')).thenReturn(JSON.stringify(initialApiTokenCookie));
injectServices(cookieServiceMock);

store.dispatch(loginUserSuccess({ customer: { customerNo: '123' } } as CustomerLoginType));

apiTokenService.restore$().subscribe(restored => {
expect(restored).toBeTrue();

done();
});
});

it('should react on a loaded basket in state for an anonymous user', done => {
when(cookieServiceMock.get('apiToken')).thenReturn(
JSON.stringify({
...initialApiTokenCookie,
isAnonymous: true,
} as ApiTokenCookie)
);
injectServices(cookieServiceMock);

store.dispatch(loadBasketSuccess({ basket: { id: '123', totals: {} } as Basket }));

apiTokenService.restore$().subscribe(restored => {
expect(restored).toBeTrue();

done();
});
});

it('should react on no state state changes when types list is not available', done => {
when(cookieServiceMock.get('apiToken')).thenReturn(JSON.stringify(initialApiTokenCookie));
injectServices(cookieServiceMock);

apiTokenService.restore$([]).subscribe(restored => {
expect(restored).toBeTrue();

done();
});
});
});

describe('order', () => {
it('should react on a loaded order in state when an order apiToken type is available', done => {
when(cookieServiceMock.get('apiToken')).thenReturn(
JSON.stringify({
...initialApiTokenCookie,
type: 'order',
orderId: '123',
} as ApiTokenCookie)
);
injectServices(cookieServiceMock);

store.dispatch(loadOrderSuccess({ order: { id: '123' } as Order }));

apiTokenService.restore$().subscribe(restored => {
expect(restored).toBeTrue();

done();
});
});
});
});
});
Loading

0 comments on commit 4b263f1

Please sign in to comment.