From 5620a6f57fb0721d48fed5e010d17b921dfc1366 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 21 Jul 2020 23:42:40 -0700 Subject: [PATCH] Expose the entire ServiceItem in the API - Move JSON rendering into ServiceItem object - Provide entire service item object through API - Fix some potential bugs and write tests --- openlp/core/api/versions/v2/controller.py | 47 +--- openlp/core/lib/serviceitem.py | 60 +++- openlp/core/projectors/db.py | 6 +- .../openlp_core/lib/test_serviceitem.py | 265 +++++++++++++++++- tests/functional/openlp_core/test_state.py | 79 ++++++ .../projectors/test_projector_db.py | 127 ++++++++- 6 files changed, 533 insertions(+), 51 deletions(-) diff --git a/openlp/core/api/versions/v2/controller.py b/openlp/core/api/versions/v2/controller.py index 3d4ff845f..58efac6c6 100644 --- a/openlp/core/api/versions/v2/controller.py +++ b/openlp/core/api/versions/v2/controller.py @@ -19,16 +19,10 @@ # along with this program. If not, see . # ########################################################################## import logging -import os -import urllib.request -from pathlib import Path from openlp.core.api.lib import login_required from openlp.core.common import ThemeLevel from openlp.core.common.registry import Registry -from openlp.core.common.applocation import AppLocation -from openlp.core.lib import create_thumb -from openlp.core.lib.serviceitem import ItemCapabilities from flask import jsonify, request, abort, Blueprint @@ -41,44 +35,11 @@ def controller_text_api(): log.debug('controller-v2-live-item') live_controller = Registry().get('live_controller') current_item = live_controller.service_item - data = [] + live_item = {} if current_item: - for index, frame in enumerate(current_item.get_frames()): - item = {} - item['tag'] = index + 1 - item['selected'] = live_controller.selected_row == index - item['title'] = current_item.title - if current_item.is_text(): - if frame['verse']: - item['tag'] = str(frame['verse']) - item['text'] = frame['text'] - item['html'] = current_item.rendered_slides[index]['text'] - item['chords'] = current_item.rendered_slides[index]['chords'] - elif current_item.is_image() and not frame.get('image', '') and \ - Registry().get('settings_thread').value('api/thumbnails'): - thumbnail_path = os.path.join('images', 'thumbnails', frame['title']) - full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path - if not full_thumbnail_path.exists(): - create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False) - item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path)) - item['text'] = str(frame['title']) - item['html'] = str(frame['title']) - else: - # presentations and other things - if current_item.is_capable(ItemCapabilities.HasDisplayTitle): - item['title'] = str(frame['display_title']) - if current_item.is_capable(ItemCapabilities.HasNotes): - item['slide_notes'] = str(frame['notes']) - if current_item.is_capable(ItemCapabilities.HasThumbnails) and \ - Registry().get('settings_thread').value('api/thumbnails'): - # If the file is under our app directory tree send the portion after the match - data_path = str(AppLocation.get_data_path()) - if frame['image'][0:len(data_path)] == data_path: - item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):]) - item['text'] = str(frame['title']) - item['html'] = str(frame['title']) - data.append(item) - return jsonify(data) + live_item = current_item.to_dict() + live_item['slides'][live_controller.selected_row]['selected'] = True + return jsonify(live_item) @controller_views.route('/show', methods=['POST']) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 88c1d9309..e7018432c 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -25,6 +25,7 @@ import datetime import logging import ntpath import os +import urllib.request import uuid from copy import deepcopy from pathlib import Path @@ -32,7 +33,6 @@ from shutil import copytree, copy, move from PyQt5 import QtGui -from openlp.core.state import State from openlp.core.common import ThemeLevel, sha256_file_hash from openlp.core.common.applocation import AppLocation from openlp.core.common.enum import ServiceItemType @@ -41,7 +41,9 @@ from openlp.core.common.mixins import RegistryProperties from openlp.core.common.registry import Registry from openlp.core.display.render import remove_tags, render_tags, render_chords_for_printing from openlp.core.lib import ItemCapabilities +from openlp.core.lib import create_thumb from openlp.core.lib.theme import BackgroundType +from openlp.core.state import State from openlp.core.ui.icons import UiIcons from openlp.core.ui.media import parse_stream_path @@ -844,3 +846,59 @@ class ServiceItem(RegistryProperties): elif self.is_image() and self.slides and 'thumbnail' in self.slides[0]: return os.path.dirname(self.slides[0]['thumbnail']) return None + + def to_dict(self): + """ + Convert the service item into a dictionary + """ + data_dict = { + 'title': self.title, + 'name': self.name, + 'type': str(self.service_item_type), + 'theme': self.theme, + 'footer': self.raw_footer, + 'audit': self.audit, + 'notes': self.notes, + 'fromPlugin': self.from_plugin, + 'capabilities': self.capabilities, + 'backgroundAudio': [str(file_path) for file_path in self.background_audio], + 'isThemeOverwritten': self.theme_overwritten, + 'slides': [] + } + for index, frame in enumerate(self.get_frames()): + item = { + 'tag': index + 1, + 'title': self.title, + 'selected': False + } + if self.is_text(): + if frame['verse']: + item['tag'] = str(frame['verse']) + item['text'] = frame['text'] + item['html'] = self.rendered_slides[index]['text'] + item['chords'] = self.rendered_slides[index]['chords'] + elif self.is_image() and not frame.get('image', '') and \ + Registry().get('settings_thread').value('api/thumbnails'): + thumbnail_path = os.path.join('images', 'thumbnails', frame['title']) + full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path + if not full_thumbnail_path.exists(): + create_thumb(Path(self.get_frame_path(index)), full_thumbnail_path, False) + item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path)) + item['text'] = str(frame['title']) + item['html'] = str(frame['title']) + else: + # presentations and other things + if self.is_capable(ItemCapabilities.HasDisplayTitle): + item['title'] = str(frame['display_title']) + if self.is_capable(ItemCapabilities.HasNotes): + item['slide_notes'] = str(frame['notes']) + if self.is_capable(ItemCapabilities.HasThumbnails) and \ + Registry().get('settings_thread').value('api/thumbnails'): + # If the file is under our app directory tree send the portion after the match + data_path = str(AppLocation.get_data_path()) + if frame['image'][0:len(data_path)] == data_path: + item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):]) + item['text'] = str(frame['title']) + item['html'] = str(frame['title']) + data_dict['slides'].append(item) + return data_dict diff --git a/openlp/core/projectors/db.py b/openlp/core/projectors/db.py index 9887b5f49..08466211f 100644 --- a/openlp/core/projectors/db.py +++ b/openlp/core/projectors/db.py @@ -417,10 +417,10 @@ class ProjectorDB(Manager): """ source_dict = {} # Apparently, there was a change to the projector object. Test for which object has db id - if hasattr(projector, "id"): - chk = projector.id - elif hasattr(projector.entry, "id"): + if hasattr(projector, 'entry') and hasattr(projector.entry, 'id'): chk = projector.entry.id + else: + chk = projector.id # Get default list first for key in projector.source_available: diff --git a/tests/functional/openlp_core/lib/test_serviceitem.py b/tests/functional/openlp_core/lib/test_serviceitem.py index faa7ef369..e0a507e61 100644 --- a/tests/functional/openlp_core/lib/test_serviceitem.py +++ b/tests/functional/openlp_core/lib/test_serviceitem.py @@ -76,10 +76,9 @@ def service_item_env(state): def test_service_item_basic(settings): """ - Test the Service Item - basic test + Test creating a new Service Item without a plugin """ # GIVEN: A new service item - # WHEN: A service item is created (without a plugin) service_item = ServiceItem(None) @@ -88,6 +87,22 @@ def test_service_item_basic(settings): assert service_item.missing_frames() is True, 'There should not be any frames in the service item' +def test_service_item_with_plugin(settings): + """ + Test creating a new Service Item with a plugin + """ + # GIVEN: A new service item + mocked_plugin = MagicMock() + mocked_plugin.name = 'songs' + # WHEN: A service item is created (with a plugin) + service_item = ServiceItem(mocked_plugin) + + # THEN: We should get back a valid service item + assert service_item.name == 'songs', 'The service item name should be the same as the plugin name' + assert service_item.is_valid is True, 'The new service item should be valid' + assert service_item.missing_frames() is True, 'There should not be any frames in the service item' + + def test_service_item_load_custom_from_service(state_media, settings, service_item_env): """ Test the Service Item - adding a custom slide from a saved service @@ -522,3 +537,249 @@ def test_service_item_get_theme_data_song_level_global_fallback(settings): # THEN: theme should be the global theme assert theme == mocked_theme_manager.global_theme + + +def test_remove_capability(settings): + # GIVEN: A service item with a capability + service_item = ServiceItem(None) + service_item.add_capability(ItemCapabilities.CanEdit) + assert ItemCapabilities.CanEdit in service_item.capabilities + + # WHEN: A capability is removed + service_item.remove_capability(ItemCapabilities.CanEdit) + + # THEN: The capability should no longer be there + assert ItemCapabilities.CanEdit not in service_item.capabilities, 'The capability should not be in the list' + + +def test_to_dict_text_item(state_media, settings, service_item_env): + """ + Test that the to_dict() method returns the correct data for the service item + """ + # GIVEN: A ServiceItem with a service loaded from file + mocked_plugin = MagicMock() + mocked_plugin.name = 'songs' + service_item = ServiceItem(mocked_plugin) + service_item.add_icon = MagicMock() + FormattingTags.load_tags() + line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj') + service_item.set_from_service(line, Path('/test/')) + + # WHEN: to_dict() is called + result = service_item.to_dict() + + # THEN: The correct dictionary should be returned + expected_dict = { + + 'audit': ['Amazing Grace', ['John Newton'], '', ''], + 'backgroundAudio': ['/test/amazing_grace.mp3'], + 'capabilities': [2, 1, 5, 8, 9, 13, 15], + 'footer': ['Amazing Grace', 'Written by: John Newton'], + 'fromPlugin': False, + 'isThemeOverwritten': False, + 'name': 'songs', + 'notes': '', + 'slides': [ + { + 'chords': 'Amazing Grace! how sweet the sound\n' + 'That saved a wretch like me;\n' + 'I once was lost, but now am found,\n' + 'Was blind, but now I see.', + 'html': 'Amazing Grace! how sweet the sound\n' + 'That saved a wretch like me;\n' + 'I once was lost, but now am found,\n' + 'Was blind, but now I see.', + 'selected': False, + 'tag': 'V1', + 'text': 'Amazing Grace! how sweet the sound\n' + 'That saved a wretch like me;\n' + 'I once was lost, but now am found,\n' + 'Was blind, but now I see.', + 'title': 'Amazing Grace' + }, + { + 'chords': '’Twas grace that taught my heart to fear,\n' + 'And grace my fears relieved;\n' + 'How precious did that grace appear,\n' + 'The hour I first believed!', + 'html': '’Twas grace that taught my heart to fear,\n' + 'And grace my fears relieved;\n' + 'How precious did that grace appear,\n' + 'The hour I first believed!', + 'selected': False, + 'tag': 'V2', + 'text': '’Twas grace that taught my heart to fear,\n' + 'And grace my fears relieved;\n' + 'How precious did that grace appear,\n' + 'The hour I first believed!', + 'title': 'Amazing Grace' + }, + { + 'chords': 'Through many dangers, toils and snares\n' + 'I have already come;\n' + '’Tis grace that brought me safe thus far,\n' + 'And grace will lead me home.', + 'html': 'Through many dangers, toils and snares\n' + 'I have already come;\n' + '’Tis grace that brought me safe thus far,\n' + 'And grace will lead me home.', + 'selected': False, + 'tag': 'V3', + 'text': 'Through many dangers, toils and snares\n' + 'I have already come;\n' + '’Tis grace that brought me safe thus far,\n' + 'And grace will lead me home.', + 'title': 'Amazing Grace' + }, + { + 'chords': 'The Lord has promised good to me,\n' + 'His word my hope secures;\n' + 'He will my shield and portion be\n' + 'As long as life endures.', + 'html': 'The Lord has promised good to me,\n' + 'His word my hope secures;\n' + 'He will my shield and portion be\n' + 'As long as life endures.', + 'selected': False, + 'tag': 'V4', + 'text': 'The Lord has promised good to me,\n' + 'His word my hope secures;\n' + 'He will my shield and portion be\n' + 'As long as life endures.', + 'title': 'Amazing Grace' + }, + { + 'chords': 'Yes, when this heart and flesh shall fail,\n' + 'And mortal life shall cease,\n' + 'I shall possess within the veil\n' + 'A life of joy and peace.', + 'html': 'Yes, when this heart and flesh shall fail,\n' + 'And mortal life shall cease,\n' + 'I shall possess within the veil\n' + 'A life of joy and peace.', + 'selected': False, + 'tag': 'V5', + 'text': 'Yes, when this heart and flesh shall fail,\n' + 'And mortal life shall cease,\n' + 'I shall possess within the veil\n' + 'A life of joy and peace.', + 'title': 'Amazing Grace' + }, + { + 'chords': 'When we’ve been there a thousand years,\n' + 'Bright shining as the sun,\n' + 'We’ve no less days to sing God’s praise\n' + 'Than when we first begun.', + 'html': 'When we’ve been there a thousand years,\n' + 'Bright shining as the sun,\n' + 'We’ve no less days to sing God’s praise\n' + 'Than when we first begun.', + 'selected': False, + 'tag': 'V6', + 'text': 'When we’ve been there a thousand years,\n' + 'Bright shining as the sun,\n' + 'We’ve no less days to sing God’s praise\n' + 'Than when we first begun.', + 'title': 'Amazing Grace' + } + ], + 'theme': None, + 'title': 'Amazing Grace', + 'type': 'ServiceItemType.Text' + } + assert result == expected_dict + + +def test_to_dict_image_item(state_media, settings, service_item_env): + """ + Test that the to_dict() method returns the correct data for the service item + """ + # GIVEN: A ServiceItem with a service loaded from file + mocked_plugin = MagicMock() + mocked_plugin.name = 'image' + service_item = ServiceItem(mocked_plugin) + service_item.add_icon = MagicMock() + FormattingTags.load_tags() + line = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj') + + with patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash: + mocked_sha256_file_hash.return_value = '3a7ccbdb0b5a3db169c4692d7aad0ec8' + service_item.set_from_service(line) + + # WHEN: to_dict() is called + result = service_item.to_dict() + + # THEN: The correct dictionary should be returned + expected_dict = { + 'audit': '', + 'backgroundAudio': [], + 'capabilities': [3, 1, 5, 6], + 'footer': [], + 'fromPlugin': False, + 'isThemeOverwritten': False, + 'name': 'images', + 'notes': '', + 'slides': [ + { + 'html': 'image_1.jpg', + 'img': '/images/thumbnails/image_1.jpg', + 'selected': False, + 'tag': 1, + 'text': 'image_1.jpg', + 'title': 'Images' + } + ], + 'theme': -1, + 'title': 'Images', + 'type': 'ServiceItemType.Image' + } + assert result == expected_dict + + +def test_to_dict_presentation_item(state_media, settings, service_item_env): + """ + Test that the to_dict() method returns the correct data for the service item + """ + # GIVEN: A ServiceItem with a service loaded from file + mocked_plugin = MagicMock() + mocked_plugin.name = 'presentations' + service_item = ServiceItem(mocked_plugin) + presentation_name = 'test.pptx' + image = Path('thumbnails/abcd/slide1.png') + display_title = 'DisplayTitle' + notes = 'Note1\nNote2\n' + + # WHEN: adding presentation to service_item + with patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash,\ + patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as mocked_get_section_data_path: + mocked_sha256_file_hash.return_value = '4a067fed6834ea2bc4b8819f11636365' + mocked_get_section_data_path.return_value = Path('.') + service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes) + + # WHEN: to_dict() is called + result = service_item.to_dict() + + # THEN: The correct dictionary should be returned + expected_dict = { + 'audit': '', + 'backgroundAudio': [], + 'capabilities': [], + 'footer': [], + 'fromPlugin': False, + 'isThemeOverwritten': False, + 'name': 'presentations', + 'notes': '', + 'slides': [ + { + 'html': 'test.pptx', + 'selected': False, + 'tag': 1, + 'text': 'test.pptx', + 'title': '' + } + ], + 'theme': None, + 'title': '', + 'type': 'ServiceItemType.Command' + } + assert result == expected_dict diff --git a/tests/functional/openlp_core/test_state.py b/tests/functional/openlp_core/test_state.py index 6518cef8a..49e07558d 100644 --- a/tests/functional/openlp_core/test_state.py +++ b/tests/functional/openlp_core/test_state.py @@ -29,6 +29,26 @@ Test the States class. """ +def test_load_settings(state): + # GIVEN: Some modules + State().modules = {'mock': MagicMock} + + # WHEN: load_settings() is run + State().load_settings() + + # THEN: the modules should be empty + assert State().modules == {}, 'There should be no modules in the State object' + + +def test_save_settings(state): + # GIVEN: Niks + # WHEN: save_settings() is run + State().save_settings() + + # THEN: Nothing should happen + assert True, 'There should be no exceptions' + + def test_add_service(state): # GIVEN a new state # WHEN I add a new service @@ -128,3 +148,62 @@ def test_basic_preconditions_pass(state, registry): assert State().modules['test'].pass_preconditions is True assert State().modules['test2'].pass_preconditions is False assert State().modules['test1'].pass_preconditions is True + + +def test_missing_text(state): + """ + Test that settings the missing text in a module works + """ + # GIVEN: A state with a module + State().modules['test'] = MagicMock() + + # WHEN: missing_text() is called + State().missing_text('test', 'Test test') + + # THEN: The text is set + assert State().modules['test'].text == 'Test test', 'The text on the module should have been set' + + +def test_get_text(state): + """ + Test that the get_text() method returns the text of all the states + """ + # GIVEN: Some states with text + State().modules.update({'test1': MagicMock(text='Test 1'), 'test2': MagicMock(text='Test 2')}) + + # WHEN: get_text() is called + result = State().get_text() + + # THEN: The correct text is returned + assert result == 'Test 1\nTest 2\n', 'The full text is returned' + + +def test_check_preconditions_no_required(state): + """ + Test that the check_preconditions() method returns the correct attribute when there are no requirements + """ + # GIVEN: A State with no requires + State().modules.update({'test_pre1': MagicMock(requires=None, pass_preconditions=True)}) + + # WHEN: check_preconditions() is called + result = State().check_preconditions('test_pre1') + + # THEN: The correct result should be returned + assert result is True + + +def test_check_preconditions_required_module(state): + """ + Test that the check_preconditions() method returns the correct attribute when there is another required module + """ + # GIVEN: A State with two modules + State().modules.update({ + 'test_pre2': MagicMock(requires='test_pre3', pass_preconditions=True), + 'test_pre3': MagicMock(requires=None, pass_preconditions=False) + }) + + # WHEN: check_preconditions() is called + result = State().check_preconditions('test_pre2') + + # THEN: The correct result should be returned + assert result is False diff --git a/tests/openlp_core/projectors/test_projector_db.py b/tests/openlp_core/projectors/test_projector_db.py index e06b968b0..ad415a2ba 100644 --- a/tests/openlp_core/projectors/test_projector_db.py +++ b/tests/openlp_core/projectors/test_projector_db.py @@ -27,7 +27,7 @@ PREREQUISITE: add_record() and get_all() functions validated. import pytest import os import shutil -from unittest.mock import patch +from unittest.mock import MagicMock, patch from openlp.core.lib.db import upgrade_db from openlp.core.projectors import upgrade @@ -321,6 +321,21 @@ def test_get_projector_by_id_none(projector): def test_get_projector_all_none(projector): + """ + Test get_projector_all() when self.get_all_objects() returns None + """ + # GIVEN: Mocked out get_all_objects + with patch.object(projector, 'get_all_objects') as mocked_get_all_objects: + mocked_get_all_objects.return_value = None + + # WHEN: We retrieve the database entries + results = projector.get_projector_all() + + # THEN: Verify results is empty + assert [] == results, 'Returned results should have returned an empty list' + + +def test_get_projector_all_empty(projector): """ Test get_projector_all() with no projectors in db """ @@ -329,7 +344,7 @@ def test_get_projector_all_none(projector): # WHEN: We retrieve the database entries results = projector.get_projector_all() - # THEN: Verify results is None + # THEN: Verify results is empty assert [] == results, 'Returned results should have returned an empty list' @@ -436,3 +451,111 @@ def test_delete_projector_fail(projector): # THEN: Results should be False assert results is False, 'delete_projector() should have returned False' + + +def test_get_source_list_no_sources(projector): + """ + Test that an empty source list is returned + """ + # GIVEN: A mocked projector + mocked_projector = MagicMock(id='1', source_available=[]) + + # WHEN: get_source_list is run + results = projector.get_source_list(mocked_projector) + + # THEN: The list should be empty + assert results == {}, 'The list of sources returned should be empty' + + +def test_get_source_list_source_is_none(projector): + """ + Test that a default code is returned when a source is not in the database + """ + # GIVEN: A mocked projector + mocked_projector = MagicMock(id='1', source_available=['11']) + + with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered: + mocked_get_object_filtered.return_value = None + # WHEN: get_source_list is run + results = projector.get_source_list(mocked_projector) + + # THEN: The list should contain the one default item + assert results == {'11': 'RGB 1'}, 'The list of sources returned should contain "RGB1"' + + +def test_get_source_list_source_has_item(projector): + """ + Test that a default code is returned when a source is in the database + """ + # GIVEN: A mocked projector + mocked_projector = MagicMock(entry=MagicMock(id='1'), source_available=['5Y']) + + with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered: + mocked_get_object_filtered.return_value = MagicMock(text='VGA 1') + # WHEN: get_source_list is run + results = projector.get_source_list(mocked_projector) + + # THEN: The list should contain the one default item + assert results == {'5Y': 'VGA 1'}, 'The list of sources returned should contain "RGB1"' + + +def test_get_source_by_id_none(projector): + """ + Test that no source in the db returns None + """ + # GIVEN: A Mocked get_object_filtered method + with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered: + mocked_get_object_filtered.return_value = None + + # WHEN: Get the source by ID + result = projector.get_source_by_id('source') + + # THEN: The result should be None + assert result is None, 'None should be returned by get_source_by_id' + + +def test_get_source_by_id(projector): + """ + Test that a source in the db returns that source + """ + # GIVEN: A Mocked get_object_filtered method + mocked_entry = MagicMock() + with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered: + mocked_get_object_filtered.return_value = mocked_entry + + # WHEN: Get the source by ID + result = projector.get_source_by_id('source') + + # THEN: The result should be the mocked entry + assert result is mocked_entry, 'The mocked entry should be returned by get_source_by_id' + + +def test_get_source_by_code_none(projector): + """ + Test that no source in the db returns None + """ + # GIVEN: A Mocked get_object_filtered method + with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered: + mocked_get_object_filtered.return_value = None + + # WHEN: Get the source by code + result = projector.get_source_by_code('11', 52) + + # THEN: The result should be None + assert result is None, 'None should be returned by get_source_by_code' + + +def test_get_source_by_code(projector): + """ + Test that a source in the db returns that source + """ + # GIVEN: A Mocked get_object_filtered method + mocked_entry = MagicMock() + with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered: + mocked_get_object_filtered.return_value = mocked_entry + + # WHEN: Get the source by code + result = projector.get_source_by_code('5Y', 12) + + # THEN: The result should be the mocked entry + assert result is mocked_entry, 'The mocked entry should be returned by get_source_by_code'