// Live integration tests against a ParkSmarter instance, using the Sandpoint // zones. Targets the `test` instance by default (PS_TEST_ENV to override). // // NOTE: the non-prod instances (dev/stage/test) are IP-restricted to IPS's // network and return 403 "Web App - Unavailable" from the public internet, so // these SKIP with a clear reason unless the instance is reachable. To run them // live against prod (read-only) with the Sandpoint zones: // PS_TEST_ENV=prodv2 node --test test/integration.test.mjs // (needs parksmarter-client/.creds.json for the authenticated calls). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, existsSync } from 'node:fs'; import { ParkSmarterClient, isFreeEstimate } from '../dist/index.js'; const ENV = process.env.PS_TEST_ENV || 'test'; const SANDPOINT = { latitude: 48.27538, longitude: -116.54779 }; const credsUrl = new URL('../.creds.json', import.meta.url); const creds = existsSync(credsUrl) ? JSON.parse(readFileSync(credsUrl, 'utf8')) : {}; // Probe reachability once; if the instance blocks us, skip the whole suite. let reason = false; try { const av = await new ParkSmarterClient({ environment: ENV, timeoutMs: 12000 }).getApplicationValidity(); if (!av || (!av.SessionId && !av.Config)) throw new Error('no bootstrap payload'); } catch (e) { reason = `'${ENV}' instance unreachable (${e.name} ${e.status ?? ''}); non-prod is IP-restricted. ` + `Run PS_TEST_ENV=prodv2 with .creds.json to exercise these live.`; } async function client() { const ps = new ParkSmarterClient({ environment: ENV, timeoutMs: 15000 }); if (creds.phoneNumber) await ps.loginWithPhone({ phoneNumber: creds.phoneNumber, password: creds.password }); return ps; } test(`[${ENV}] ApplicationValidity bootstraps a session`, { skip: reason }, async () => { const av = await new ParkSmarterClient({ environment: ENV }).getApplicationValidity(); assert.ok(av.SessionId || av.Config, 'expected a bootstrap payload'); }); test(`[${ENV}] Sandpoint (DSB) zones are returned near 48.27,-116.55`, { skip: reason }, async () => { const ps = await client(); const m = await ps.getMetersByLocation(SANDPOINT); assert.ok(Array.isArray(m.Zones) && m.Zones.length > 0, 'expected zones near Sandpoint'); const names = m.Zones.map((z) => z.ZoneName || ''); assert.ok(names.some((n) => /^DSB/i.test(n)), `expected DSB* zones, got: ${names.slice(0, 8).join(',')}`); }); test(`[${ENV}] single estimate is well-formed and free/paid is coherent`, { skip: reason }, async () => { const ps = await client(); const me = creds.phoneNumber ? await ps.getUserDetail() : null; const vehicleId = me?.VehicleDetails?.[0]?.VehicleID ?? 0; const m = await ps.getMetersByLocation(SANDPOINT); const z = m.Zones[0]; const sp = z.Spaces?.[0]; const base = { zoneId: z.ZoneId, spaceId: sp.SpaceId, customerId: z.CustomerId, vehicleId }; const single = await ps.getParkingEstimateSingle({ ...base, durationInMinutes: 30, creditCardId: 0 }); assert.equal(single.Response?.Status, 'Success', 'single estimate should succeed'); // Multi may be empty or error for some zones/windows — assert only the shape. const multi = await ps.getParkingEstimateMulti(base); assert.ok(Object.prototype.hasOwnProperty.call(multi, 'ParkingDetail'), 'multi has ParkingDetail'); if (isFreeEstimate(single)) { assert.equal(Number(single.MaxTime) || 0, 0, 'free window => MaxTime 0'); } else { assert.ok(Number(single.ParkingDetail?.ParkingCost) >= 0, 'paid window => a numeric cost'); } });