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','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});