v3.25.6: the PLI dictionary save feedback + the timer wording
The v3.25.5 report's follow-ups: a field the server could not use was silently dropped, and the PWA also filtered non-hex fields client-side (never sending them). The POST now collects them and names them in the response - `invalid` (a malformed key/value: not hex, an odd number of digits, a non-string value) and `unknown` (a well-formed code outside the qualifier list), both omitted when everything was applied - and the PWA's Save sends every field, composes "Not saved: 0A (invalid value), 18 (unknown qualifier)" (`pysimPliRejectMsg`) into a new #pli-save-msg line under the button and shows "Not saved" briefly. The guide's timer exception now says "card-initiated automatic action" (the timer retries every 5 s; polling/menu are suspended, not retrying). Docs: api.md's POST contract, help EN/RU - which also said "22 PLI qualifiers" while the list has 25. Tests: the rejection lists per case (odd length, non-hex value/key, non-string, unknown code) and the valid-save absence, the PWA message helper and the wiring pins. 887 frontend + 875 Python; sw cache simple-v430.
This commit is contained in:
+10
-4
@@ -1520,11 +1520,17 @@ override is set.
|
||||
|
||||
### `POST /api/pli-dict`
|
||||
|
||||
Updates dictionary entries. Body is a map of qualifier code to hex value;
|
||||
keys must be known qualifiers and values valid hex, otherwise they are
|
||||
ignored. A non-empty value becomes an override; an **empty value clears the
|
||||
Updates dictionary entries. Body is a map of qualifier code to hex value.
|
||||
A non-empty value becomes an override; an **empty value clears the
|
||||
override**, so the built-in default (if any) applies again. Returns the
|
||||
updated effective dictionary.
|
||||
updated effective dictionary plus, when any field was not saved, the lists
|
||||
that name them: `invalid` (a malformed key or value — not hex, an odd number
|
||||
of digits, a non-string value) and `unknown` (a well-formed code outside the
|
||||
qualifier list); both keys are omitted when everything was applied (v3.25.6).
|
||||
|
||||
```json
|
||||
{"00": "130500F1100000", "0A": "630102", "invalid": ["0G"], "unknown": ["18"]}
|
||||
```
|
||||
|
||||
### `POST /api/scp81/bip`
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@ the data really flows to/from the configured BIP target, and the BIP events
|
||||
(Data available, Channel status) flow for the channels the card itself opened -
|
||||
so an applet's own HTTP/BIP conversation completes inside a scripted run. The
|
||||
BIP **control** API stays blocked while a run is active (409), like the
|
||||
terminal's other automatic actions (polling, menu). The one remaining
|
||||
terminal's other automatic actions (polling, menu). The one card-initiated
|
||||
automatic action that still waits is the **timer expiration** event: a
|
||||
TIMER MANAGEMENT timer the card armed is not fired during a run (it retries
|
||||
every 5 s and fires when the run ends) - a deliberate exception to the
|
||||
|
||||
@@ -527,14 +527,14 @@
|
||||
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Фоновый <strong>опрос включён по умолчанию</strong> (CAT-терминал опрашивает карту в режиме ожидания, TS 102 221 §14.6.2): после интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1–255 с, по умолчанию 30 с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. Переключатель включает и выключает опрос; команда карты <strong>POLL INTERVAL</strong> обновляет интервал (TS 102 223 §6.4.6; TERMINAL RESPONSE сообщает фактически используемый интервал), а после <strong>POLLING OFF</strong> переключатель показывает <strong>OFF</strong> янтарным цветом до нового POLL INTERVAL — ручная кнопка <strong>Отправить STATUS</strong> продолжает работать (на presence detection и ручной STATUS POLLING OFF не влияет). При извлечении карты опрос отключается и включается снова при следующем equip.</p>
|
||||
|
||||
<h3 id="pli-dict" class="text-lg font-medium mb-2">8.6 «Конфигурация TR» — данные ответа PROVIDE LOCAL INFORMATION</h3>
|
||||
<p class="text-sm mb-2">Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 §8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:</p>
|
||||
<p class="text-sm mb-2">Редактируемые hex-значения для всех 25 квалификаторов PLI (TS 102 223 §8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
|
||||
<li><strong>01</strong> IMEI · <strong>03</strong> Дата/время/TZ · <strong>04</strong> Язык · <strong>05</strong> Timing Advance</li>
|
||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||
<li><strong>0A</strong> Battery · <strong>0E</strong> Multiple Access Technologies</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Для квалификаторов, определённых в спецификациях ETSI, заранее заданы разумные значения по умолчанию (местоположение — тестовая сеть 001/01, тестовый IMEI 490154203237518 и его IMEISV, язык «en», технология доступа E-UTRAN, автоматический поиск сети, полный заряд батареи): заданное значение переопределяет умолчание, а <strong>очистка поля возвращает умолчание</strong>; квалификатор даты/времени, пока пуст, отвечается часами хоста, а специфичные для 3GPP/3GPP2 квалификаторы (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) остаются пустыми — спецификации ETSI не определяют для них содержимое. Когда карта выдаёт PLI, сервер вставляет значения в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.</p>
|
||||
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Для квалификаторов, определённых в спецификациях ETSI, заранее заданы разумные значения по умолчанию (местоположение — тестовая сеть 001/01, тестовый IMEI 490154203237518 и его IMEISV, язык «en», технология доступа E-UTRAN, автоматический поиск сети, полный заряд батареи): заданное значение переопределяет умолчание, а <strong>очистка поля возвращает умолчание</strong>; квалификатор даты/времени, пока пуст, отвечается часами хоста, а специфичные для 3GPP/3GPP2 квалификаторы (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) остаются пустыми — спецификации ETSI не определяют для них содержимое. Поле, которое сервер не может использовать (значение не hex, нечётное число цифр, код вне списка квалификаторов), указывается под кнопкой «Сохранить», а не отбрасывается молча. Когда карта выдаёт PLI, сервер вставляет значения в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.</p>
|
||||
|
||||
<h3 id="network-simulation" class="text-lg font-medium mb-2">8.7 Симуляция сети</h3>
|
||||
<p class="text-sm mb-2">Воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс в <code class="font-mono text-sm">projects/UICC_NAA.md</code>): подключение EPS, потеря сервиса / ограниченный сервис, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast и AUTHENTICATE. По кнопке на сценарий; карта должна быть подключена.</p>
|
||||
|
||||
+2
-2
@@ -526,14 +526,14 @@
|
||||
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). Background <strong>Polling</strong> is on by default (a CAT terminal polls during idle, TS 102 221 §14.6.2): after the idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1–255 s, default 30 s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. The toggle turns it off/on; the card's <strong>POLL INTERVAL</strong> command updates the interval (TS 102 223 §6.4.6; the TERMINAL RESPONSE reports the interval actually used), and after a <strong>POLLING OFF</strong> the toggle shows <strong>OFF</strong> in amber until a new POLL INTERVAL arrives — the manual <strong>Send STATUS</strong> button keeps working (presence detection and manual STATUS are not affected by POLLING OFF). Polling is disabled when the card is removed and re-enabled by the next equip.</p>
|
||||
|
||||
<h3 id="pli-dict" class="text-lg font-medium mb-2">8.6 TR Config — PROVIDE LOCAL INFORMATION response data</h3>
|
||||
<p class="text-sm mb-2">Editable hex values for all 22 PLI qualifiers (TS 102 223 §8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:</p>
|
||||
<p class="text-sm mb-2">Editable hex values for all 25 PLI qualifiers (TS 102 223 §8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
|
||||
<li><strong>01</strong> IMEI · <strong>03</strong> Date/Time/TZ · <strong>04</strong> Language · <strong>05</strong> Timing Advance</li>
|
||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||
<li><strong>0A</strong> Battery · <strong>0E</strong> Multiple Access Technologies</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Values persist server-side until restart. Sensible defaults are pre-settled for the qualifiers the ETSI specs define (location as the test network 001/01, the test IMEI 490154203237518 and its IMEISV, language ‘en’, E-UTRAN access technology, automatic search mode, a full battery): a value you set overrides its default, and <strong>clearing the field restores it</strong>; the date/time qualifier is answered from the host clock while empty, and the 3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) stay empty - the ETSI specs define no contents for them. When the card issues PLI, the server injects the values into the TERMINAL RESPONSE - also into a test-script expectation's answer.</p>
|
||||
<p class="text-sm mb-3">Values persist server-side until restart. Sensible defaults are pre-settled for the qualifiers the ETSI specs define (location as the test network 001/01, the test IMEI 490154203237518 and its IMEISV, language ‘en’, E-UTRAN access technology, automatic search mode, a full battery): a value you set overrides its default, and <strong>clearing the field restores it</strong>; the date/time qualifier is answered from the host clock while empty, and the 3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) stay empty - the ETSI specs define no contents for them. A field the server cannot use (a value that is not hex, an odd number of digits, a code outside the qualifier list) is named under the Save button instead of being silently dropped. When the card issues PLI, the server injects the values into the TERMINAL RESPONSE - also into a test-script expectation's answer.</p>
|
||||
|
||||
<h3 id="network-simulation" class="text-lg font-medium mb-2">8.7 Network simulation</h3>
|
||||
<p class="text-sm mb-2">Replays the write patterns a real phone performs when the network condition changes (trace study in <code class="font-mono text-sm">projects/UICC_NAA.md</code>): EPS attach, service loss / limited service, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration and AUTHENTICATE. One button per scenario; the card must be equipped.</p>
|
||||
|
||||
+31
-10
@@ -1302,6 +1302,7 @@
|
||||
<div id="phone-sub-tr" class="hidden">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-sm text-gray-500 dark:text-slate-400" data-l10n="PROVIDE LOCAL INFORMATION response data">PROVIDE LOCAL INFORMATION response data</span>
|
||||
<span id="pli-save-msg" class="flex-1 mx-2 text-right text-xs text-red-600"></span>
|
||||
<button id="pli-save-btn" data-needs="server" onclick="pysimPliSave()" class="px-2.5 py-1 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Save">Save</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 dark:text-slate-500 mb-1" data-l10n="The date/time qualifier is answered from the host clock while its entry is empty.">The date/time qualifier is answered from the host clock while its entry is empty.</div>
|
||||
@@ -1905,7 +1906,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '3.25.5';
|
||||
const SIMPLE_VERSION = '3.25.6';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
@@ -18404,25 +18405,42 @@ function pysimPliApplyRow(code) {
|
||||
if (tog) tog.textContent = '▶';
|
||||
}
|
||||
|
||||
// The server names the fields it could not use (invalid value/key, unknown
|
||||
// qualifier) - show them instead of silently dropping the edit (v3.25.6).
|
||||
function pysimPliRejectMsg(resp) {
|
||||
const bad = [];
|
||||
((resp && resp.invalid) || []).forEach(c => bad.push(c + ' (' + t('invalid value') + ')'));
|
||||
((resp && resp.unknown) || []).forEach(c => bad.push(c + ' (' + t('unknown qualifier') + ')'));
|
||||
return bad.length ? t('Not saved') + ': ' + bad.join(', ') : '';
|
||||
}
|
||||
|
||||
async function pysimPliSave() {
|
||||
const btn = document.getElementById('pli-save-btn');
|
||||
const msg = document.getElementById('pli-save-msg');
|
||||
btn.disabled = true;
|
||||
btn.textContent = t('Saving...');
|
||||
if (msg) msg.textContent = '';
|
||||
const data = {};
|
||||
PLI_QUALIFIERS.forEach(q => {
|
||||
const el = document.getElementById('pli-v-' + q.code);
|
||||
if (el) {
|
||||
const v = el.value.trim().replace(/\s/g, '');
|
||||
if (v && !/^[0-9A-Fa-f]*$/.test(v)) return;
|
||||
data[q.code] = v;
|
||||
}
|
||||
// every field is sent - the server validates and names what it cannot
|
||||
// use (a bad value is no longer skipped silently, v3.25.6)
|
||||
if (el) data[q.code] = el.value.trim().replace(/\s/g, '');
|
||||
});
|
||||
try {
|
||||
await pysimFetch('/api/pli-dict', data);
|
||||
const resp = await pysimFetch('/api/pli-dict', data);
|
||||
const reject = pysimPliRejectMsg(resp);
|
||||
pysimPliRender(); // a cleared field shows its default again
|
||||
btn.textContent = t('Saved');
|
||||
btn.classList.add('bg-emerald-700');
|
||||
setTimeout(() => { btn.textContent = t('Save'); btn.classList.remove('bg-emerald-700'); btn.disabled = false; }, 1500);
|
||||
if (msg) msg.textContent = reject;
|
||||
if (reject) {
|
||||
btn.textContent = t('Not saved');
|
||||
btn.classList.add('bg-red-600');
|
||||
setTimeout(() => { btn.textContent = t('Save'); btn.classList.remove('bg-red-600'); btn.disabled = false; }, 2500);
|
||||
} else {
|
||||
btn.textContent = t('Saved');
|
||||
btn.classList.add('bg-emerald-700');
|
||||
setTimeout(() => { btn.textContent = t('Save'); btn.classList.remove('bg-emerald-700'); btn.disabled = false; }, 1500);
|
||||
}
|
||||
} catch (e) {
|
||||
btn.textContent = 'Error';
|
||||
btn.classList.add('bg-red-600');
|
||||
@@ -23435,6 +23453,9 @@ const LANG_RU = {
|
||||
'Saving...': 'Сохранение...',
|
||||
'Saved': 'Сохранено',
|
||||
'Save': 'Сохранить',
|
||||
'Not saved': 'Не сохранено',
|
||||
'invalid value': 'недопустимое значение',
|
||||
'unknown qualifier': 'неизвестный квалификатор',
|
||||
'Profiler': 'Профайлер',
|
||||
'New profile': 'Новый профиль',
|
||||
'Profile from card': 'Профиль с карты',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v429';
|
||||
const CACHE = 'simple-v430';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -87,3 +87,43 @@ test('the server PLI defaults decode through their codecs', () => {
|
||||
assert.deepStrictEqual(PLI_CODECS['09'].decode('650101'), { mode: '1' });
|
||||
assert.deepStrictEqual(PLI_CODECS['0A'].decode('630104'), { charge: '4' });
|
||||
});
|
||||
|
||||
// the same extraction the profiler tests use
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
test('a rejected PLI save is reported, not silently dropped', () => {
|
||||
// the server names the fields it could not use (invalid/unknown) and the
|
||||
// PWA shows them under the Save button (v3.25.6)
|
||||
global.t = s => s;
|
||||
eval(extractFunc(html, 'pysimPliRejectMsg'));
|
||||
assert.strictEqual(pysimPliRejectMsg({}), '');
|
||||
assert.strictEqual(pysimPliRejectMsg({ '0A': '630104' }), '');
|
||||
assert.strictEqual(pysimPliRejectMsg({ invalid: ['0A'] }),
|
||||
'Not saved: 0A (invalid value)');
|
||||
assert.strictEqual(pysimPliRejectMsg({ unknown: ['18'] }),
|
||||
'Not saved: 18 (unknown qualifier)');
|
||||
assert.strictEqual(pysimPliRejectMsg({ invalid: ['0A'], unknown: ['18'] }),
|
||||
'Not saved: 0A (invalid value), 18 (unknown qualifier)');
|
||||
// the save posts every field (no silent client-side skip) and wires the
|
||||
// message element + the reject branch
|
||||
assert.ok(html.includes('if (el) data[q.code] = el.value.trim()'),
|
||||
'every field is sent to the server for validation');
|
||||
assert.ok(html.includes('id="pli-save-msg"'), 'the message element exists');
|
||||
assert.ok(html.includes('const reject = pysimPliRejectMsg(resp);'),
|
||||
'the save reports the rejection');
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.25.5"
|
||||
version = "3.25.6"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.25.5'
|
||||
VERSION = '3.25.6'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -9103,25 +9103,41 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
elif self.path == '/api/pli-dict':
|
||||
body = self._read_body()
|
||||
self._log_req(body)
|
||||
invalid, unknown = [], []
|
||||
if isinstance(body, dict):
|
||||
for k, v in body.items():
|
||||
if isinstance(v, str):
|
||||
if not isinstance(v, str):
|
||||
invalid.append(k)
|
||||
continue
|
||||
try:
|
||||
code = int(k, 16)
|
||||
except ValueError:
|
||||
invalid.append(k)
|
||||
continue
|
||||
# a known qualifier - never "an override already exists":
|
||||
# a new override must stick, and a clear must not poison a
|
||||
# later set (v3.25.5, live report)
|
||||
if code not in PLI_QUALIFIER_NAMES:
|
||||
unknown.append(k)
|
||||
continue
|
||||
if v:
|
||||
try:
|
||||
code = int(k, 16)
|
||||
# a known qualifier - never "an override already
|
||||
# exists": a new override must stick, and a clear
|
||||
# must not poison a later set (v3.25.5, live report)
|
||||
if code in PLI_QUALIFIER_NAMES:
|
||||
if v:
|
||||
bytes.fromhex(v) # valid hex only
|
||||
_PLI_DATA[code] = v
|
||||
else:
|
||||
# clearing the field restores the default
|
||||
_PLI_DATA.pop(code, None)
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
bytes.fromhex(v) # valid hex only
|
||||
except ValueError:
|
||||
invalid.append(k)
|
||||
continue
|
||||
_PLI_DATA[code] = v
|
||||
else:
|
||||
# clearing the field restores the default
|
||||
_PLI_DATA.pop(code, None)
|
||||
resp = {('%02X' % q): (_PLI_DATA.get(q) or PLI_DEFAULTS.get(q, ''))
|
||||
for q in PLI_QUALIFIER_NAMES}
|
||||
# a field the server could not use is named in the response, never
|
||||
# silently dropped (v3.25.6): the PWA shows it under the Save button
|
||||
if invalid:
|
||||
resp['invalid'] = invalid
|
||||
if unknown:
|
||||
resp['unknown'] = unknown
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/poll-toggle':
|
||||
|
||||
@@ -199,11 +199,44 @@ class PliDictHttpTests(unittest.TestCase):
|
||||
status, resp = self._req('POST', '/api/pli-dict', {'18': 'AABB'})
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertNotIn(0x18, srv._PLI_DATA)
|
||||
self.assertEqual(resp['unknown'], ['18'])
|
||||
self.assertEqual(resp['0A'], '630103')
|
||||
finally:
|
||||
srv._PLI_DATA.clear()
|
||||
srv._PLI_DATA.update(saved)
|
||||
|
||||
def test_rejected_values_are_reported(self):
|
||||
# a field the server cannot use is named in the response, never
|
||||
# silently dropped (v3.25.6): the PWA shows it under the Save button
|
||||
import pysim_simple_server.server as srv
|
||||
saved = dict(srv._PLI_DATA)
|
||||
try:
|
||||
# an odd-length hex value
|
||||
status, resp = self._req('POST', '/api/pli-dict', {'0A': '63010'})
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertEqual(resp['invalid'], ['0A'])
|
||||
self.assertNotIn(0x0A, srv._PLI_DATA)
|
||||
self.assertEqual(resp['0A'], srv.PLI_DEFAULTS[0x0A])
|
||||
# a non-hex value and a non-hex key
|
||||
status, resp = self._req('POST', '/api/pli-dict', {'0A': 'ZZ', '0G': 'AA'})
|
||||
self.assertEqual(sorted(resp['invalid']), ['0A', '0G'])
|
||||
self.assertNotIn('unknown', resp)
|
||||
# a non-string value
|
||||
status, resp = self._req('POST', '/api/pli-dict', {'0A': 123})
|
||||
self.assertEqual(resp['invalid'], ['0A'])
|
||||
# an unknown (well-formed) qualifier
|
||||
status, resp = self._req('POST', '/api/pli-dict', {'18': 'AABB'})
|
||||
self.assertEqual(resp['unknown'], ['18'])
|
||||
self.assertNotIn('invalid', resp)
|
||||
# a valid save carries neither rejection list
|
||||
status, resp = self._req('POST', '/api/pli-dict', {'0A': '630102'})
|
||||
self.assertEqual(resp['0A'], '630102')
|
||||
self.assertNotIn('invalid', resp)
|
||||
self.assertNotIn('unknown', resp)
|
||||
finally:
|
||||
srv._PLI_DATA.clear()
|
||||
srv._PLI_DATA.update(saved)
|
||||
|
||||
|
||||
class CardFreeGetTests(unittest.TestCase):
|
||||
"""Cached GETs must answer while the card lock is held.
|
||||
|
||||
Reference in New Issue
Block a user