/* * Unit tests for playlist_check_translate_song(). */ #include "MakeTag.hxx" #include "playlist/PlaylistSong.hxx" #include "song/DetachedSong.hxx" #include "SongLoader.hxx" #include "client/IClient.hxx" #include "tag/Builder.hxx" #include "tag/Names.hxx" #include "tag/Tag.hxx" #include "util/Domain.hxx" #include "fs/AllocatedPath.hxx" #include "ls.hxx" #include "db/DatabaseSong.hxx" #include "storage/Registry.hxx" #include "storage/StorageInterface.hxx" #include "storage/plugins/LocalStorage.hxx" #include "Mapper.hxx" #include "time/ChronoUtil.hxx" #include #include #include bool uri_supported_scheme(const char *uri) noexcept { return strncmp(uri, "http://", 7) == 0; } const StoragePlugin * GetStoragePluginByUri(const char *) noexcept { // dummy symbol return nullptr; } static constexpr auto music_directory = PATH_LITERAL("/music"); static Storage *storage; static Tag MakeTag1a() { return MakeTag(TAG_ARTIST, "artist_a1", TAG_TITLE, "title_a1", TAG_ALBUM, "album_a1"); } static Tag MakeTag1b() { return MakeTag(TAG_ARTIST, "artist_b1", TAG_TITLE, "title_b1", TAG_COMMENT, "comment_b1"); } static Tag MakeTag1c() { return MakeTag(TAG_ARTIST, "artist_b1", TAG_TITLE, "title_b1", TAG_COMMENT, "comment_b1", TAG_ALBUM, "album_a1"); } static Tag MakeTag2a() { return MakeTag(TAG_ARTIST, "artist_a2", TAG_TITLE, "title_a2", TAG_ALBUM, "album_a2"); } static Tag MakeTag2b() { return MakeTag(TAG_ARTIST, "artist_b2", TAG_TITLE, "title_b2", TAG_COMMENT, "comment_b2"); } static Tag MakeTag2c() { return MakeTag(TAG_ARTIST, "artist_b2", TAG_TITLE, "title_b2", TAG_COMMENT, "comment_b2", TAG_ALBUM, "album_a2"); } static const char *uri1 = "/foo/bar.ogg"; static const char *uri2 = "foo/bar.ogg"; DetachedSong DatabaseDetachSong([[maybe_unused]] const Database &db, [[maybe_unused]] const Storage *_storage, const char *uri) { if (strcmp(uri, uri2) == 0) return DetachedSong(uri, MakeTag2a()); throw std::runtime_error("No such song"); } bool DetachedSong::LoadFile(Path path) { if (path.ToUTF8() == uri1) { SetTag(MakeTag1a()); return true; } return false; } class TestClient final : public IClient { public: // virtual methods from class IClient void AllowFile([[maybe_unused]] Path path_fs) const override { /* always fail, so a SongLoader with a non-nullptr Client pointer will be regarded "insecure", while one with client==nullptr will allow all files */ throw std::runtime_error{"foo"}; } #ifdef ENABLE_DATABASE const Database *GetDatabase() const noexcept override { return reinterpret_cast(this); } Storage *GetStorage() const noexcept override { return ::storage; } #endif // ENABLE_DATABASE }; static std::string ToString(const Tag &tag) { std::string result; if (!tag.duration.IsNegative()) result.append(std::to_string(tag.duration.ToMS())); for (const auto &item : tag) { result.push_back('|'); result.append(tag_item_names[item.type]); result.push_back('='); result.append(item.value); } return result; } static std::string ToString(const DetachedSong &song) { std::string result = song.GetURI(); result.push_back('|'); if (!IsNegative(song.GetLastModified())) result.append(std::to_string(std::chrono::system_clock::to_time_t(song.GetLastModified()))); result.push_back('|'); if (song.GetStartTime().IsPositive()) result.append(std::to_string(song.GetStartTime().ToMS())); result.push_back('-'); if (song.GetEndTime().IsPositive()) result.append(std::to_string(song.GetEndTime().ToMS())); result.push_back('|'); result.append(ToString(song.GetTag())); return result; } class TranslateSongTest : public ::testing::Test { std::unique_ptr _storage; protected: void SetUp() override { _storage = CreateLocalStorage(Path::FromFS(music_directory)); storage = _storage.get(); } void TearDown() override { _storage.reset(); } }; TEST_F(TranslateSongTest, AbsoluteURI) { DetachedSong song1("http://example.com/foo.ogg"); auto se = ToString(song1); const SongLoader loader(nullptr, nullptr); EXPECT_TRUE(playlist_check_translate_song(song1, "/ignored", loader)); EXPECT_EQ(se, ToString(song1)); } TEST_F(TranslateSongTest, Insecure) { /* illegal because secure=false */ DetachedSong song1 (uri1); TestClient client; const SongLoader loader{client}; EXPECT_FALSE(playlist_check_translate_song(song1, {}, loader)); } TEST_F(TranslateSongTest, Secure) { DetachedSong song1(uri1, MakeTag1b()); auto se = ToString(DetachedSong(uri1, MakeTag1c())); const SongLoader loader(nullptr, nullptr); EXPECT_TRUE(playlist_check_translate_song(song1, "/ignored", loader)); EXPECT_EQ(se, ToString(song1)); } TEST_F(TranslateSongTest, InDatabase) { const SongLoader loader(reinterpret_cast(1), storage); DetachedSong song1("doesntexist"); EXPECT_FALSE(playlist_check_translate_song(song1, {}, loader)); DetachedSong song2(uri2, MakeTag2b()); auto se = ToString(DetachedSong(uri2, MakeTag2c())); EXPECT_TRUE(playlist_check_translate_song(song2, {}, loader)); EXPECT_EQ(se, ToString(song2)); DetachedSong song3("/music/foo/bar.ogg", MakeTag2b()); se = ToString(DetachedSong(uri2, MakeTag2c())); EXPECT_TRUE(playlist_check_translate_song(song3, {}, loader)); EXPECT_EQ(se, ToString(song3)); } TEST_F(TranslateSongTest, Relative) { const Database &db = *reinterpret_cast(1); const SongLoader secure_loader(&db, storage); TestClient client; const SongLoader insecure_loader{client, &db, storage}; /* map to music_directory */ DetachedSong song1("bar.ogg", MakeTag2b()); auto se = ToString(DetachedSong(uri2, MakeTag2c())); EXPECT_TRUE(playlist_check_translate_song(song1, "/music/foo", insecure_loader)); EXPECT_EQ(se, ToString(song1)); /* illegal because secure=false */ DetachedSong song2("bar.ogg", MakeTag2b()); EXPECT_FALSE(playlist_check_translate_song(song1, "/foo", insecure_loader)); /* legal because secure=true */ DetachedSong song3("bar.ogg", MakeTag1b()); se = ToString(DetachedSong(uri1, MakeTag1c())); EXPECT_TRUE(playlist_check_translate_song(song3, "/foo", secure_loader)); EXPECT_EQ(se, ToString(song3)); /* relative to http:// */ DetachedSong song4("bar.ogg", MakeTag2a()); se = ToString(DetachedSong("http://example.com/foo/bar.ogg", MakeTag2a())); EXPECT_TRUE(playlist_check_translate_song(song4, "http://example.com/foo", insecure_loader)); EXPECT_EQ(se, ToString(song4)); } TEST_F(TranslateSongTest, Backslash) { const SongLoader loader(reinterpret_cast(1), storage); DetachedSong song1("foo\\bar.ogg", MakeTag2b()); #ifdef _WIN32 /* on Windows, all backslashes are converted to slashes in relative paths from playlists */ auto se = ToString(DetachedSong(uri2, MakeTag2c())); EXPECT_TRUE(playlist_check_translate_song(song1, {}, loader)); EXPECT_EQ(se, ToString(song1)); #else /* backslash only supported on Windows */ EXPECT_FALSE(playlist_check_translate_song(song1, {}, loader)); #endif }