From f6c9ab0c5d184000ac887e545891b26da7d35ba3 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 3 Sep 2021 14:36:51 +0200 Subject: [PATCH 1/8] Added test for first conteroller Signed-off-by: Christian Wolf --- .../Unit/Controller/ConfigControllerTest.php | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 tests/Unit/Controller/ConfigControllerTest.php diff --git a/tests/Unit/Controller/ConfigControllerTest.php b/tests/Unit/Controller/ConfigControllerTest.php new file mode 100644 index 000000000..2e374d06c --- /dev/null +++ b/tests/Unit/Controller/ConfigControllerTest.php @@ -0,0 +1,182 @@ + + * @covers :: + */ +class ConfigControllerTest extends TestCase { + + /** + * @var ConfigController|MockObject + */ + private $sut; + /** + * @var RecipeService|MockObject + */ + private $recipeService; + /** + * @var IURLGenerator|MockObject + */ + private $urlGenerator; + /** + * @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->urlGenerator = $this->createMock(IURLGenerator::class); + $this->dbCacheService = $this->createMock(DbCacheService::class); + $this->restParser = $this->createMock(RestParameterParser::class); + + $this->sut = new ConfigController('cookbook', $this->request, $this->urlGenerator, $this->recipeService, $this->dbCacheService, $this->restParser); + } + + /** + * @covers ::__construct + */ + public function testConstructor(): void { + $this->ensurePropertyIsCorrect('service', $this->recipeService); + $this->ensurePropertyIsCorrect('urlGenerator', $this->urlGenerator); + $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 + ], + ]; + } + +} From fce78517aef22967ae495805c04db26dfd55b7fb Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 3 Sep 2021 14:42:30 +0200 Subject: [PATCH 2/8] Removed obsolete property from class Signed-off-by: Christian Wolf --- lib/Controller/ConfigController.php | 8 +------- tests/Unit/Controller/ConfigControllerTest.php | 9 +-------- 2 files changed, 2 insertions(+), 15 deletions(-) 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/tests/Unit/Controller/ConfigControllerTest.php b/tests/Unit/Controller/ConfigControllerTest.php index 2e374d06c..31dba543d 100644 --- a/tests/Unit/Controller/ConfigControllerTest.php +++ b/tests/Unit/Controller/ConfigControllerTest.php @@ -3,7 +3,6 @@ namespace OCA\Cookbook\tests\Unit\Controller; use OCP\IRequest; -use OCP\IURLGenerator; use PHPUnit\Framework\TestCase; use OCA\Cookbook\Service\RecipeService; use OCA\Cookbook\Service\DbCacheService; @@ -30,10 +29,6 @@ class ConfigControllerTest extends TestCase { * @var RecipeService|MockObject */ private $recipeService; - /** - * @var IURLGenerator|MockObject - */ - private $urlGenerator; /** * @var DbCacheService|MockObject */ @@ -52,11 +47,10 @@ public function setUp(): void { $this->request = $this->createMock(IRequest::class); $this->recipeService = $this->createMock(RecipeService::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->dbCacheService = $this->createMock(DbCacheService::class); $this->restParser = $this->createMock(RestParameterParser::class); - $this->sut = new ConfigController('cookbook', $this->request, $this->urlGenerator, $this->recipeService, $this->dbCacheService, $this->restParser); + $this->sut = new ConfigController('cookbook', $this->request, $this->recipeService, $this->dbCacheService, $this->restParser); } /** @@ -64,7 +58,6 @@ public function setUp(): void { */ public function testConstructor(): void { $this->ensurePropertyIsCorrect('service', $this->recipeService); - $this->ensurePropertyIsCorrect('urlGenerator', $this->urlGenerator); $this->ensurePropertyIsCorrect('dbCacheService', $this->dbCacheService); $this->ensurePropertyIsCorrect('restParser', $this->restParser); } From 73dd58a10416eab185e00a8ca173d244fb256934 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 10 Sep 2021 20:14:36 +0200 Subject: [PATCH 3/8] Added tests for RecipeController Signed-off-by: Christian Wolf --- .../Unit/Controller/RecipeControllerTest.php | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 tests/Unit/Controller/RecipeControllerTest.php diff --git a/tests/Unit/Controller/RecipeControllerTest.php b/tests/Unit/Controller/RecipeControllerTest.php new file mode 100644 index 000000000..93378efd7 --- /dev/null +++ b/tests/Unit/Controller/RecipeControllerTest.php @@ -0,0 +1,374 @@ + + * @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 [ + [], + false, + null, + ], + 'normalIndex' => [ + [ + [ + 'recipe_id' => 123, + 'name' => 'First recipe', + ], + [ + 'recipe_id' => 125, + 'name' => 'Second recipe', + ], + ], + false, + null, + ], + 'empySearch' => [ + [], + true, + 'a,b,c', + ], + ]; + } +} From 94c1ac72ee6d7cc31155cc8c419cb0f12638e6f2 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 10 Sep 2021 20:14:52 +0200 Subject: [PATCH 4/8] Update recipe controller to use parameter correctly Signed-off-by: Christian Wolf --- lib/Controller/RecipeController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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); } From f17126c4bfb350eccb04ff614800c28c1254f85e Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sat, 11 Sep 2021 13:59:38 +0200 Subject: [PATCH 5/8] Updated MainController to remove obsolete code Signed-off-by: Christian Wolf --- lib/Controller/MainController.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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); } } From bb903e7bc00f211fea93000e2ad4ff798902cfc8 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sat, 11 Sep 2021 14:00:02 +0200 Subject: [PATCH 6/8] Added test code for MainController Signed-off-by: Christian Wolf --- tests/Unit/Controller/MainControllerTest.php | 667 +++++++++++++++++++ 1 file changed, 667 insertions(+) create mode 100644 tests/Unit/Controller/MainControllerTest.php diff --git a/tests/Unit/Controller/MainControllerTest.php b/tests/Unit/Controller/MainControllerTest.php new file mode 100644 index 000000000..6873f58d3 --- /dev/null +++ b/tests/Unit/Controller/MainControllerTest.php @@ -0,0 +1,667 @@ + + * @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; $iurlGenerator->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()); + } +} From edcf07e313e94f70f8c7e17bfd1f014fe9800424 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sat, 11 Sep 2021 14:04:18 +0200 Subject: [PATCH 7/8] Updated changelog Signed-off-by: Christian Wolf --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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 From 14702b4fdc42eb2a218fedeaedb090add3d16a75 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Sat, 11 Sep 2021 14:05:20 +0200 Subject: [PATCH 8/8] Corrected code styling Signed-off-by: Christian Wolf --- .../Unit/Controller/ConfigControllerTest.php | 10 ++++------ tests/Unit/Controller/MainControllerTest.php | 17 ++++++++-------- .../Unit/Controller/RecipeControllerTest.php | 20 ++++++++----------- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/tests/Unit/Controller/ConfigControllerTest.php b/tests/Unit/Controller/ConfigControllerTest.php index 31dba543d..16a39a18a 100644 --- a/tests/Unit/Controller/ConfigControllerTest.php +++ b/tests/Unit/Controller/ConfigControllerTest.php @@ -10,7 +10,6 @@ use PHPUnit\Framework\MockObject\MockObject; use OCA\Cookbook\Controller\ConfigController; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use ReflectionProperty; @@ -89,7 +88,7 @@ public function testList(): void { $this->dbCacheService->expects($this->once())->method('triggerCheck'); $folder = '/the/folder/to/check'; - $interval = 5*60; + $interval = 5 * 60; $printImage = true; $expectedData = [ @@ -120,7 +119,7 @@ public function testConfig($data, $folderPath, $interval, $printImage): void { $this->dbCacheService->expects($this->once())->method('triggerCheck'); - if(is_null($folderPath)){ + if (is_null($folderPath)) { $this->recipeService->expects($this->never())->method('setUserFolderPath'); $this->dbCacheService->expects($this->never())->method('updateCache'); } else { @@ -128,13 +127,13 @@ public function testConfig($data, $folderPath, $interval, $printImage): void { $this->dbCacheService->expects($this->once())->method('updateCache'); } - if(is_null($interval)){ + 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)) { + if (is_null($printImage)) { $this->recipeService->expects($this->never())->method('setPrintImage'); } else { $this->recipeService->expects($this->once())->method('setPrintImage')->with($printImage); @@ -171,5 +170,4 @@ public function dataProviderConfig() { ], ]; } - } diff --git a/tests/Unit/Controller/MainControllerTest.php b/tests/Unit/Controller/MainControllerTest.php index 6873f58d3..3e9d5ef56 100644 --- a/tests/Unit/Controller/MainControllerTest.php +++ b/tests/Unit/Controller/MainControllerTest.php @@ -47,8 +47,7 @@ class MainControllerTest extends TestCase { */ private $sut; - public function setUp(): void - { + public function setUp(): void { parent::setUp(); $this->recipeService = $this->createMock(RecipeService::class); @@ -63,16 +62,14 @@ public function setUp(): void /** * @covers ::__construct */ - public function testConstructor(): void - { + 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) - { + private function ensurePropertyIsCorrect(string $name, &$val) { $property = new ReflectionProperty(MainController::class, $name); $property->setAccessible(true); $this->assertSame($val, $property->getValue($this->sut)); @@ -370,7 +367,7 @@ private function getExpectedRecipes($recipes): array { $ret = $recipes; $ids = []; - for($i=0; $irestParser->expects($this->once())->method('getParameters')->willReturn(['name' => $cat]); $n = count($recipes); - $indices = array_map(function ($v) {return [$v['recipe_id']];}, $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){ + $this->recipeService->expects($this->exactly($n))->method('addRecipe')->with($this->callback(function ($p) use ($cat) { return $p['recipeCategory'] === $cat; })); diff --git a/tests/Unit/Controller/RecipeControllerTest.php b/tests/Unit/Controller/RecipeControllerTest.php index 93378efd7..cbec87e80 100644 --- a/tests/Unit/Controller/RecipeControllerTest.php +++ b/tests/Unit/Controller/RecipeControllerTest.php @@ -24,8 +24,7 @@ * @covers :: * @covers :: */ -class RecipeControllerTest extends TestCase -{ +class RecipeControllerTest extends TestCase { /** * @var RecipeService|MockObject */ @@ -48,8 +47,7 @@ class RecipeControllerTest extends TestCase */ private $sut; - public function setUp(): void - { + public function setUp(): void { parent::setUp(); $this->recipeService = $this->createMock(RecipeService::class); @@ -64,16 +62,14 @@ public function setUp(): void /** * @covers ::__construct */ - public function testConstructor(): void - { + 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) - { + private function ensurePropertyIsCorrect(string $name, &$val) { $property = new ReflectionProperty(RecipeController::class, $name); $property->setAccessible(true); $this->assertSame($val, $property->getValue($this->sut)); @@ -255,7 +251,7 @@ public function testDestroyFailed(): void { public function testImage($setSize, $size): void { $this->ensureCacheCheckTriggered(); - if($setSize) { + if ($setSize) { $_GET['size'] = $size; } @@ -304,7 +300,7 @@ public function testIndex($recipes, $setKeywords, $keywords): void { $this->recipeService->method('getAllRecipesInSearchIndex')->willReturn($recipes); $this->recipeService->method('findRecipesInSearchIndex')->willReturn($recipes); - if($setKeywords) { + if ($setKeywords) { $_GET['keywords'] = $keywords; $this->recipeService->expects($this->once())->method('findRecipesInSearchIndex')->with($keywords); } else { @@ -312,7 +308,7 @@ public function testIndex($recipes, $setKeywords, $keywords): void { } $this->urlGenerator->method('linkToRoute')->will($this->returnCallback(function ($name, $params) { - if($name !== 'cookbook.recipe.image') { + if ($name !== 'cookbook.recipe.image') { throw new Exception('Must use correct controller'); } @@ -334,7 +330,7 @@ public function testIndex($recipes, $setKeywords, $keywords): void { private function updateIndexRecipesAsExpected($recipes): array { $ret = $recipes; - for($i=0; $i