diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b794011..bd1443340 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +### Added +- Added unit tests for controllers + [#790](https://github.com/nextcloud/cookbook/pull/790) @christianlupus + ### Fixed - Mark app as compatible with Nextcloud 22 [#778](https://github.com/nextcloud/cookbook/pull/778) @christianlupus diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index dc079abe4..28730d9bf 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -8,7 +8,6 @@ use OCP\AppFramework\Controller; use OCA\Cookbook\Service\RecipeService; -use OCP\IURLGenerator; use OCA\Cookbook\Service\DbCacheService; use OCA\Cookbook\Helper\RestParameterParser; @@ -17,10 +16,6 @@ class ConfigController extends Controller { * @var RecipeService */ private $service; - /** - * @var IURLGenerator - */ - private $urlGenerator; /** * @var DbCacheService @@ -32,11 +27,10 @@ class ConfigController extends Controller { */ private $restParser; - public function __construct($AppName, IRequest $request, IURLGenerator $urlGenerator, RecipeService $recipeService, DbCacheService $dbCacheService, RestParameterParser $restParser) { + public function __construct($AppName, IRequest $request, RecipeService $recipeService, DbCacheService $dbCacheService, RestParameterParser $restParser) { parent::__construct($AppName, $request); $this->service = $recipeService; - $this->urlGenerator = $urlGenerator; $this->dbCacheService = $dbCacheService; $this->restParser = $restParser; } diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index 8a39d168d..f536de0d7 100755 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -134,11 +134,6 @@ public function search($query) { } return new DataResponse($recipes, 200, ['Content-Type' => 'application/json']); - // TODO: Remove obsolete code below when this is ready - $response = new TemplateResponse($this->appName, 'content/search', ['query' => $query, 'recipes' => $recipes]); - $response->renderAs('blank'); - - return $response; } catch (\Exception $e) { return new DataResponse($e->getMessage(), 500); } @@ -237,7 +232,7 @@ public function tags($keywords) { return new DataResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); } catch (\Exception $e) { - error_log($e->getMessage()); + // error_log($e->getMessage()); return new DataResponse($e->getMessage(), 500); } } diff --git a/lib/Controller/RecipeController.php b/lib/Controller/RecipeController.php index d16cc2239..774af396c 100755 --- a/lib/Controller/RecipeController.php +++ b/lib/Controller/RecipeController.php @@ -94,6 +94,7 @@ public function show($id) { * @param $id * * @return DataResponse + * @todo Parameter id is never used. Fix that */ public function update($id) { $this->dbCacheService->triggerCheck(); @@ -145,7 +146,7 @@ public function destroy($id) { try { $this->service->deleteRecipe($id); - return new DataResponse('Recipe ' . $_GET['id'] . ' deleted successfully', Http::STATUS_OK); + return new DataResponse('Recipe ' . $id . ' deleted successfully', Http::STATUS_OK); } catch (\Exception $e) { return new DataResponse($e->getMessage(), 502); } diff --git a/tests/Unit/Controller/ConfigControllerTest.php b/tests/Unit/Controller/ConfigControllerTest.php new file mode 100644 index 000000000..16a39a18a --- /dev/null +++ b/tests/Unit/Controller/ConfigControllerTest.php @@ -0,0 +1,173 @@ + + * @covers :: + */ +class ConfigControllerTest extends TestCase { + + /** + * @var ConfigController|MockObject + */ + private $sut; + /** + * @var RecipeService|MockObject + */ + private $recipeService; + /** + * @var DbCacheService|MockObject + */ + private $dbCacheService; + /** + * @var RestParameterParser|MockObject + */ + private $restParser; + /** + * @var IRequest|MockObject + */ + private $request; + + public function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->recipeService = $this->createMock(RecipeService::class); + $this->dbCacheService = $this->createMock(DbCacheService::class); + $this->restParser = $this->createMock(RestParameterParser::class); + + $this->sut = new ConfigController('cookbook', $this->request, $this->recipeService, $this->dbCacheService, $this->restParser); + } + + /** + * @covers ::__construct + */ + public function testConstructor(): void { + $this->ensurePropertyIsCorrect('service', $this->recipeService); + $this->ensurePropertyIsCorrect('dbCacheService', $this->dbCacheService); + $this->ensurePropertyIsCorrect('restParser', $this->restParser); + } + + private function ensurePropertyIsCorrect(string $name, &$val) { + $property = new ReflectionProperty(ConfigController::class, $name); + $property->setAccessible(true); + $this->assertSame($val, $property->getValue($this->sut)); + } + + /** + * @covers ::reindex + */ + public function testReindex(): void { + $this->dbCacheService->expects($this->once())->method('updateCache'); + + /** + * @var Response $response + */ + $response = $this->sut->reindex(); + + $this->assertEquals(200, $response->getStatus()); + } + + /** + * @covers ::list + */ + public function testList(): void { + $this->dbCacheService->expects($this->once())->method('triggerCheck'); + + $folder = '/the/folder/to/check'; + $interval = 5 * 60; + $printImage = true; + + $expectedData = [ + 'folder' => $folder, + 'update_interval' => $interval, + 'print_image' => $printImage, + ]; + + $this->recipeService->method('getUserFolderPath')->willReturn($folder); + $this->dbCacheService->method('getSearchIndexUpdateInterval')->willReturn($interval); + $this->recipeService->method('getPrintImage')->willReturn($printImage); + + /** + * @var DataResponse $response + */ + $response = $this->sut->list(); + + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals($expectedData, $response->getData()); + } + + /** + * @dataProvider dataProviderConfig + * @covers ::config + */ + public function testConfig($data, $folderPath, $interval, $printImage): void { + $this->restParser->method('getParameters')->willReturn($data); + + $this->dbCacheService->expects($this->once())->method('triggerCheck'); + + if (is_null($folderPath)) { + $this->recipeService->expects($this->never())->method('setUserFolderPath'); + $this->dbCacheService->expects($this->never())->method('updateCache'); + } else { + $this->recipeService->expects($this->once())->method('setUserFolderPath')->with($folderPath); + $this->dbCacheService->expects($this->once())->method('updateCache'); + } + + if (is_null($interval)) { + $this->recipeService->expects($this->never())->method('setSearchIndexUpdateInterval'); + } else { + $this->recipeService->expects($this->once())->method('setSearchIndexUpdateInterval')->with($interval); + } + + if (is_null($printImage)) { + $this->recipeService->expects($this->never())->method('setPrintImage'); + } else { + $this->recipeService->expects($this->once())->method('setPrintImage')->with($printImage); + } + + /** + * @var DataResponse $response + */ + $response = $this->sut->config(); + + $this->assertEquals(200, $response->getStatus()); + } + + public function dataProviderConfig() { + return [ + 'noChange' => [ + [], null, null, null + ], + 'changeFolder' => [ + ['folder' => '/path/to/whatever'], '/path/to/whatever', null, null + ], + 'changeinterval' => [ + ['update_interval' => 15], null, 15, null + ], + 'changePrint' => [ + ['print_image' => true], null, null, true + ], + 'changeAll' => [ + [ + 'folder' => '/my/custom/path', + 'update_interval' => 12, + 'print_image' => false + ], '/my/custom/path', 12, false + ], + ]; + } +} diff --git a/tests/Unit/Controller/MainControllerTest.php b/tests/Unit/Controller/MainControllerTest.php new file mode 100644 index 000000000..3e9d5ef56 --- /dev/null +++ b/tests/Unit/Controller/MainControllerTest.php @@ -0,0 +1,666 @@ + + * @covers :: + */ +class MainControllerTest extends TestCase { + + /** + * @var MockObject|RecipeService + */ + private $recipeService; + /** + * @var IURLGenerator|MockObject + */ + private $urlGenerator; + /** + * @var DbCacheService|MockObject + */ + private $dbCacheService; + /** + * @var RestParameterParser|MockObject + */ + private $restParser; + + /** + * @var MainController + */ + private $sut; + + public function setUp(): void { + parent::setUp(); + + $this->recipeService = $this->createMock(RecipeService::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->dbCacheService = $this->createMock(DbCacheService::class); + $this->restParser = $this->createMock(RestParameterParser::class); + $request = $this->createStub(IRequest::class); + + $this->sut = new MainController('cookbook', $request, $this->recipeService, $this->dbCacheService, $this->urlGenerator, $this->restParser); + } + + /** + * @covers ::__construct + */ + public function testConstructor(): void { + $this->ensurePropertyIsCorrect('urlGenerator', $this->urlGenerator); + $this->ensurePropertyIsCorrect('service', $this->recipeService); + $this->ensurePropertyIsCorrect('dbCacheService', $this->dbCacheService); + $this->ensurePropertyIsCorrect('restParser', $this->restParser); + } + + private function ensurePropertyIsCorrect(string $name, &$val) { + $property = new ReflectionProperty(MainController::class, $name); + $property->setAccessible(true); + $this->assertSame($val, $property->getValue($this->sut)); + } + + private function ensureCacheCheckTriggered(): void { + $this->dbCacheService->expects($this->once())->method('triggerCheck'); + } + + /** + * @covers ::index + */ + public function testIndex(): void { + $this->ensureCacheCheckTriggered(); + $ret = $this->sut->index(); + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals('index', $ret->getTemplateName()); + } + + /** + * @covers ::index + */ + public function testIndexInvalidUser(): void { + $this->recipeService->method('getFolderForUser')->willThrowException(new UserFolderNotWritableException()); + $ret = $this->sut->index(); + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals('invalid_guest', $ret->getTemplateName()); + } + + /** + * @covers ::getApiVersion + */ + public function testGetAPIVersion(): void { + $ret = $this->sut->getApiVersion(); + $this->assertEquals(200, $ret->getStatus()); + + $retData = $ret->getData(); + $this->assertTrue(isset($retData['cookbook_version'])); + $this->assertEquals(3, count($retData['cookbook_version'])); + $this->assertTrue(isset($retData['api_version'])); + $this->assertTrue(isset($retData['api_version']['epoch'])); + $this->assertTrue(isset($retData['api_version']['major'])); + $this->assertTrue(isset($retData['api_version']['minor'])); + } + + /** + * @covers ::categories + */ + public function testGetCategories(): void { + $this->ensureCacheCheckTriggered(); + + $cat = ['Foo', 'Bar', 'Baz']; + $this->recipeService->expects($this->once())->method('getAllCategoriesInSearchIndex')->willReturn($cat); + + $ret = $this->sut->categories(); + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($cat, $ret->getData()); + } + + /** + * @covers ::keywords + */ + public function testGetKeywords(): void { + $this->ensureCacheCheckTriggered(); + + $kw = ['Foo', 'Bar', 'Baz']; + $this->recipeService->expects($this->once())->method('getAllKeywordsInSearchIndex')->willReturn($kw); + + $ret = $this->sut->keywords(); + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($kw, $ret->getData()); + } + + /** + * @covers ::new + * @dataProvider dataProviderNew + */ + public function testNew($data, $id): void { + $this->ensureCacheCheckTriggered(); + + $this->restParser->method('getParameters')->willReturn($data); + $file = $this->createMock(File::class); + $file->method('getParent')->willReturnSelf(); + $file->method('getId')->willReturn($id); + $this->recipeService->expects($this->once())->method('addRecipe')->with($data)->willReturn($file); + $this->dbCacheService->expects($this->once())->method('addRecipe')->with($file); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->new(); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($id, $ret->getData()); + } + + public function dataProviderNew() { + return [ + 'success' => [ + ['some', 'recipe', 'data'], + 10 + ], + ]; + } + + /** + * @covers ::new + * @dataProvider dataProviderNew + */ + public function testNewFailed($data, $id): void { + $this->ensureCacheCheckTriggered(); + + $errMsg = 'My error message'; + + $this->restParser->method('getParameters')->willReturn($data); + $this->recipeService->expects($this->once())->method('addRecipe')->with($data)->willThrowException(new Exception($errMsg)); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->new(); + + $this->assertEquals(500, $ret->getStatus()); + $this->assertEquals($errMsg, $ret->getData()); + } + + /** + * @covers ::update + * @dataProvider dataProviderUpdate + */ + public function testUpdate($data, $id): void { + $this->ensureCacheCheckTriggered(); + + $this->restParser->method('getParameters')->willReturn($data); + $file = $this->createMock(File::class); + $file->method('getParent')->willReturnSelf(); + $file->method('getId')->willReturn($id); + $this->recipeService->expects($this->once())->method('addRecipe')->with($data)->willReturn($file); + $this->dbCacheService->expects($this->once())->method('addRecipe')->with($file); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->update($id); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($id, $ret->getData()); + } + + public function dataProviderUpdate() { + return [ + 'success' => [ + ['some', 'recipe', 'data', 'id' => 10], + 10 + ], + ]; + } + + /** + * @covers ::update + * @dataProvider dataProviderUpdate + */ + public function testUpdateFailed($data, $id): void { + $this->ensureCacheCheckTriggered(); + + $errMsg = 'My error message'; + + $this->restParser->method('getParameters')->willReturn($data); + $this->recipeService->expects($this->once())->method('addRecipe')->with($data)->willThrowException(new Exception($errMsg)); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->update($id); + + $this->assertEquals(500, $ret->getStatus()); + $this->assertEquals($errMsg, $ret->getData()); + } + + /** + * @covers ::import + */ + public function testImportFailed(): void { + $this->ensureCacheCheckTriggered(); + + $this->restParser->method('getParameters')->willReturn([]); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->import(); + + $this->assertEquals(400, $ret->getStatus()); + } + + /** + * @covers ::import + */ + public function testImport(): void { + $this->ensureCacheCheckTriggered(); + + $url = 'http://example.com/Recipe.html'; + $file = $this->createStub(File::class); + $json = [ + 'id' => 123, + 'name' => 'The recipe name', + ]; + + $this->restParser->method('getParameters')->willReturn([ 'url' => $url ]); + $this->recipeService->expects($this->once())->method('downloadRecipe')->with($url)->willReturn($file); + $this->recipeService->expects($this->once())->method('parseRecipeFile')->with($file)->willReturn($json); + $this->dbCacheService->expects($this->once())->method('addRecipe')->with($file); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->import(); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($json, $ret->getData()); + } + + /** + * @covers ::import + */ + public function testImportExisting(): void { + $this->ensureCacheCheckTriggered(); + + $url = 'http://example.com/Recipe.html'; + $errorMsg = 'The error message'; + $ex = new RecipeExistsException($errorMsg); + + $this->restParser->method('getParameters')->willReturn([ 'url' => $url ]); + $this->recipeService->expects($this->once())->method('downloadRecipe')->with($url)->willThrowException($ex); + + /** + * @var JSONResponse $ret + */ + $ret = $this->sut->import(); + + $expected = [ + 'msg' => $ex->getMessage(), + 'line' => $ex->getLine(), + 'file' => $ex->getFile(), + ]; + + $this->assertEquals(409, $ret->getStatus()); + $this->assertEquals($expected, $ret->getData()); + } + + /** + * @covers ::import + */ + public function testImportOther(): void { + $this->ensureCacheCheckTriggered(); + + $url = 'http://example.com/Recipe.html'; + $errorMsg = 'The error message'; + $ex = new Exception($errorMsg); + + $this->restParser->method('getParameters')->willReturn([ 'url' => $url ]); + $this->recipeService->expects($this->once())->method('downloadRecipe')->with($url)->willThrowException($ex); + + /** + * @var JSONResponse $ret + */ + $ret = $this->sut->import(); + + + $this->assertEquals(400, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()); + } + + /** + * @covers ::category + * @dataProvider dataProviderCategory + */ + public function testCategory($cat, $recipes): void { + $this->ensureCacheCheckTriggered(); + + $this->recipeService->method('getRecipesByCategory')->with($cat)->willReturn($recipes); + + $expected = $this->getExpectedRecipes($recipes); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->category(urlencode($cat)); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($expected, $ret->getData()); + } + + private function getExpectedRecipes($recipes): array { + $ret = $recipes; + + $ids = []; + for ($i = 0; $i < count($recipes); $i++) { + $id = $recipes[$i]['recipe_id']; + $ids[] = $id; + $ret[$i]['imageUrl'] = "/path/to/image/$id/thumb"; + $ret[$i]['imagePlaceholderUrl'] = "/path/to/image/$id/thumb16"; + } + + $this->urlGenerator->method('linkToRoute')->with( + 'cookbook.recipe.image', + $this->callback(function ($p) use ($ids) { + return isset($p['id']) && isset($p['size']) && false !== array_search($p['id'], $ids); + }) + )->willReturnCallback(function ($name, $p) use ($ret) { + // return $ret[$idx[$p['id']]]; + $id = $p['id']; + $size = $p['size']; + return "/path/to/image/$id/$size"; + }); + + return $ret; + } + + public function dataProviderCategory(): array { + return [ + 'noRecipes' => [ + 'My category', + [] + ], + 'someRecipes' => [ + 'My category', + [ + [ + 'name' => 'My recipe 1', + 'recipe_id' => 123, + ], + [ + 'name' => 'My recipe 2', + 'recipe_id' => 122, + ], + ] + ], + ]; + } + + /** + * @covers ::category + */ + public function testCategoryFailed(): void { + $this->ensureCacheCheckTriggered(); + + $cat = 'My category'; + $errorMsg = 'The error is found.'; + $this->recipeService->method('getRecipesByCategory')->with($cat)->willThrowException(new Exception($errorMsg)); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->category(urlencode($cat)); + + $this->assertEquals(500, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()); + } + + /** + * @covers ::tags + * @dataProvider dataProviderTags + */ + public function testTags($keywords, $recipes): void { + $this->ensureCacheCheckTriggered(); + + $this->recipeService->method('getRecipesByKeywords')->with($keywords)->willReturn($recipes); + + $expected = $this->getExpectedRecipes($recipes); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->tags(urlencode($keywords)); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($expected, $ret->getData()); + } + + public function dataProviderTags(): array { + return [ + 'noTag' => [ + '*', + [ + [ + 'name' => 'My recipe 1', + 'recipe_id' => 123, + ], + [ + 'name' => 'My recipe 2', + 'recipe_id' => 122, + ], + ] + ], + 'noRecipes' => [ + 'Tag A,Tag B', + [] + ], + 'someRecipes' => [ + 'Tag A, Tag B', + [ + [ + 'name' => 'My recipe 1', + 'recipe_id' => 123, + ], + [ + 'name' => 'My recipe 2', + 'recipe_id' => 122, + ], + ] + ], + ]; + } + + /** + * @covers ::tags + */ + public function testTagsFailed(): void { + $this->ensureCacheCheckTriggered(); + + $keywords = 'Tag 1,Tag B'; + $errorMsg = 'The error is found.'; + $this->recipeService->method('getRecipesByKeywords')->with($keywords)->willThrowException(new Exception($errorMsg)); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->tags(urlencode($keywords)); + + $this->assertEquals(500, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()); + } + + /** + * @covers ::search + * @dataProvider dpSearch + * @todo no implementation in controller + */ + public function testSearch($query, $recipes): void { + $this->ensureCacheCheckTriggered(); + + $this->recipeService->expects($this->once())->method('findRecipesInSearchIndex')->with($query)->willReturn($recipes); + + $expected = $this->getExpectedRecipes($recipes); + + /** + * @var DataResponse $res + */ + $res = $this->sut->search(urlencode($query)); + + $this->assertEquals(200, $res->getStatus()); + $this->assertEquals($expected, $res->getData()); + } + + public function dpSearch() { + return [ + 'noRecipes' => [ + 'some query', + [], + ], + 'someRecipes' => [ + 'some query', + [ + [ + 'name' => 'First recipe', + 'recipe_id' => 123, + ], + ], + ], + ]; + } + + /** + * @covers ::search + */ + public function testSearchFailed(): void { + $this->ensureCacheCheckTriggered(); + + $query = 'some query'; + $errorMsg = 'Could not search for recipes'; + $this->recipeService->expects($this->once())->method('findRecipesInSearchIndex')->with($query)->willThrowException(new Exception($errorMsg)); + + /** + * @var DataResponse $res + */ + $res = $this->sut->search(urlencode($query)); + + $this->assertEquals(500, $res->getStatus()); + $this->assertEquals($errorMsg, $res->getData()); + } + + /** + * @covers ::categoryUpdate + * @dataProvider dataProviderCategoryUpdateNoName + */ + public function testCategoryUpdateNoName($requestParams): void { + $this->ensureCacheCheckTriggered(); + + $this->restParser->expects($this->once())->method('getParameters')->willReturn($requestParams); + + $ret = $this->sut->categoryUpdate(''); + + $this->assertEquals(400, $ret->getStatus()); + } + + public function dataProviderCategoryUpdateNoName() { + yield [[]]; + yield [[ + 'some', 'variable' + ]]; + yield [['name' => null]]; + yield [['name' => '']]; + } + + /** + * @covers ::categoryUpdate + * @dataProvider dpCategoryUpdate + * @todo No business logic in controller + */ + public function testCategoryUpdate($cat, $oldCat, $recipes): void { + $this->ensureCacheCheckTriggered(); + + $this->recipeService->expects($this->once())->method('getRecipesByCategory')->with($oldCat)->willReturn($recipes); + $this->dbCacheService->expects($this->once())->method('updateCache'); + + $this->restParser->expects($this->once())->method('getParameters')->willReturn(['name' => $cat]); + + $n = count($recipes); + $indices = array_map(function ($v) { + return [$v['recipe_id']]; + }, $recipes); + $this->recipeService->expects($this->exactly($n))->method('getRecipeById')->withConsecutive(...$indices); + $this->recipeService->expects($this->exactly($n))->method('addRecipe')->with($this->callback(function ($p) use ($cat) { + return $p['recipeCategory'] === $cat; + })); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->categoryUpdate(urlencode($oldCat)); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($cat, $ret->getData()); + } + + public function dpCategoryUpdate() { + return [ + 'noRecipes' => [ + 'new Category Name', + 'Old category', + [] + ], + 'someRecipes' => [ + 'new Category Name', + 'Old category', + [ + [ + 'name' => 'First recipe', + 'recipeCategory' => 'some fancy category', + 'recipe_id' => 123, + ], + [ + 'name' => 'Second recipe', + 'recipeCategory' => 'some fancy category', + 'recipe_id' => 124, + ], + ] + ], + ]; + } + + /** + * @covers ::categoryUpdate + */ + public function testCategoryUpdateFailure(): void { + $this->ensureCacheCheckTriggered(); + + $this->restParser->expects($this->once())->method('getParameters')->willReturn(['name' => 'New category']); + + $errorMsg = 'Something bad has happened.'; + $oldCat = 'Old category'; + + $this->recipeService->expects($this->once())->method('getRecipesByCategory')->with($oldCat)->willThrowException(new Exception($errorMsg)); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->categoryUpdate(urlencode($oldCat)); + + $this->assertEquals(500, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()); + } +} diff --git a/tests/Unit/Controller/RecipeControllerTest.php b/tests/Unit/Controller/RecipeControllerTest.php new file mode 100644 index 000000000..cbec87e80 --- /dev/null +++ b/tests/Unit/Controller/RecipeControllerTest.php @@ -0,0 +1,370 @@ + + * @covers :: + */ +class RecipeControllerTest extends TestCase { + /** + * @var RecipeService|MockObject + */ + private $recipeService; + /** + * @var IURLGenerator|MockObject + */ + private $urlGenerator; + /** + * @var DbCacheService|MockObject + */ + private $dbCacheService; + /** + * @var RestParameterParser|MockObject + */ + private $restParser; + + /** + * @var RecipeController + */ + private $sut; + + public function setUp(): void { + parent::setUp(); + + $this->recipeService = $this->createMock(RecipeService::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->dbCacheService = $this->createMock(DbCacheService::class); + $this->restParser = $this->createMock(RestParameterParser::class); + $request = $this->createStub(IRequest::class); + + $this->sut = new RecipeController('cookbook', $request, $this->urlGenerator, $this->recipeService, $this->dbCacheService, $this->restParser); + } + + /** + * @covers ::__construct + */ + public function testConstructor(): void { + $this->ensurePropertyIsCorrect('urlGenerator', $this->urlGenerator); + $this->ensurePropertyIsCorrect('service', $this->recipeService); + $this->ensurePropertyIsCorrect('dbCacheService', $this->dbCacheService); + $this->ensurePropertyIsCorrect('restParser', $this->restParser); + } + + private function ensurePropertyIsCorrect(string $name, &$val) { + $property = new ReflectionProperty(RecipeController::class, $name); + $property->setAccessible(true); + $this->assertSame($val, $property->getValue($this->sut)); + } + + private function ensureCacheCheckTriggered(): void { + $this->dbCacheService->expects($this->once())->method('triggerCheck'); + } + + /** + * @covers ::update + * @todo Foo + */ + public function testUpdate(): void { + $this->ensureCacheCheckTriggered(); + + $data = ['a', 'new', 'array']; + /** + * @var File|MockObject $file + */ + $file = $this->createMock(File::class); + $file->method('getParent')->willReturnSelf(); + $file->method('getId')->willReturn(50); + + $this->restParser->method('getParameters')->willReturn($data); + $this->recipeService->expects($this->once())->method('addRecipe')->with($data)->willReturn($file); + $this->dbCacheService->expects($this->once())->method('addRecipe')->with($file); + + $ret = $this->sut->update(1); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals(50, $ret->getData()); + } + + /** + * @covers ::create + */ + public function testCreate(): void { + $this->ensureCacheCheckTriggered(); + + $recipe = ['a', 'recipe', 'as', 'array']; + $this->restParser->method('getParameters')->willReturn($recipe); + + /** + * @var File|MockObject $file + */ + $file = $this->createMock(File::class); + $id = 23; + $file->method('getId')->willReturn($id); + $file->method('getParent')->willReturnSelf(); + + $this->recipeService->expects($this->once())->method('addRecipe')->with($recipe)->willReturn($file); + + $this->dbCacheService->expects($this->once())->method('addRecipe')->with($file); + + $ret = $this->sut->create(); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($id, $ret->getData()); + } + + /** + * @covers ::create + */ + public function testCreateExisting(): void { + $this->ensureCacheCheckTriggered(); + + $recipe = ['a', 'recipe', 'as', 'array']; + $this->restParser->method('getParameters')->willReturn($recipe); + + $ex = new RecipeExistsException('message'); + $this->recipeService->expects($this->once())->method('addRecipe')->with($recipe)->willThrowException($ex); + + $this->dbCacheService->expects($this->never())->method('addRecipe'); + + $ret = $this->sut->create(); + + $this->assertEquals(409, $ret->getStatus()); + $expected = [ + 'msg' => 'message', + 'file' => __FILE__, + 'line' => $ex->getLine(), + ]; + $this->assertEquals($expected, $ret->getData()); + } + + /** + * @covers ::show + */ + public function testShow(): void { + $this->ensureCacheCheckTriggered(); + + $id = 123; + $recipe = [ + 'name' => "My Name", + 'description' => 'a useful description', + 'id' => $id, + ]; + $this->recipeService->method('getRecipeById')->with($id)->willReturn($recipe); + $this->recipeService->method('getPrintImage')->willReturn(true); + $imageUrl = "/path/to/image/of/id/123"; + + $this->urlGenerator->method('linkToRoute')->with( + 'cookbook.recipe.image', + $this->callback(function ($p) use ($id) { + return isset($p['size']) && $p['id'] === $id; + }) + )->willReturn($imageUrl); + $expected = $recipe; + $expected['printImage'] = true; + $expected['imageUrl'] = $imageUrl; + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->show($id); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($expected, $ret->getData()); + } + + /** + * @covers ::show + */ + public function testShowFailure(): void { + $this->ensureCacheCheckTriggered(); + + $id = 123; + $this->recipeService->method('getRecipeById')->with($id)->willReturn(null); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->show($id); + + $this->assertEquals(404, $ret->getStatus()); + } + + /** + * @covers ::destroy + */ + public function testDestroy(): void { + $this->ensureCacheCheckTriggered(); + $id = 123; + + $this->recipeService->expects($this->once())->method('deleteRecipe')->with($id); + /** + * @var DataResponse $ret + */ + $ret = $this->sut->destroy($id); + + $this->assertEquals(200, $ret->getStatus()); + } + + /** + * @covers ::destroy + */ + public function testDestroyFailed(): void { + $this->ensureCacheCheckTriggered(); + $id = 123; + $errorMsg = 'This is the error message.'; + $this->recipeService->expects($this->once())->method('deleteRecipe')->with($id)->willThrowException(new Exception($errorMsg)); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->destroy($id); + + $this->assertEquals(502, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()); + } + + /** + * @covers ::image + * @dataProvider dataProviderImage + * @todo Assert on image data/file name + * @todo Avoid business codde in controller + */ + public function testImage($setSize, $size): void { + $this->ensureCacheCheckTriggered(); + + if ($setSize) { + $_GET['size'] = $size; + } + + $file = $this->createStub(File::class); + $id = 123; + $this->recipeService->method('getRecipeImageFileByFolderId')->with($id, $size)->willReturn($file); + + /** + * @var FileDisplayResponse $ret + */ + $ret = $this->sut->image($id); + + $this->assertEquals(200, $ret->getStatus()); + + // Hack: Get output via IOutput mockup + /** + * @var MockObject|Ioutput $output + */ + $output = $this->createMock(IOutput::class); + $file->method('getSize')->willReturn(100); + $content = 'Some content comes here'; + $file->method('getContent')->willReturn($content); + + $output->method('getHttpResponseCode')->willReturn(Http::STATUS_OK); + $output->expects($this->atLeastOnce())->method('setOutput')->with($content); + + $ret->callback($output); + } + + public function dataProviderImage(): array { + return [ + [false, null], + [true, null], + [true, 'full'], + ]; + } + + /** + * @covers ::index + * @dataProvider dataProviderIndex + * @todo no work on controller + */ + public function testIndex($recipes, $setKeywords, $keywords): void { + $this->ensureCacheCheckTriggered(); + + $this->recipeService->method('getAllRecipesInSearchIndex')->willReturn($recipes); + $this->recipeService->method('findRecipesInSearchIndex')->willReturn($recipes); + + if ($setKeywords) { + $_GET['keywords'] = $keywords; + $this->recipeService->expects($this->once())->method('findRecipesInSearchIndex')->with($keywords); + } else { + $this->recipeService->expects($this->once())->method('getAllRecipesInSearchIndex'); + } + + $this->urlGenerator->method('linkToRoute')->will($this->returnCallback(function ($name, $params) { + if ($name !== 'cookbook.recipe.image') { + throw new Exception('Must use correct controller'); + } + + $id = $params['id']; + $size = $params['size']; + return "/path/to/controller/$id/$size"; + })); + + /** + * @var DataResponse $ret + */ + $ret = $this->sut->index(); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($this->updateIndexRecipesAsExpected($recipes), $ret->getData()); + + // $this->markTestIncomplete('assertions are missing'); + } + + private function updateIndexRecipesAsExpected($recipes): array { + $ret = $recipes; + for ($i = 0; $i < count($ret); $i++) { + $id = $ret[$i]['recipe_id']; + $ret[$i]['imageUrl'] = "/path/to/controller/$id/thumb"; + $ret[$i]['imagePlaceholderUrl'] = "/path/to/controller/$id/thumb16"; + } + + return $ret; + } + + public function dataProviderIndex(): array { + return [ + 'emptyIndex' => [ + [], + false, + null, + ], + 'normalIndex' => [ + [ + [ + 'recipe_id' => 123, + 'name' => 'First recipe', + ], + [ + 'recipe_id' => 125, + 'name' => 'Second recipe', + ], + ], + false, + null, + ], + 'empySearch' => [ + [], + true, + 'a,b,c', + ], + ]; + } +}