#region license // This file is part of Vocaluxe. // // Vocaluxe is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Vocaluxe is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Vocaluxe. If not, see . #endregion extern alias songserver; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; using Vocaluxe.Base.SongServer; using VocaluxeSongServer.Contracts; using CLibraryScanner = songserver::VocaluxeSongServer.Library.CLibraryScanner; using ServerProgram = songserver::Program; namespace Tests.SongServer { [TestFixture] public class CSongServerTest { private string _Root; private string _Library; private string _Cache; [SetUp] public void SetUp() { _Root = Path.Combine(Path.GetTempPath(), "vlx-songserver-test-" + Guid.NewGuid().ToString("N")); _Library = Path.Combine(_Root, "library"); _Cache = Path.Combine(_Root, "cache"); Directory.CreateDirectory(Path.Combine(_Library, "A - One")); Directory.CreateDirectory(Path.Combine(_Library, "Sub", "Künstler - Lied")); Directory.CreateDirectory(Path.Combine(_Library, "NoSong")); File.WriteAllText(Path.Combine(_Library, "A - One", "A - One.txt"), "#TITLE:One\n#ARTIST:A\n#MP3:One.mp3\n#COVER:cover.jpg\n#BPM:300\n: 0 4 5 La\nE\n"); File.WriteAllBytes(Path.Combine(_Library, "A - One", "One.mp3"), _RandomBytes(300000, 1)); File.WriteAllBytes(Path.Combine(_Library, "A - One", "cover.jpg"), _RandomBytes(2000, 2)); // Latin-1 encoded, like many old song files File.WriteAllBytes(Path.Combine(_Library, "Sub", "Künstler - Lied", "lied.txt"), Encoding.Latin1.GetBytes("#TITLE:Lied über\n#ARTIST:Künstler\n#AUDIO:a.ogg\n#BPM:200\n: 0 4 5 La\nE\n")); File.WriteAllBytes(Path.Combine(_Library, "Sub", "Künstler - Lied", "a.ogg"), _RandomBytes(1000, 3)); File.WriteAllText(Path.Combine(_Library, "NoSong", "readme.txt"), "not a song"); } [TearDown] public void TearDown() { try { Directory.Delete(_Root, true); } catch (IOException) { } } private static byte[] _RandomBytes(int count, int seed) { var bytes = new byte[count]; new Random(seed).NextBytes(bytes); return bytes; } [Test] public void TestParseHeader() { var header = CLibraryScanner.ParseHeader("#TITLE:Song\r\n#ARTIST:Artist\r\n#MP3:x.mp3\r\n: 0 1 1 a\r\n#TITLE:ignored\n"); Assert.That(header, Is.Not.Null); Assert.That(header.Title, Is.EqualTo("Song")); Assert.That(header.Artist, Is.EqualTo("Artist")); Assert.That(CLibraryScanner.ParseHeader("#TITLE:Song\n#ARTIST:Artist\n"), Is.Null, "no audio"); Assert.That(CLibraryScanner.ParseHeader("just text"), Is.Null); } [Test] public void TestDecodeText() { Assert.That(CLibraryScanner.DecodeText(Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes("über")).ToArray()), Is.EqualTo("über")); Assert.That(CLibraryScanner.DecodeText(Encoding.UTF8.GetBytes("über")), Is.EqualTo("über")); Assert.That(CLibraryScanner.DecodeText(Encoding.Latin1.GetBytes("über")), Is.EqualTo("über")); } [Test] public void TestScan() { var scanner = new CLibraryScanner(NullLogger.Instance); var snapshot = scanner.Scan(_Library); Assert.That(snapshot.Dto.Songs.Select(s => s.Path), Is.EqualTo(new[] { "A - One/A - One.txt", "Sub/Künstler - Lied/lied.txt" })); Assert.That(snapshot.Dto.Songs[1].Title, Is.EqualTo("Lied über")); Assert.That(snapshot.Dto.Files.Select(f => f.Path), Does.Not.Contain("NoSong/readme.txt"), "only files in song folders are served"); Assert.That(snapshot.Dto.Files.Count, Is.EqualTo(5)); var version = snapshot.Dto.Version; Assert.That(scanner.Scan(_Library).Dto.Version, Is.EqualTo(version), "unchanged library, same version"); File.WriteAllBytes(Path.Combine(_Library, "A - One", "One.mp3"), _RandomBytes(300001, 1)); Assert.That(scanner.Scan(_Library).Dto.Version, Is.Not.EqualTo(version), "changed file, new version"); } private WebApplicationFactory _CreateServer(string apiKey = "") { return new WebApplicationFactory().WithWebHostBuilder(b => { b.UseSetting("Library:Path", _Library); b.UseSetting("Library:ApiKey", apiKey); }); } private static async Task _GetLibrary(HttpClient client) { for (var i = 0; i < 50; i++) { var response = await client.GetAsync("/api/v1/library"); if (response.StatusCode != HttpStatusCode.ServiceUnavailable) { response.EnsureSuccessStatusCode(); return JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync(), new JsonSerializerOptions(JsonSerializerDefaults.Web)); } await Task.Delay(100); // first scan still running } throw new TimeoutException("library scan did not finish"); } [Test] public async Task TestApiKey() { using (var server = _CreateServer("secret")) { var client = server.CreateClient(); Assert.That((await client.GetAsync("/api/v1/library")).StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); client.DefaultRequestHeaders.Add("X-Api-Key", "secret"); Assert.That((await _GetLibrary(client)).Songs.Count, Is.EqualTo(2)); } } [Test] public async Task TestDownloadOnDemand() { using (var server = _CreateServer()) { var client = server.CreateClient(); var library = await _GetLibrary(client); var provider = new CRemoteSongFileProvider(client, _Cache, library.Files); var mp3 = CRemoteSongFileProvider.GetLocalPath(_Cache, "A - One/One.mp3"); // Case differs from the server file, as often in song files var cover = CRemoteSongFileProvider.GetLocalPath(_Cache, "A - One/COVER.jpg"); var ogg = CRemoteSongFileProvider.GetLocalPath(_Cache, "Sub/Künstler - Lied/a.ogg"); Assert.That(provider.Exists(mp3), Is.True); Assert.That(provider.IsLocal(mp3), Is.False); Assert.That(provider.Exists(Path.Combine(_Cache, "nothing.mp3")), Is.False); Assert.That(provider.GetProgress(new[] { mp3 }), Is.EqualTo(0f)); // Interrupted earlier download: the rest is fetched with a Range request Directory.CreateDirectory(Path.GetDirectoryName(mp3)); File.WriteAllBytes(mp3 + ".part", _RandomBytes(300000, 1).Take(1000).ToArray()); await provider.RequestAsync(new[] { mp3, cover, ogg, mp3 }); Assert.That(provider.IsLocal(mp3) && provider.IsLocal(cover) && provider.IsLocal(ogg), Is.True); Assert.That(File.ReadAllBytes(mp3), Is.EqualTo(File.ReadAllBytes(Path.Combine(_Library, "A - One", "One.mp3")))); Assert.That(File.ReadAllBytes(ogg), Is.EqualTo(File.ReadAllBytes(Path.Combine(_Library, "Sub", "Künstler - Lied", "a.ogg")))); Assert.That(File.Exists(mp3 + ".part"), Is.False); Assert.That(provider.GetProgress(new[] { mp3, cover, ogg }), Is.EqualTo(1f)); // A changed file on the server makes the local copy outdated var changed = library.Files.Select(f => f.Path == "A - One/One.mp3" ? new FileDto { Path = f.Path, Size = f.Size, Modified = f.Modified + 60000 } : f); Assert.That(new CRemoteSongFileProvider(client, _Cache, changed).IsLocal(mp3), Is.False); } } [Test] public async Task TestBundleAndFileSecurity() { using (var server = _CreateServer()) { var client = server.CreateClient(); await _GetLibrary(client); var body = new StringContent(JsonSerializer.Serialize(new BundleRequest { Paths = new List { "Sub/Künstler - Lied/lied.txt", "NoSong/readme.txt" } }, new JsonSerializerOptions(JsonSerializerDefaults.Web)), Encoding.UTF8, "application/json"); var response = await client.PostAsync("/api/v1/files/bundle", body); response.EnsureSuccessStatusCode(); using (var zip = new System.IO.Compression.ZipArchive(await response.Content.ReadAsStreamAsync())) { Assert.That(zip.Entries.Select(e => e.FullName), Is.EqualTo(new[] { "Sub/Künstler - Lied/lied.txt" })); } Assert.That((await client.GetAsync("/api/v1/files/NoSong/readme.txt")).StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); Assert.That((await client.GetAsync("/api/v1/files/..%2F..%2Fsecret.txt")).StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); } } } }