@internetarchive/ia-book-actions 1.4.6-alpha1 → 1.4.6-alpha3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/services/actions-handler/actions-handler-service.js +17 -13
- package/src/core/services/loan-token-poller.js +21 -9
- package/src/ia-book-actions.js +30 -12
- package/test/core/services/actions-handler-service.test.js +14 -0
- package/test/core/services/loan-token-poller.test.js +44 -1
- package/test/ia-book-actions.test.js +111 -44
package/package.json
CHANGED
|
@@ -131,11 +131,12 @@ export default async function ActionsHandlerService(options) {
|
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* Report it, don't just swallow it.
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
134
|
+
* Report it, don't just swallow it. This catches a rejected fetch
|
|
135
|
+
* (offline, dropped or blocked connection) and anything thrown after the
|
|
136
|
+
* response arrived, including by the caller's own callbacks. Non-JSON
|
|
137
|
+
* bodies are handled in parseServiceResponse() and don't reach here.
|
|
138
|
+
* Calling neither `success` nor `error` would leave every caller waiting
|
|
139
|
+
* on a callback that never comes. Only a rejected fetch is a network
|
|
139
140
|
* problem, so only that one says so.
|
|
140
141
|
*
|
|
141
142
|
* For renew_loan that's not merely a missing modal: IABookActions
|
|
@@ -145,9 +146,13 @@ export default async function ActionsHandlerService(options) {
|
|
|
145
146
|
* connection is exactly when this fires.
|
|
146
147
|
*/
|
|
147
148
|
log(`[IABookActions] ✗ ${option.action} threw`, error);
|
|
149
|
+
// networkFailure marks "the service never answered" (like
|
|
150
|
+
// nonJsonResponse in parseServiceResponse) — see
|
|
151
|
+
// IABookActions.handleLendingActionError.
|
|
148
152
|
option?.error?.({
|
|
149
153
|
error: reachedResponse ? unexpectedError : networkError,
|
|
150
154
|
detail: String(error),
|
|
155
|
+
...(reachedResponse ? {} : { networkFailure: true }),
|
|
151
156
|
});
|
|
152
157
|
}
|
|
153
158
|
}
|
|
@@ -167,16 +172,15 @@ export async function parseServiceResponse(response, option) {
|
|
|
167
172
|
const { status } = response;
|
|
168
173
|
const contentType = response.headers?.get('content-type') || '';
|
|
169
174
|
|
|
170
|
-
// The service always answers with JSON
|
|
171
|
-
//
|
|
175
|
+
// The service always answers with JSON; a body that doesn't parse never
|
|
176
|
+
// came from it. Parsed regardless of Content-Type, so a valid answer is
|
|
177
|
+
// never mistaken for an error page if a header is ever missing.
|
|
172
178
|
const body = await response.text();
|
|
173
179
|
let data;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
data = undefined;
|
|
179
|
-
}
|
|
180
|
+
try {
|
|
181
|
+
data = JSON.parse(body);
|
|
182
|
+
} catch (parseError) {
|
|
183
|
+
data = undefined;
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
if (data === undefined) {
|
|
@@ -47,6 +47,13 @@ export class LoanTokenPoller {
|
|
|
47
47
|
|
|
48
48
|
this.loanTokenInterval = undefined;
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* create_token failures in a row since the last success, passed to
|
|
52
|
+
* errorCallback so the consumer can tell a one-off blip from a run of
|
|
53
|
+
* them — see IABookActions.handleLendingActionError.
|
|
54
|
+
*/
|
|
55
|
+
this.consecutiveFailures = 0;
|
|
56
|
+
|
|
50
57
|
/**
|
|
51
58
|
* loan analytics instance
|
|
52
59
|
* @see loan-analytics.js
|
|
@@ -63,12 +70,9 @@ export class LoanTokenPoller {
|
|
|
63
70
|
async bookAccessed() {
|
|
64
71
|
if (this.borrowType) {
|
|
65
72
|
if (this.skipInitialCall) {
|
|
66
|
-
log(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
identifier: this.identifier,
|
|
70
|
-
}
|
|
71
|
-
);
|
|
73
|
+
log('[LoanTokenPoller] skipping initial create_token — already minted by the renewal response', {
|
|
74
|
+
identifier: this.identifier,
|
|
75
|
+
});
|
|
72
76
|
} else {
|
|
73
77
|
// Do an initial token, then set an interval
|
|
74
78
|
this.handleLoanTokenPoller(true);
|
|
@@ -116,6 +120,7 @@ export class LoanTokenPoller {
|
|
|
116
120
|
identifier: this.identifier,
|
|
117
121
|
isInitial,
|
|
118
122
|
});
|
|
123
|
+
this.consecutiveFailures = 0;
|
|
119
124
|
if (isInitial) this.successCallback();
|
|
120
125
|
},
|
|
121
126
|
});
|
|
@@ -133,17 +138,23 @@ export class LoanTokenPoller {
|
|
|
133
138
|
*/
|
|
134
139
|
handleTokenError(data, isInitial) {
|
|
135
140
|
const action = 'create_token';
|
|
141
|
+
this.consecutiveFailures += 1;
|
|
142
|
+
const { consecutiveFailures } = this;
|
|
136
143
|
|
|
137
144
|
log('[LoanTokenPoller] create_token failed', {
|
|
138
145
|
identifier: this.identifier,
|
|
139
146
|
isInitial,
|
|
147
|
+
consecutiveFailures,
|
|
140
148
|
error: data?.error,
|
|
141
149
|
});
|
|
142
150
|
|
|
143
|
-
// isInitial
|
|
144
|
-
// open at all" from "a mid-read refresh
|
|
151
|
+
// isInitial and consecutiveFailures ride along so the consumer can
|
|
152
|
+
// distinguish "the book won't open at all" from "a mid-read refresh
|
|
153
|
+
// blipped", and a one-off blip from a run of them — see
|
|
145
154
|
// IABookActions.handleLendingActionError.
|
|
146
|
-
this.errorCallback({
|
|
155
|
+
this.errorCallback({
|
|
156
|
+
detail: { action, data, isInitial, consecutiveFailures },
|
|
157
|
+
});
|
|
147
158
|
|
|
148
159
|
// send error to Sentry
|
|
149
160
|
sentryCaptureMessage(
|
|
@@ -154,6 +165,7 @@ export class LoanTokenPoller {
|
|
|
154
165
|
extra: {
|
|
155
166
|
borrowType: this.borrowType,
|
|
156
167
|
isInitial,
|
|
168
|
+
consecutiveFailures,
|
|
157
169
|
error: data?.error,
|
|
158
170
|
status: data?.status,
|
|
159
171
|
nonJsonResponse: data?.nonJsonResponse === true,
|
package/src/ia-book-actions.js
CHANGED
|
@@ -42,6 +42,16 @@ export const modalButtonStyle = {
|
|
|
42
42
|
'background:none;font-size:inherit;border:0;padding:0;color:#0000ee;cursor:pointer;text-decoration:underline',
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* How many create_token refreshes in a row may fail without an answer from
|
|
47
|
+
* the service before the patron is shown the error modal. Each successful
|
|
48
|
+
* refresh mints an access token good for 10 minutes (petabox
|
|
49
|
+
* LendingConfig::BOOKREADER_AUTH_SECONDS) and refreshes run every 2 minutes
|
|
50
|
+
* (tokenDelay), so the 3rd failure in a row (~6 minutes on) still shows the
|
|
51
|
+
* modal before that token runs out and pages stop loading.
|
|
52
|
+
*/
|
|
53
|
+
export const MAX_SILENT_TOKEN_REFRESH_FAILURES = 2;
|
|
54
|
+
|
|
45
55
|
export default class IABookActions extends LitElement {
|
|
46
56
|
static get properties() {
|
|
47
57
|
return {
|
|
@@ -1014,36 +1024,44 @@ export default class IABookActions extends LitElement {
|
|
|
1014
1024
|
recoveringFromLoanExpiry: this.recoveringFromLoanExpiry,
|
|
1015
1025
|
});
|
|
1016
1026
|
|
|
1027
|
+
const consecutiveFailures = event?.detail?.consecutiveFailures ?? 1;
|
|
1017
1028
|
if (
|
|
1018
1029
|
action === 'create_token' &&
|
|
1019
1030
|
!isInitial &&
|
|
1020
|
-
eventData?.nonJsonResponse === true
|
|
1021
|
-
|
|
1031
|
+
(eventData?.nonJsonResponse === true ||
|
|
1032
|
+
eventData?.networkFailure === true) &&
|
|
1033
|
+
consecutiveFailures <= MAX_SILENT_TOKEN_REFRESH_FAILURES
|
|
1022
1034
|
) {
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
//
|
|
1026
|
-
//
|
|
1027
|
-
//
|
|
1028
|
-
// and
|
|
1035
|
+
// The service never answered this routine refresh: something in
|
|
1036
|
+
// front of it did (the WAF's /about/400.html, an outage page), or the
|
|
1037
|
+
// request never got through. That says nothing about the loan, so
|
|
1038
|
+
// no modal and no session teardown — it's already reported to Sentry
|
|
1039
|
+
// (ActionsHandlerService/LoanTokenPoller), the current access token
|
|
1040
|
+
// keeps the book readable, and the poll interval is left running, so
|
|
1041
|
+
// the next tick tries again. The service's own answer (e.g. "You do
|
|
1042
|
+
// not currently have this book borrowed.") still falls through below.
|
|
1029
1043
|
//
|
|
1030
1044
|
// Not for the initial call: its success is what runs
|
|
1031
1045
|
// lendingBarPostInit() and opens the book (see startLoanTokenPoller),
|
|
1032
1046
|
// so suppressing it would leave a reader that never loads, silently.
|
|
1047
|
+
//
|
|
1048
|
+
// And not forever: past MAX_SILENT_TOKEN_REFRESH_FAILURES in a row,
|
|
1049
|
+
// the last good access token is about to expire, so fall through to
|
|
1050
|
+
// the modal rather than let the reader break with nothing shown.
|
|
1033
1051
|
log(
|
|
1034
|
-
'[IABookActions] create_token
|
|
1052
|
+
'[IABookActions] create_token routine refresh got no answer from the service — no modal',
|
|
1035
1053
|
{
|
|
1036
1054
|
identifier: this.identifier,
|
|
1037
1055
|
status: eventData?.status,
|
|
1056
|
+
errorMsg,
|
|
1038
1057
|
}
|
|
1039
1058
|
);
|
|
1040
1059
|
return;
|
|
1041
1060
|
}
|
|
1042
1061
|
|
|
1043
1062
|
if (action === 'create_token') {
|
|
1044
|
-
// Any other create_token failure — initial or a routine
|
|
1045
|
-
//
|
|
1046
|
-
// longer be confirmed (e.g. a stale loan-record
|
|
1063
|
+
// Any other create_token failure — initial, or a routine refresh the
|
|
1064
|
+
// service itself answered — means access can no longer be confirmed (e.g. a stale loan-record
|
|
1047
1065
|
// cache on some other node can make this request fail even though
|
|
1048
1066
|
// the loan is fine, or the loan may genuinely have just been
|
|
1049
1067
|
// returned via BookReaderImages.php's revokeAccess()). Don't retry
|
|
@@ -59,6 +59,17 @@ describe('ActionsHandlerService#parseServiceResponse', () => {
|
|
|
59
59
|
expect(context.extra.bodySnippet).to.contain('<!DOCTYPE');
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
it("treats a JSON body as the service's answer even without a JSON content type", async () => {
|
|
63
|
+
const body = { success: true, token: 'abc' };
|
|
64
|
+
const response = new Response(JSON.stringify(body), {
|
|
65
|
+
status: 200,
|
|
66
|
+
headers: { 'Content-Type': 'text/html' },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(await parseServiceResponse(response, option)).to.deep.equal(body);
|
|
70
|
+
expect(window.Sentry.captureMessage.called).to.be.false;
|
|
71
|
+
});
|
|
72
|
+
|
|
62
73
|
it('treats a JSON content type with an unparseable body as non-JSON', async () => {
|
|
63
74
|
const response = new Response('<!DOCTYPE html>', {
|
|
64
75
|
status: 502,
|
|
@@ -135,6 +146,7 @@ describe('ActionsHandlerService - thrown errors', () => {
|
|
|
135
146
|
|
|
136
147
|
expect(error.calledOnce).to.be.true;
|
|
137
148
|
expect(error.firstCall.args[0].error).to.equal(networkError);
|
|
149
|
+
expect(error.firstCall.args[0].networkFailure).to.be.true;
|
|
138
150
|
const [, context] = window.Sentry.captureException.firstCall.args;
|
|
139
151
|
expect(context.tags).to.deep.equal({
|
|
140
152
|
identifier: 'foo',
|
|
@@ -158,5 +170,7 @@ describe('ActionsHandlerService - thrown errors', () => {
|
|
|
158
170
|
expect(error.calledOnce).to.be.true;
|
|
159
171
|
expect(error.firstCall.args[0].error).to.equal(unexpectedError);
|
|
160
172
|
expect(error.firstCall.args[0].detail).to.contain('boom');
|
|
173
|
+
// the service did answer, so this isn't treated as "no answer"
|
|
174
|
+
expect(error.firstCall.args[0].networkFailure).to.be.undefined;
|
|
161
175
|
});
|
|
162
176
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expect } from '@open-wc/testing';
|
|
1
|
+
import { expect, aTimeout } from '@open-wc/testing';
|
|
2
2
|
import Sinon from 'sinon';
|
|
3
3
|
|
|
4
4
|
import { LoanTokenPoller } from '../../../src/core/services/loan-token-poller.js';
|
|
@@ -223,3 +223,46 @@ describe('handleTokenError - HTML 400 from the WAF (WEBDEV-9595)', () => {
|
|
|
223
223
|
poller.disconnectedCallback();
|
|
224
224
|
});
|
|
225
225
|
});
|
|
226
|
+
|
|
227
|
+
describe('consecutive create_token failures (WEBDEV-9595)', () => {
|
|
228
|
+
const makePoller = errorCallback =>
|
|
229
|
+
new LoanTokenPoller({
|
|
230
|
+
identifier: 'identifier1',
|
|
231
|
+
borrowType: 'borrowed',
|
|
232
|
+
successCallback: () => {},
|
|
233
|
+
errorCallback,
|
|
234
|
+
pollerDelay: 120,
|
|
235
|
+
skipInitialCall: true,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('passes the count of failures in a row to errorCallback', () => {
|
|
239
|
+
const errorCallback = Sinon.spy();
|
|
240
|
+
const poller = makePoller(errorCallback);
|
|
241
|
+
|
|
242
|
+
poller.handleTokenError({ error: 'x' }, false);
|
|
243
|
+
poller.handleTokenError({ error: 'x' }, false);
|
|
244
|
+
poller.handleTokenError({ error: 'x' }, false);
|
|
245
|
+
|
|
246
|
+
expect(
|
|
247
|
+
errorCallback.getCalls().map(c => c.args[0].detail.consecutiveFailures)
|
|
248
|
+
).to.deep.equal([1, 2, 3]);
|
|
249
|
+
poller.disconnectedCallback();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('resets the count after a successful refresh', async () => {
|
|
253
|
+
const errorCallback = Sinon.spy();
|
|
254
|
+
const poller = makePoller(errorCallback);
|
|
255
|
+
|
|
256
|
+
poller.handleTokenError({ error: 'x' }, false);
|
|
257
|
+
poller.handleTokenError({ error: 'x' }, false);
|
|
258
|
+
// on localhost the service fakes a create_token success
|
|
259
|
+
poller.handleLoanTokenPoller(false);
|
|
260
|
+
await aTimeout(300);
|
|
261
|
+
poller.handleTokenError({ error: 'x' }, false);
|
|
262
|
+
|
|
263
|
+
expect(errorCallback.lastCall.args[0].detail.consecutiveFailures).to.equal(
|
|
264
|
+
1
|
|
265
|
+
);
|
|
266
|
+
poller.disconnectedCallback();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { html, fixture, expect, aTimeout } from '@open-wc/testing';
|
|
2
2
|
import '@internetarchive/modal-manager';
|
|
3
|
-
import '../src/ia-book-actions.js';
|
|
4
3
|
import Sinon from 'sinon';
|
|
5
4
|
|
|
6
5
|
import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
|
|
7
6
|
import { LocalCache } from '@internetarchive/local-cache';
|
|
8
|
-
import {
|
|
7
|
+
import { MAX_SILENT_TOKEN_REFRESH_FAILURES } from '../src/ia-book-actions.js';
|
|
8
|
+
import {
|
|
9
|
+
unexpectedResponseError,
|
|
10
|
+
networkError,
|
|
11
|
+
} from '../src/core/services/actions-handler/actions-handler-service.js';
|
|
9
12
|
|
|
10
13
|
beforeEach(() => {
|
|
11
14
|
const modalManager = document.createElement('modal-manager');
|
|
@@ -1734,93 +1737,157 @@ describe('Sentry reports (WEBDEV-9595)', () => {
|
|
|
1734
1737
|
});
|
|
1735
1738
|
});
|
|
1736
1739
|
|
|
1737
|
-
describe('handleLendingActionError -
|
|
1740
|
+
describe('handleLendingActionError - routine create_token refresh failures (WEBDEV-9595)', () => {
|
|
1738
1741
|
const htmlBadRequest = {
|
|
1739
1742
|
success: false,
|
|
1740
1743
|
error: unexpectedResponseError(400),
|
|
1741
1744
|
status: 400,
|
|
1742
1745
|
nonJsonResponse: true,
|
|
1743
1746
|
};
|
|
1747
|
+
// the service never answered: no modal on a routine refresh
|
|
1748
|
+
const noAnswer = {
|
|
1749
|
+
'an HTML 400 (WAF block)': htmlBadRequest,
|
|
1750
|
+
'an HTML 502': {
|
|
1751
|
+
...htmlBadRequest,
|
|
1752
|
+
error: unexpectedResponseError(502),
|
|
1753
|
+
status: 502,
|
|
1754
|
+
},
|
|
1755
|
+
'a blocked or failed request': {
|
|
1756
|
+
error: networkError,
|
|
1757
|
+
detail: 'TypeError: Failed to fetch',
|
|
1758
|
+
networkFailure: true,
|
|
1759
|
+
},
|
|
1760
|
+
};
|
|
1761
|
+
// the service answered "no": the loan may really be gone
|
|
1762
|
+
const serviceAnswer = {
|
|
1763
|
+
success: false,
|
|
1764
|
+
error: 'You do not currently have this book borrowed.',
|
|
1765
|
+
status: 400,
|
|
1766
|
+
};
|
|
1744
1767
|
const liveLoan = {
|
|
1745
1768
|
is_lendable: true,
|
|
1746
1769
|
user_has_borrowed: true,
|
|
1747
1770
|
browsingExpired: false,
|
|
1748
1771
|
secondsLeftOnLoan: 300,
|
|
1749
1772
|
};
|
|
1750
|
-
|
|
1751
|
-
it('shows no modal and keeps the session going on a routine refresh', async () => {
|
|
1773
|
+
const setup = async () => {
|
|
1752
1774
|
const el = await fixture(
|
|
1753
1775
|
container({
|
|
1754
1776
|
userid: '@user1',
|
|
1755
|
-
identifier: '
|
|
1777
|
+
identifier: 'token-refresh-failure',
|
|
1756
1778
|
lendingStatus: liveLoan,
|
|
1757
1779
|
})
|
|
1758
1780
|
);
|
|
1759
1781
|
await el.updateComplete;
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1782
|
+
return el;
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
for (const [name, data] of Object.entries(noAnswer)) {
|
|
1786
|
+
it(`shows no modal and keeps the session going for ${name}`, async () => {
|
|
1787
|
+
const el = await setup();
|
|
1788
|
+
const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
|
|
1789
|
+
const clearAllSpy = Sinon.spy(window.IALendingIntervals, 'clearAll');
|
|
1790
|
+
el.tokenPoller = { disconnectedCallback: Sinon.spy() };
|
|
1791
|
+
|
|
1792
|
+
try {
|
|
1793
|
+
el.handleLendingActionError({
|
|
1794
|
+
detail: { action: 'create_token', isInitial: false, data },
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
expect(showErrorModalSpy.called).to.be.false;
|
|
1798
|
+
// none of the create_token teardown ran
|
|
1799
|
+
expect(clearAllSpy.called).to.be.false;
|
|
1800
|
+
expect(el.tokenPoller.disconnectedCallback.called).to.be.false;
|
|
1801
|
+
expect(el.lendingStatus.secondsLeftOnLoan).to.equal(300);
|
|
1802
|
+
expect(el.lendingStatus.available_to_browse).to.be.undefined;
|
|
1803
|
+
} finally {
|
|
1804
|
+
clearAllSpy.restore();
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
it(`still shows the modal for ${name} when the book is first opening`, async () => {
|
|
1809
|
+
const el = await setup();
|
|
1810
|
+
const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
|
|
1763
1811
|
|
|
1764
|
-
try {
|
|
1765
1812
|
el.handleLendingActionError({
|
|
1766
|
-
detail: { action: 'create_token', isInitial:
|
|
1813
|
+
detail: { action: 'create_token', isInitial: true, data },
|
|
1767
1814
|
});
|
|
1768
1815
|
|
|
1769
|
-
expect(showErrorModalSpy.
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
expect(el.tokenPoller.disconnectedCallback.called).to.be.false;
|
|
1773
|
-
expect(el.lendingStatus.secondsLeftOnLoan).to.equal(300);
|
|
1774
|
-
expect(el.lendingStatus.available_to_browse).to.be.undefined;
|
|
1775
|
-
} finally {
|
|
1776
|
-
clearAllSpy.restore();
|
|
1777
|
-
}
|
|
1778
|
-
});
|
|
1816
|
+
expect(showErrorModalSpy.calledOnce).to.be.true;
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1779
1819
|
|
|
1780
|
-
it(
|
|
1781
|
-
const el = await
|
|
1782
|
-
container({
|
|
1783
|
-
userid: '@user1',
|
|
1784
|
-
identifier: 'html-400-initial',
|
|
1785
|
-
lendingStatus: liveLoan,
|
|
1786
|
-
})
|
|
1787
|
-
);
|
|
1788
|
-
await el.updateComplete;
|
|
1820
|
+
it("still shows the modal and resets the bar when the service itself says no", async () => {
|
|
1821
|
+
const el = await setup();
|
|
1789
1822
|
const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
|
|
1790
1823
|
|
|
1791
1824
|
el.handleLendingActionError({
|
|
1792
|
-
detail: { action: 'create_token', isInitial:
|
|
1825
|
+
detail: { action: 'create_token', isInitial: false, data: serviceAnswer },
|
|
1793
1826
|
});
|
|
1794
1827
|
|
|
1795
|
-
expect(
|
|
1828
|
+
expect(
|
|
1829
|
+
showErrorModalSpy.calledOnceWith(serviceAnswer.error, 'create_token')
|
|
1830
|
+
).to.be.true;
|
|
1831
|
+
expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
|
|
1796
1832
|
});
|
|
1833
|
+
});
|
|
1797
1834
|
|
|
1798
|
-
|
|
1835
|
+
describe('handleLendingActionError - a run of unanswered token refreshes (WEBDEV-9595)', () => {
|
|
1836
|
+
const blocked = {
|
|
1837
|
+
error: networkError,
|
|
1838
|
+
detail: 'TypeError: Failed to fetch',
|
|
1839
|
+
networkFailure: true,
|
|
1840
|
+
};
|
|
1841
|
+
const setup = async () => {
|
|
1799
1842
|
const el = await fixture(
|
|
1800
1843
|
container({
|
|
1801
1844
|
userid: '@user1',
|
|
1802
|
-
identifier: '
|
|
1803
|
-
lendingStatus:
|
|
1845
|
+
identifier: 'token-refresh-run',
|
|
1846
|
+
lendingStatus: {
|
|
1847
|
+
is_lendable: true,
|
|
1848
|
+
user_has_borrowed: true,
|
|
1849
|
+
browsingExpired: false,
|
|
1850
|
+
secondsLeftOnLoan: 300,
|
|
1851
|
+
},
|
|
1804
1852
|
})
|
|
1805
1853
|
);
|
|
1806
1854
|
await el.updateComplete;
|
|
1855
|
+
return el;
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
it(`stays silent for up to ${MAX_SILENT_TOKEN_REFRESH_FAILURES} failures in a row`, async () => {
|
|
1859
|
+
const el = await setup();
|
|
1860
|
+
const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
|
|
1861
|
+
|
|
1862
|
+
for (let n = 1; n <= MAX_SILENT_TOKEN_REFRESH_FAILURES; n += 1) {
|
|
1863
|
+
el.handleLendingActionError({
|
|
1864
|
+
detail: {
|
|
1865
|
+
action: 'create_token',
|
|
1866
|
+
isInitial: false,
|
|
1867
|
+
consecutiveFailures: n,
|
|
1868
|
+
data: blocked,
|
|
1869
|
+
},
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
expect(showErrorModalSpy.called).to.be.false;
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
it('shows the modal and resets the bar on the next one, before the access token runs out', async () => {
|
|
1877
|
+
const el = await setup();
|
|
1807
1878
|
const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
|
|
1808
1879
|
|
|
1809
1880
|
el.handleLendingActionError({
|
|
1810
1881
|
detail: {
|
|
1811
1882
|
action: 'create_token',
|
|
1812
1883
|
isInitial: false,
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
});
|
|
1816
|
-
el.handleLendingActionError({
|
|
1817
|
-
detail: {
|
|
1818
|
-
action: 'create_token',
|
|
1819
|
-
isInitial: false,
|
|
1820
|
-
data: { ...htmlBadRequest, status: 502 },
|
|
1884
|
+
consecutiveFailures: MAX_SILENT_TOKEN_REFRESH_FAILURES + 1,
|
|
1885
|
+
data: blocked,
|
|
1821
1886
|
},
|
|
1822
1887
|
});
|
|
1823
1888
|
|
|
1824
|
-
expect(showErrorModalSpy.
|
|
1889
|
+
expect(showErrorModalSpy.calledOnceWith(networkError, 'create_token')).to
|
|
1890
|
+
.be.true;
|
|
1891
|
+
expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
|
|
1825
1892
|
});
|
|
1826
1893
|
});
|