diff --git a/codebeaver.yml b/codebeaver.yml new file mode 100644 index 0000000..68c1df6 --- /dev/null +++ b/codebeaver.yml @@ -0,0 +1,2 @@ +from:python-pytest-poetry +# This file was generated automatically by CodeBeaver based on your repository. Learn how to customize it here: https://docs.codebeaver.ai/configuration \ No newline at end of file diff --git a/tests/test_deezer.py b/tests/test_deezer.py new file mode 100644 index 0000000..b5aceba --- /dev/null +++ b/tests/test_deezer.py @@ -0,0 +1,285 @@ +import pytest +from deezer_downloader import deezer +import re +import json +import struct +import urllib.parse +import html.parser +import requests +import io +from binascii import a2b_hex, b2a_hex +from Crypto.Cipher import Blowfish +from Crypto.Hash import MD5 + +# No additional imports needed beyond what already exist in the test file. +# No additional imports needed beyond what already exist in the test file. +def test_calcbfkey_known_value(): + """ + Test that calcbfkey returns the expected Blowfish decryption key + for a known song id ('123456'). This increases coverage for the crypto helper function. + """ + # The MD5 hash of "123456" is "e10adc3949ba59abbe56e057f20f883e". + # Then, calcbfkey computes the key by XORing each character of the first 16 characters + # with the corresponding character in the last 16 characters and the static key b"g4el58wc0zvf9na1". + # The expected result from these operations is " +def test_get_song_infos_not_logged_in(monkeypatch): + """ + Test that get_song_infos_from_deezer_website raises Deezer403Exception + when the response from Deezer does not contain the expected "MD5_ORIGIN" + (simulating a situation where we are not logged in). + """ + # Define a dummy response that does not contain "MD5_ORIGIN" + class DummyResponse: + def __init__(self, status_code, text): + self.status_code = status_code + self.text = text + def raise_for_status(self): + # Assume response is OK so do nothing + pass + # Define a dummy session where get() returns our dummy response + class DummySession: + def get(self, url): + # Returning a page without "MD5_ORIGIN" + return DummyResponse(200, "
Not logged in") + # Monkeypatch the global session in the deezer module + monkeypatch.setattr(deezer, "session", DummySession()) + # This call should raise a Deezer403Exception because "MD5_ORIGIN" is missing. + with pytest.raises(deezer.Deezer403Exception) as excinfo: + deezer.get_song_infos_from_deezer_website(deezer.TYPE_TRACK, "dummy_id") + + # Optionally, check that the exception message mentions login issues. + assert "not logged in" in str(excinfo.value).lower() +def test_downloadpicture_returns_dummy_content(monkeypatch): + """ + Test that downloadpicture returns the expected dummy image content. + This test monkeypatches the session.get method to simulate a network response. + """ + dummy_content = b"dummyimage" + + class DummyResponse: + def __init__(self, content): + self.content = content + class DummySession: + def get(self, url): + # Verify that the URL is constructed correctly for a cover image. + assert "cover" in url + return DummyResponse(dummy_content) + # Monkeypatch the global session in the deezer module with our dummy session. + monkeypatch.setattr(deezer, "session", DummySession()) + + # Call downloadpicture and verify that it returns the dummy content. + result = deezer.downloadpicture("dummy_pic_id") + assert result == dummy_content +def test_writeid3v1_tag_creates_tag(): + """ + Test that writeid3v1_1 writes an ID3v1.1 tag that starts with the 'TAG' header + and that the tag length matches the expected size. + """ + # Create a dummy song dictionary with necessary keys + dummy_song = { + "SNG_TITLE": "Test Song", + "ART_NAME": "Test Artist", + "ALB_TITLE": "Test Album", + "TRACK_NUMBER": "1" + } + # Set the global album_Data variable with dummy values required by album_get + deezer.album_Data = { + "PHYSICAL_RELEASE_DATE": "2020", + "LABEL_NAME": "Test Label", + "TRACKS": "10" + } + # Use BytesIO to capture the output written by writeid3v1_1 + output_file = io.BytesIO() + deezer.writeid3v1_1(output_file, dummy_song) + result = output_file.getvalue() + # Check that the tag starts with the "TAG" header (first 3 bytes) + assert result.startswith(b"TAG") + # Calculate expected size using the same struct format used in writeid3v1_1: + # The format string is "3s30s30s30s4s28sBHB" (combining the pieces from the code) + expected_size = struct.calcsize("3s30s30s30s4s28sBHB") + # Check that the written tag length equals the expected size + assert len(result) == expected_size +def test_set_song_quality(monkeypatch, capsys): + """ + Test that set_song_quality correctly sets the global sound_format: + - When lossless is supported and quality_config is "flac", it should set sound_format to "FLAC". + - When lossless is not supported, it should fallback to "MP3_128" and print a warning. + """ + # Case 1: flac quality_config and premium lossless support => should set to FLAC. + deezer.set_song_quality("flac", {"lossless": True}) + assert deezer.sound_format == "FLAC", "Expected sound_format to be FLAC when lossless is supported." + # Case 2: flac quality_config but lossless not supported => should fallback to MP3_128 and print warning. + deezer.set_song_quality("flac", {"lossless": False}) + assert deezer.sound_format == "MP3_128", "Expected sound_format to fallback to MP3_128 when lossless is not supported." + captured = capsys.readouterr().out + assert "WARNING: flac quality is configured" in captured, "Expected a warning message when flac is not supported." +def test_parse_deezer_playlist_returns_playlist_data(monkeypatch): + """ + Test that parse_deezer_playlist correctly processes a dummy API response and returns the expected + playlist name and list of songs. + """ + class DummyResponse: + def __init__(self, json_data): + self._json = json_data + self.status_code = 200 + def json(self): + return self._json + class DummySession: + def post(self, url, json=None): + if "method=deezer.getUserData" in url: + # Simulate CSRF token retrieval + return DummyResponse({"results": {"checkForm": "dummycsrf"}}) + elif "method=deezer.pagePlaylist" in url: + # Simulate playlist request returning dummy data + return DummyResponse({ + "error": "", + "results": { + "DATA": {"TITLE": "My Playlist", "NB_SONG": 2}, + "SONGS": {"count": 2, "data": [ + {"SNG_ID": "1", "SNG_TITLE": "Song1"}, + {"SNG_ID": "2", "SNG_TITLE": "Song2"} + ]} + } + }) + else: + return DummyResponse({}) + + # Monkeypatch the global session with DummySession + monkeypatch.setattr(deezer, "session", DummySession()) + + # Call parse_deezer_playlist with a dummy playlist_id (digits will be extracted) + playlist_name, songs = deezer.parse_deezer_playlist("12345") + + # Assert that the response matches our dummy data + assert playlist_name == "My Playlist" + assert isinstance(songs, list) + assert len(songs) == 2 + assert songs[0]["SNG_ID"] == "1" + assert songs[0]["SNG_TITLE"] == "Song1" + assert songs[1]["SNG_ID"] == "2" + assert songs[1]["SNG_TITLE"] == "Song2" +def test_blowfish_decrypt_returns_original(): + """ + Test that blowfishDecrypt correctly decrypts ciphertext produced by Blowfish encryption. + This increases test coverage for the crypto helper function by ensuring that encryption/decryption + with the fixed IV and key returns the original data. + """ + # Use the same key as the decryption function expects + key = "g4el58wc0zvf9na1" + # The initialization vector as a hex string used in blowfishDecrypt: "0001020304050607" + iv = a2b_hex("0001020304050607") + # Prepare a plaintext block that is exactly 8 bytes (Blowfish block size) + plaintext = b"ABCDEFGH" + # Create a Blowfish cipher with the same key and IV, using CBC mode + cipher = Blowfish.new(key.encode(), Blowfish.MODE_CBC, iv) + # Encrypt the plaintext to obtain ciphertext + ciphertext = cipher.encrypt(plaintext) + # Now use the function under test to decrypt the ciphertext + decrypted = deezer.blowfishDecrypt(ciphertext, key) + # Verify that the decrypted text matches the original plaintext + assert decrypted == plaintext +def test_script_extractor_extracts_scripts(): + """ + Test that ScriptExtractor correctly extracts all script contents from HTML with multiple ' + '' + '' + '