All checks were successful
build-apk / build (push) Successful in 49m41s
App: - When the single-estimate fallback sees MaxTime 0 (or an all-$0.00 ladder), show "Parking is currently free" instead of a bogus $0.00 purchase ladder. Free-detection extracted to tested client helpers (isFreeEstimate/ladderAllFree). Client + tests (node:test, zero deps): - src/estimates.ts: pure isFreeEstimate/ladderAllFree helpers (exported). - test/estimates + test/http (mocked fetch): headers, query serialization, error->ParkSmarterApiError, rolling Auth_Token refresh, 401 clears token. - test/integration: live checks against the Sandpoint (DSB) zones; targets the `test` instance by default and SKIPS when it's unreachable (non-prod is IP-restricted -> 403). Verified 3/3 green against prodv2 read-only. - CI runs `npm test` as a build gate. Note: dev/stage/test instances return 403 "Web App - Unavailable" from the public internet (IP-restricted). API schema is otherwise unchanged vs Jul 6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
81 lines
3.4 KiB
JavaScript
81 lines
3.4 KiB
JavaScript
// Client HTTP behavior via an injected mock fetch (no network).
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { ParkSmarterClient, ParkSmarterApiError, MemoryTokenStore } from '../dist/index.js';
|
|
|
|
/** Build a fake fetch that records calls and returns canned responses. */
|
|
function mockFetch(handler) {
|
|
const calls = [];
|
|
const fetchImpl = async (url, init) => {
|
|
calls.push({ url: String(url), init });
|
|
const { status = 200, body = {} } = handler({ url: String(url), init, calls }) ?? {};
|
|
const text = typeof body === 'string' ? body : JSON.stringify(body);
|
|
return {
|
|
status,
|
|
ok: status >= 200 && status < 300,
|
|
text: async () => text,
|
|
headers: new Map(),
|
|
};
|
|
};
|
|
return { fetchImpl, calls };
|
|
}
|
|
|
|
test('every request carries Application_Token and a fresh X-Request-Id', async () => {
|
|
const { fetchImpl, calls } = mockFetch(() => ({ body: { Response: { Status: 'Success' } } }));
|
|
const ps = new ParkSmarterClient({ environment: 'prodv2', fetchImpl });
|
|
await ps.getApplicationValidity();
|
|
const h = calls[0].init.headers;
|
|
assert.ok(h['Application_Token'], 'Application_Token present');
|
|
assert.match(h['X-Request-Id'], /[0-9a-f]{8}-[0-9a-f]{4}/i, 'X-Request-Id is a UUID');
|
|
});
|
|
|
|
test('single-estimate query serializes the ParkSmarter param names', async () => {
|
|
const { fetchImpl, calls } = mockFetch(() => ({
|
|
body: { ParkingDetail: {}, MinTime: 5, MaxTime: 33, Response: { Status: 'Success' } },
|
|
}));
|
|
const ps = new ParkSmarterClient({ environment: 'prodv2', fetchImpl });
|
|
await ps.getParkingEstimateSingle({
|
|
zoneId: 113165, spaceId: 329522, customerId: 217, vehicleId: 42,
|
|
durationInMinutes: 30, creditCardId: 0,
|
|
});
|
|
const url = calls[0].url;
|
|
assert.match(url, /ZoneID=113165/);
|
|
assert.match(url, /SpaceID=329522/);
|
|
assert.match(url, /CustomerID=217/);
|
|
assert.match(url, /ParkingDuration=30/);
|
|
assert.match(url, /VehicleID=42/);
|
|
});
|
|
|
|
test('a non-2xx response throws ParkSmarterApiError carrying the status', async () => {
|
|
const { fetchImpl } = mockFetch(() => ({ status: 500, body: { message: 'boom' } }));
|
|
const ps = new ParkSmarterClient({ environment: 'prodv2', fetchImpl });
|
|
await assert.rejects(
|
|
() => ps.getApplicationValidity(),
|
|
(e) => e instanceof ParkSmarterApiError && e.status === 500,
|
|
);
|
|
});
|
|
|
|
test('rolling Auth_Token from the Response envelope is stored and re-sent', async () => {
|
|
const tokens = new MemoryTokenStore();
|
|
const { fetchImpl, calls } = mockFetch(() => ({
|
|
body: { Response: { Status: 'Success', Auth_Token: 'REFRESHED123' } },
|
|
}));
|
|
const ps = new ParkSmarterClient({ environment: 'prodv2', fetchImpl, tokens });
|
|
await ps.getUserDetail();
|
|
await ps.getUserDetail();
|
|
assert.equal(calls[1].init.headers['Auth_Token'], 'REFRESHED123');
|
|
assert.equal(await tokens.getAuthToken(), 'REFRESHED123');
|
|
});
|
|
|
|
test('a 401 clears the auth token and invokes onUnauthorized', async () => {
|
|
const tokens = new MemoryTokenStore();
|
|
await tokens.setAuthToken('OLD');
|
|
let unauthorized = 0;
|
|
const { fetchImpl } = mockFetch(() => ({ status: 401, body: { Response: { Status: 'Error' } } }));
|
|
const ps = new ParkSmarterClient({
|
|
environment: 'prodv2', fetchImpl, tokens, onUnauthorized: () => { unauthorized += 1; },
|
|
});
|
|
await assert.rejects(() => ps.getUserDetail());
|
|
assert.equal(unauthorized, 1);
|
|
assert.equal(await tokens.getAuthToken(), null);
|
|
});
|