const {chromium}=require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert=require('node:assert/strict');const fs=require('fs');
(async()=>{
const browser=await chromium.launch({headless:true,args:['--no-sandbox'],env:process.env});const page=await browser.newPage();const writes=[],errors=[],passed=[];
page.on('pageerror',e=>errors.push(e.message));
await page.route('**/api/firewall/**',async route=>{if(route.request().method()==='GET')return route.continue();writes.push({path:new URL(route.request().url()).pathname,body:route.request().postDataJSON()});await route.fulfill({json:{success:true,id:route.request().postDataJSON()?.id||'filter-preview'}})});
try{
await page.goto(process.env.DEBTIK_TEST_URL||'http://127.0.0.1:8080/',{waitUntil:'domcontentloaded'});await page.locator('#login-username').fill(process.env.DEBTIK_TEST_USER || 'admin');await page.locator('#login-password').fill(process.env.DEBTIK_TEST_PASSWORD || 'admin');await page.locator('#login-submit-btn').click();await page.waitForFunction(()=>state.currentUser!==null);
await page.evaluate(async()=>{navigate('firewall');await loadFirewall(false)});
for(const prefix of ['fw','nat','mangle'])for(const key of ['layer7','connbytes','connrate','pcc','tlshost','psd','ipv4options','dstlimit','ingresspriority','priority','hotspot'])assert.equal(await page.locator('#'+prefix+'-field-'+key).count(),0);
for(const [type,bad]of [['filter',['tarpit','fasttrack-connection']],['nat',['same','socksify','endpoint-independent-nat']],['mangle',['route','sniff-pc','sniff-tzsp','clear-df','strip-ipv4-options','fasttrack-connection']]])for(const action of bad)assert.equal(await page.locator('#'+type+'-modal-action option[value="'+action+'"]').count(),0);
passed.push('Unsupported fields and actions physically removed');
for(const [type,open,apply]of [['filter','openEditFilterModal','applyFilterRule'],['nat','openEditNATModal','applyNATRule'],['mangle','openEditMangleModal','applyMangleRule']]){
const chain=await page.evaluate(async({type,open,apply})=>{const key=type==='filter'?'filterRules':type==='nat'?'natRules':'mangleRules';state[key]=[{id:type+'-preview',chain:'CUSTOM_TEST',action:'accept',disabled:false,passthrough:true}];window[open](type+'-preview');const value=document.getElementById(type+'-modal-chain').value;await window[apply](true);return value},{type,open,apply});
assert.equal(chain,'CUSTOM_TEST');assert.equal(writes.at(-1).body.chain,'CUSTOM_TEST');
}passed.push('Custom chain preserved in all three forms');
for(const [type,prefix,open,apply] of [['filter','fw','openEditFilterModal','applyFilterRule'],['nat','nat','openEditNATModal','applyNATRule'],['mangle','mangle','openEditMangleModal','applyMangleRule']]){
const fields=await page.evaluate(async({type,prefix,open,apply})=>{const key=type==='filter'?'filterRules':type==='nat'?'natRules':'mangleRules';state[key]=[{id:type+'-all',chain:type==='nat'?'prerouting':'forward',action:'accept',protocol:'all',content:'all',packet_mark:'all',passthrough:true}];window[open](type+'-all');const v={content:document.getElementById(prefix+'-val-content').value,contentEnabled:!document.getElementById(prefix+'-field-content').classList.contains('hidden'),mark:document.getElementById(prefix+'-val-pktmark').value,markEnabled:!document.getElementById(prefix+'-field-pktmark').classList.contains('hidden'),protocolEnabled:!document.getElementById(prefix+'-field-proto').classList.contains('hidden')};await window[apply](true);return v},{type,prefix,open,apply});
assert.deepEqual(fields,{content:'all',contentEnabled:true,mark:'all',markEnabled:true,protocolEnabled:false});
assert.equal(writes.at(-1).body.content,'all');assert.equal(writes.at(-1).body.packet_mark,'all');assert.equal(writes.at(-1).body.protocol,'all');
}passed.push('Literal all survives edit and save for all three tables');
assert.equal(await page.locator('#mangle-modal-newttl').getAttribute('placeholder'),'64 (set), -1 (decrease), +1 (increase)');passed.push('TTL examples use supported syntax');
for(const [type,prefix,open,apply]of [['filter','fw','openAddFilterModal','applyFilterRule'],['nat','nat','openAddNATModal','applyNATRule']]){
await page.evaluate(async({type,prefix,open,apply})=>{window[open]();window[prefix==='fw'?'toggleFwField':'toggleNATField']('days',true);for(const d of ['sun','mon','tue','wed','thu','fri','sat'])document.getElementById(prefix+'-day-'+d).checked=d==='fri';await window[apply](true)},{type,prefix,open,apply});assert.equal(writes.at(-1).body.days,'fri');
}passed.push('Weekdays serialized for Filter and NAT');
await page.evaluate(()=>{state.filterRules=[{id:'filter-xss',chain:'input',action:'accept',comment:'
'}];renderFilterRules()});await page.waitForTimeout(250);assert.equal(await page.evaluate(()=>!!window.__fwAuditXss),false);assert.equal(await page.locator('#filter-table-body img').count(),0);passed.push('Stored firewall comment escaped');
await page.evaluate(()=>showToast('Error','
','error')); await page.waitForTimeout(50); assert.equal(await page.evaluate(()=>!!window.__toastXss),false); assert.equal(await page.locator('#toast-container img').count(),0); passed.push('Firewall error messages escaped in notifications');
await page.evaluate(()=>{state.addressLists=[{id:'dynamic-test',name:'
',address:'127.0.0.1',dynamic:true,timeout:30}];renderAddressLists()});assert.equal(await page.locator('#addrlist-table-body img').count(),0);assert.equal(await page.locator('#addrlist-table-body').getByText('Edit',{exact:true}).count(),0);passed.push('Dynamic entries escaped and read-only except Delete');
await page.evaluate(()=>{state.filterRules=[{id:'filter-reset',chain:'input',action:'accept'}];openEditFilterModal('filter-reset')});await page.evaluate(()=>resetCurrentRuleCounters());assert.equal(writes.at(-1).path,'/api/firewall/filter/filter-reset/reset-counters');passed.push('Single-rule reset calls backend');
await page.evaluate(()=>{state.filterRules=[{id:'filter-old',chain:'forward',action:'accept',tos:'min-delay'}];openEditFilterModal('filter-old');openAddFilterModal()});assert.equal(await page.locator('#fw-val-tos').inputValue(),'');passed.push('New rule does not inherit old TOS');
await page.evaluate(()=>openAddNATModal());
await page.locator('#nat-modal-action').selectOption('dst-nat');
assert.equal(await page.locator('#nat-modal-chain').inputValue(),'postrouting');
assert.equal(await page.locator('#nat-modal-action option:checked').innerText(),'DNAT');
passed.push('Selecting target leaves chain selection explicit');
await page.evaluate(()=>{openAddNATModal();document.getElementById('nat-modal-chain').value='output';});
await page.locator('#nat-modal-action').selectOption('dst-nat');
assert.equal(await page.locator('#nat-modal-chain').inputValue(),'output');
for(const chain of ['prerouting','input','output','postrouting']) assert.equal(await page.locator(`#nat-chain-options option[value="${chain}"]`).count(),1);
passed.push('NAT exposes packet paths and preserves output DNAT');
await page.evaluate(()=>{openAddNATModal();document.getElementById('nat-modal-chain').value='prerouting';handleNATChainChange()});
await page.locator('#nat-modal-action').selectOption('dst-nat');
assert.equal(await page.locator('#nat-modal-action').inputValue(),'dst-nat');
assert.equal(await page.locator('#nat-modal-toaddr').isVisible(),true);
await page.locator('#nat-modal-toaddr').fill('192.0.2.53');
await page.evaluate(()=>toggleNATField('dport',true)); await page.locator('#nat-val-dport').fill('53');
let beforeNAT=writes.length;await page.evaluate(()=>applyNATRule(true));assert.equal(writes.length,beforeNAT);
assert.equal(await page.locator('#nat-val-proto').inputValue(),'all');
await page.locator('#nat-val-proto').selectOption('udp');await page.evaluate(()=>applyNATRule(true));
assert.equal(writes.at(-1).body.protocol,'udp');assert.equal(writes.at(-1).body.dst_port,'53');assert.equal(writes.at(-1).body.to_addresses,'192.0.2.53');
assert.equal(writes.at(-1).body.chain,'prerouting');
passed.push('DNAT destination IP visible and DNS port requires explicit protocol');
await page.evaluate(()=>{state.natRules=[{id:'legacy',chain:'srcnat',action:'masquerade'},{id:'native',chain:'postrouting',action:'masquerade'}];document.getElementById('nat-filter-chain').value='postrouting';renderNATRules()});
assert.equal(await page.locator('#nat-table-body tr').count(),2);
assert.equal(await page.locator('#nat-table-body').getByText('POSTROUTING',{exact:true}).count(),2);
passed.push('Legacy NAT aliases render and filter as native chains');
await page.route('**/api/firewall/filter/filter-test/move',route=>route.fulfill({status:422,json:{message:'Move failed in kernel'}}));
await page.evaluate(()=>moveFilterRule('filter-test','up'));
assert.match(await page.locator('#toast-container').innerText(),/Move failed in kernel/);
await page.unroute('**/api/firewall/filter/filter-test/move');
passed.push('Rule operation errors are visible');
await page.evaluate(()=>handleLogoutClick());assert.deepEqual(errors,[]);console.log(JSON.stringify({passed,errors,mockedWrites:writes.length},null,2));
}finally{await browser.close()}
})().catch(e=>{console.error(e);process.exitCode=1});