Files
furumi-web/tests/import_playlists.test.cjs
T
Aleksandr Bogomiakov 4141562e33
Build and Publish / Build and Publish Docker Image (push) Successful in 9m49s
Added stat page
2026-11-12 11:52:38 +00:00

76 lines
5.1 KiB
JavaScript

const assert = require('node:assert/strict');
const {readFileSync} = require('node:fs');
const {test} = require('node:test');
const vm = require('node:vm');
const {randomUUID} = require('node:crypto');
const template = readFileSync(`${__dirname}/../templates/player/scripts.html`, 'utf8');
function downloads(fetchImpl) {
let code = template.slice(template.indexOf(" Alpine.store('torrents', {"), template.indexOf(" Alpine.store('playlists', {"));
code = code.replace(/\{\{[^}]+\}\}/g,'true').replace(/sourceTab: .*?,\n/,"sourceTab: 'files',\n");
const stores={playlists:{list:[{id:10,title:'Mine',kind:'user',is_own:true},{id:20,title:'Other',kind:'user',is_own:false}],reload:async()=>{}}};
const requests=[];
class XHR {
headers={}; upload={}; status=200; responseText='{}';
open(method,url) {this.url=url;}
setRequestHeader(key,value) {this.headers[key]=value;}
send(file) {requests.push({file,headers:this.headers});this.onload();}
}
const context=vm.createContext({Alpine:{store:(name,value)=>value?(stores[name]=value):stores[name]},fetch:fetchImpl,
T:new Proxy({}, {get:(_,key)=>key}),crypto:{randomUUID},XMLHttpRequest:XHR,redirectOnUnauthorized:()=>{},console,setInterval:()=>0,clearInterval:()=>{}});
vm.runInContext(code,context);
const store=stores.torrents;
store._poll=()=>{};store.loadSessions=async()=>{};store.loadAgentStatus=async()=>{};store.loadLocalUploadHistory=async()=>{};
return {store,requests};
}
test('destinations are independent, owned playlists only, new titles required',()=>{
const {store:s}=downloads();
assert.deepEqual(Array.from(s.youtubeOwnedPlaylists(),p=>p.id),[10]);
s.localPlaylistChoice='__new__';assert.equal(s.importDestinationValid('local'),false);
assert.equal(s.importDestinationValid('torrent'),true);
s.localFiles=[{name:'song.flac',webkitRelativePath:'Soundtrack/song.flac'}];s.importPlaylistChoiceChanged('local');
assert.equal(s.localNewPlaylistTitle,'Soundtrack');assert.equal(s.importDestinationValid('local'),true);
});
test('failed torrent starts reuse the newly created playlist and send only selected files',async()=>{
const calls=[];
const {store:s}=downloads(async(url,opts)=>{
calls.push({url,data:JSON.parse(opts.body)});
return url==='/api/player/playlists'?{ok:true,json:async()=>({id:33,title:'Soundtrack',kind:'user',is_own:true})}:{ok:false,json:async()=>({error:'retry'})};
});
s.previewData={id:'torrent-1'};s.selected=new Set([1,3]);s.torrentPlaylistChoice='__new__';s.torrentNewPlaylistTitle='Soundtrack';
await s.start();await s.start();
assert.equal(calls.filter(c=>c.url==='/api/player/playlists').length,1);
assert.deepEqual(calls[1].data,{selected_files:[1,3],target_playlist_id:33});
assert.deepEqual(calls[2].data,calls[1].data);assert.equal(s.loading,false);
});
test('local files carry the same playlist destination and retries retain upload IDs',async()=>{
const {store:s,requests}=downloads();const f={name:'a.flac',size:10};
s.setLocalFiles([f]);s.localTargetPlaylistId=10;
await s.uploadLocalFile(f,0,10);await s.uploadLocalFile(f,0,10);
assert.equal(requests[0].headers['X-Furumusic-Target-Playlist-Id'],'10');
assert.equal(requests[0].headers['X-Furumusic-Upload-Id'],requests[1].headers['X-Furumusic-Upload-Id']);
s.clearLocalFiles();s.setLocalFiles([f]);await s.uploadLocalFile(f,0,10);
assert.equal(requests[2].headers['X-Furumusic-Target-Playlist-Id'],undefined);
});
test('partially failed batches keep their destination, create one playlist and retry remaining files',async()=>{
let creates=0;let attempts=0;
const {store:s}=downloads(async(url)=>{
if(url==='/api/player/playlists') {creates++;return {ok:true,json:async()=>({id:44})};}
return {ok:true};
});
s.setLocalFiles([{name:'a.mp3',size:1},{name:'b.mp3',size:1}]);s.localPlaylistChoice='__new__';s.localNewPlaylistTitle='OST';
s.uploadLocalFile=async()=>{attempts++;if(attempts===2)throw Error('offline');return {};};
await s.uploadLocalFiles();assert.equal(s.localFiles.length,1);assert.equal(s.localPlaylistLocked,true);
assert.equal(s.localTargetPlaylistId,44);await s.uploadLocalFiles();
assert.equal(creates,1);assert.equal(s.localFiles.length,0);assert.equal(s.localFilesUploading,false);
});
test('playlist creation failure leaves the batch editable and does not signal upload completion',async()=>{
const calls=[];const {store:s}=downloads(async(url)=>{calls.push(url);return {ok:false,json:async()=>({error:'failed'})};});
s.setLocalFiles([{name:'a.mp3',size:1}]);s.localPlaylistChoice='__new__';s.localNewPlaylistTitle='OST';
await s.uploadLocalFiles();assert.equal(s.localPlaylistLocked,false);assert.equal(s.localFilesUploading,false);
assert.deepEqual(calls,['/api/player/playlists']);assert.equal(s.localFiles.length,1);
});
test('torrent destination locks after start',()=>{
const {store:s}=downloads();s.currentJob={status:'preview'};assert.equal(s.torrentDestinationLocked(),false);
s.currentJob.status='paused';assert.equal(s.torrentDestinationLocked(),true);
});