import { mkdtempSync, writeFileSync, existsSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { ListingsService } from '../app/modules/listings/service.mjs'; import { InventoryService } from '../app/modules/inventory/service.mjs'; import { MediaService } from '../app/modules/media/service.mjs'; const failures=[]; const assert=(c,m)=>{if(!c)failures.push(m)}; const expectThrow=(fn,code,m)=>{try{fn();failures.push(m)}catch(e){if(code&&e.code!==code)failures.push(`${m} (${e.code||e.message})`)}}; let listings=[{id:1,title:'Aktiv',deleted_at:null,tags:[],photos:[]},{id:2,title:'Papierkorb',deleted_at:'2026-01-01',tags:[],photos:[]}]; const listRepo={ create(data){const row={id:3,...data,tags:data.tags||[],photos:data.photos||[]};listings.push(row);return row;}, get(id){return listings.find(x=>x.id===Number(id))||null;}, list(){return listings.filter(x=>!x.deleted_at);}, update(id,data){const row=this.get(id);Object.assign(row,data);return row;}, softDelete(id){this.get(id).deleted_at='now';}, restore(id){this.get(id).deleted_at=null;}, listDeleted(){return listings.filter(x=>x.deleted_at);}, permanentDelete(id){listings=listings.filter(x=>x.id!==Number(id));}, trashCount(){return this.listDeleted().length;}, duplicate(id){const row=this.get(id);return row?this.create({...row,id:undefined,title:`${row.title} (Kopie)`,deleted_at:null}):null;}, }; const ls=new ListingsService(listRepo); expectThrow(()=>ls.validateCreate({platform:'ebay'}),'LISTING_INPUT_INVALID','Titelpflicht wurde nicht erzwungen'); expectThrow(()=>ls.validateCreate({title:'OK',hacker:true}),'LISTING_INPUT_INVALID','Unbekanntes Listingfeld wurde akzeptiert'); const cleaned=ls.validateCreate({title:'Test',description_html:'

Text

',tags:['a','b']}); assert(!cleaned.description_html.includes('ls.permanentDelete(1),'LISTING_TRASH_NOT_FOUND','Aktiver Artikel konnte endgültig gelöscht werden'); assert(ls.permanentDelete(2).ok && !listRepo.get(2),'Papierkorbartikel wurde nicht endgültig gelöscht'); const inv=new InventoryService({locations(){return[]},unassigned(){return[]},byLocation(){return[]},bulkUpdateLocation(ids){return ids.length}}); expectThrow(()=>inv.validateBulk({ids:[1],location:'A',extra:true}),'INVENTORY_INPUT_INVALID','Unbekanntes Lagerfeld wurde akzeptiert'); expectThrow(()=>inv.validateBulk({ids:Array.from({length:501},(_,i)=>i+1),location:'A'}),'INVENTORY_BATCH_TOO_LARGE','Bulk-Limit wurde nicht erzwungen'); assert(inv.bulk({ids:[1,1,2],location:'Regal A'}).updated===2,'IDs wurden nicht dedupliziert'); const dir=mkdtempSync(join(tmpdir(),'vendoo-media-')); try{ writeFileSync(join(dir,'free.jpg'),'x'); writeFileSync(join(dir,'used.jpg'),'x'); const removed=[]; const mediaRepo={list(){return[]},setFavorite(p,f){return{ok:true,image_path:p,favorite:f}},referenced(){return new Set(['used.jpg'])},removeRecords(paths){removed.push(...paths)}}; const media=new MediaService({uploadRoot:dir,sharp:null,repository:mediaRepo}); expectThrow(()=>media.validateDelete({image_paths:['../secret']}),'MEDIA_PATH_INVALID','Traversal-Pfad wurde akzeptiert'); expectThrow(()=>media.validateDelete({image_paths:['free.jpg'],extra:true}),null,'Unbekanntes Medienfeld wurde akzeptiert'); const result=media.delete({image_paths:['free.jpg','used.jpg']}); assert(result.deleted===1 && result.protected.includes('used.jpg'),'Referenzschutz liefert falsches Ergebnis'); assert(!existsSync(join(dir,'free.jpg')) && existsSync(join(dir,'used.jpg')),'Referenzschutz löschte falsche Datei'); assert(removed.length===1 && removed[0]==='free.jpg','Medienrecords wurden falsch bereinigt'); } finally { rmSync(dir,{recursive:true,force:true}); } if(failures.length){console.error(`Catalog-Service-Regression 1.41.1 fehlgeschlagen (${failures.length}):`);failures.forEach(x=>console.error(`- ${x}`));process.exit(1)} console.log('Catalog-Service-Regression 1.41.1 erfolgreich: Listing-Allowlist, HTML-Sanitizing, Papierkorbgrenze, Inventory-Bulk-Limit, Pfadschutz und Medienreferenzen geprüft.');