You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

127 lines
7.5 KiB

const test = require('node:test');
const assert = require('node:assert/strict');
const clear = require('../frontend/web/static/js/port-log-clear.js');
function node() {
return { children: [], dataset: {}, attributes: {}, events: {}, value: '', disabled: false,
append(...items) { this.children.push(...items); },
appendChild(item) { this.children.push(item); },
setAttribute(key, value) { this.attributes[key] = value; },
addEventListener(name, fn) { this.events[name] = fn; },
removeEventListener(name, fn) { if (this.events[name] === fn) delete this.events[name]; }
};
}
function setup(overrides = {}) {
const parent = node(), select = node(), calls = [], notices = [], errors = [], prompts = [];
const state = { answer: 'CH01', disposed: false, busy: 0, settled: 0 };
const options = {
kind: 'port', role: 'administrator', document: { createElement: node }, parent, channelField: select,
isDisposed: () => state.disposed,
confirm: text => { prompts.push(text); return state.answer; },
send: async body => { calls.push(body); return { channel: body.channel, deleted: 3 }; },
onBusy: () => state.busy++, onSettled: async () => state.settled++,
onSuccess: (channel, result) => notices.push({ channel, result }), onError: error => errors.push(error),
...overrides
};
const control = clear.mount(options);
const area = parent.children[0], button = area?.children[1], copy = area?.children[0];
function choose(value) { select.value = value; select.events.change?.(); }
return { parent, select, state, control, button, copy, area, choose, calls, notices, errors, prompts };
}
test('administrator clear control is absent on protected log pages and for operators', () => {
for (const kind of ['audit']) assert.equal(setup({ kind }).control, null);
for (const role of ['port_operator', '', 'viewer']) assert.equal(setup({ role }).control, null);
});
test('system clear includes audit records and requires exact SYSTEM confirmation', async () => {
const s=setup({kind:'system',channelField:null});
assert.equal(s.button.disabled,false); assert.equal(s.button.dataset.systemLogClear,'true');
assert.match(s.copy.children[1].textContent,/시스템 이벤트와 감사 기록 전체/);
for(const answer of [null,'','CH01','system',' SYSTEM ']){s.state.answer=answer;await s.button.events.click();}
assert.equal(s.calls.length,0);
s.state.answer='SYSTEM';await s.button.events.click();
assert.deepEqual(s.calls,[{confirmation:'SYSTEM'}]);
assert.match(s.prompts.at(-1),/포트 로그·포트 상태 이벤트는 보존/);
s.control.dispose();assert.equal(s.button.disabled,true);
});
test('clear supports all ports and all 48 individual ports but rejects invalid targets', async () => {
const s = setup();
assert.equal(s.button.textContent, '로그 클리어 (삭제)');
assert.equal(s.button.disabled, false);
assert.match(s.copy.children[0].textContent, /전체 포트/);
for (let i = 1; i <= 48; i++) { s.choose('CH' + String(i).padStart(2, '0')); assert.equal(s.button.disabled, false); }
for (const channel of ['CH00', 'CH49', 'ch01', '../system']) {
s.choose(channel); assert.equal(s.button.disabled, true); await s.button.events.click();
}
assert.equal(s.prompts.length, 0); assert.equal(s.calls.length, 0);
});
test('deletion scope is visible before confirmation, distinct from configuration backup', () => {
const s = setup(); s.choose('CH18');
assert.match(s.copy.children[0].textContent, /CH18/);
assert.match(s.copy.children[1].textContent, /기간·검색 조건·표시 개수와 관계없이/);
assert.match(s.copy.children[1].textContent, /새 로그 수집은 계속/);
assert.match(s.copy.children[2].textContent, /다른 포트와 시스템·감사·포트 상태 이벤트는 유지/);
assert.match(s.copy.children[2].textContent, /설정 백업에는 로그 원문이 포함되지/);
assert.equal(s.button.attributes['aria-describedby'], s.copy.children[1].id);
});
test('cancel, empty, wrong port, and whitespace confirmations never call the API', async () => {
const s = setup(); s.choose('CH01');
for (const answer of [null, '', 'CH02', 'ch01', ' CH01 ']) {
s.state.answer = answer; await s.button.events.click();
}
assert.equal(s.calls.length, 0); assert.equal(s.state.busy, 0);
assert.equal(s.notices.length, 0);
});
test('exact confirmation sends only one captured port and explains export limitations', async () => {
const s = setup(); s.choose('CH01'); await s.button.events.click();
assert.deepEqual(s.calls, [{ channel: 'CH01', confirmation: 'CH01' }]);
assert.match(s.prompts[0], /내보내기에는 현재 검색 조건과 표시 개수 제한/);
assert.equal(s.notices[0].result.deleted, 3); assert.equal(s.state.settled, 1);
assert.equal(s.button.disabled, false);
});
test('duplicate clicks and filter changes cannot retarget an in-flight deletion', async () => {
let finish;
const sent = [];
const s = setup({ send: body => { sent.push(body); return new Promise(resolve => { finish = resolve; }); } });
s.choose('CH01'); const pending = s.button.events.click();
assert.equal(s.button.disabled, true); assert.equal(s.area.attributes['aria-busy'], 'true');
s.choose('CH02'); await s.button.events.click();
assert.deepEqual(sent, [{ channel: 'CH01', confirmation: 'CH01' }]);
finish({ channel: 'CH01', deleted: 2 }); await pending;
assert.equal(s.notices[0].channel, 'CH01'); assert.equal(s.state.settled, 1);
assert.match(s.copy.children[0].textContent, /CH02/);
});
test('errors are reported without success and release refresh guards for retry', async () => {
const s = setup({ send: async () => { throw new Error('disk unavailable'); } });
s.choose('CH01'); await s.button.events.click();
assert.equal(s.errors[0].message, 'disk unavailable'); assert.equal(s.notices.length, 0);
assert.equal(s.button.disabled, false); assert.equal(s.state.settled, 1);
});
test('filter reset targets all ports, and leaving the page detaches the control', async () => {
const s = setup(); s.choose('CH01'); s.select.value = ''; s.control.update();
assert.equal(s.button.disabled, false);
assert.match(s.copy.children[0].textContent, /전체 포트/);
s.choose('CH01'); const oldClick = s.button.events.click; s.control.dispose();
assert.equal(s.button.disabled, true); assert.equal(s.select.events.change, undefined);
await oldClick(); assert.equal(s.calls.length, 0);
});
test('a response after navigation does not update a new page but releases the guard', async () => {
let finish;
const s = setup({ send: () => new Promise(resolve => { finish = resolve; }) });
s.choose('CH01'); const pending = s.button.events.click(); s.state.disposed = true; s.control.dispose();
finish({ deleted: 3 }); await pending;
assert.equal(s.notices.length, 0); assert.equal(s.state.settled, 1); assert.equal(s.button.disabled, true);
});
test('all ports require ALL confirmation and link events require LINKS', async () => {
const all = setup(); all.state.answer = 'CH01'; await all.button.events.click(); assert.equal(all.calls.length,0);
all.state.answer = 'ALL'; await all.button.events.click();
assert.deepEqual(all.calls, [{channel:'ALL',confirmation:'ALL'}]);
const link = setup({kind:'link',channelField:null});
assert.equal(link.button.disabled,false);
link.state.answer='SYSTEM'; await link.button.events.click(); assert.equal(link.calls.length,0);
link.state.answer='LINKS'; await link.button.events.click();
assert.deepEqual(link.calls,[{confirmation:'LINKS'}]);
assert.match(link.prompts[0],/현재 링크 상태/);
});