From da73a4fe2d966304b982fa21a733b2161ee36a1a Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Wed, 31 Aug 2022 19:53:54 +0200 Subject: [PATCH 1/7] Added API routes and Controllers Signed-off-by: Christian Wolf --- .github/actions/deploy/update-data.sh | 2 +- appinfo/routes.php | 22 +- lib/Controller/CategoryApiController.php | 80 ++++++ lib/Controller/ConfigApiController.php | 91 ++++++ lib/Controller/KeywordApiController.php | 41 +++ lib/Controller/RecipeApiController.php | 348 +++++++++++++++++++++++ lib/Controller/UtilApiController.php | 26 ++ 7 files changed, 607 insertions(+), 3 deletions(-) create mode 100644 lib/Controller/CategoryApiController.php create mode 100644 lib/Controller/ConfigApiController.php create mode 100644 lib/Controller/KeywordApiController.php create mode 100644 lib/Controller/RecipeApiController.php create mode 100644 lib/Controller/UtilApiController.php diff --git a/.github/actions/deploy/update-data.sh b/.github/actions/deploy/update-data.sh index 55d9fec2b..9939331da 100755 --- a/.github/actions/deploy/update-data.sh +++ b/.github/actions/deploy/update-data.sh @@ -36,5 +36,5 @@ version_arr="$major, $minor, $patch" if [ -n "$suffix" ]; then version_arr="$version_arr, '-$suffix'" fi -sed "/VERSION_TAG/s@[[].*[]]@[$version_arr]@" -i lib/Controller/MainController.php +sed "/VERSION_TAG/s@[[].*[]]@[$version_arr]@" -i lib/Controller/UtilApiController.php git add lib/Controller/MainController.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 6243f4c7f..0c54bd834 100755 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -14,7 +14,6 @@ * If you add new features here, increase the minor version of the API. * If you change the behavior or remove functionality, increase the major version there. */ - ['name' => 'main#getApiVersion', 'url' => '/api/version', 'verb' => 'GET'], ['name' => 'main#index', 'url' => '/', 'verb' => 'GET'], ['name' => 'main#keywords', 'url' => '/keywords', 'verb' => 'GET'], ['name' => 'main#categories', 'url' => '/categories', 'verb' => 'GET'], @@ -28,10 +27,29 @@ ['name' => 'main#categoryUpdate', 'url' => '/api/category/{category}', 'verb' => 'PUT'], ['name' => 'main#tags', 'url' => '/api/tags/{keywords}', 'verb' => 'GET'], ['name' => 'main#search', 'url' => '/api/search/{query}', 'verb' => 'GET'], + + /* API routes */ + + ['name' => 'util_api#getApiVersion', 'url' => '/api/version', 'verb' => 'GET'], + + ['name' => 'recipe_api#image', 'url' => '/api/v1/recipes/{id}/image', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], + ['name' => 'recipe_api#import', 'url' => '/api/v1/import', 'verb' => 'POST'], + ['name' => 'keyword_api#keywords', 'url' => '/api/v1/keywords', 'verb' => 'GET'], + ['name' => 'category_api#categories', 'url' => '/api/v1/categories', 'verb' => 'GET'], + ['name' => 'category_api#rename', 'url' => '/api/v1/category/{category}', 'verb' => 'PUT'], + ['name' => 'config_api#list', 'url' => '/api/v1/config', 'verb' => 'GET'], + ['name' => 'config_api#config', 'url' => '/api/v1/config', 'verb' => 'POST'], + ['name' => 'config_api#reindex', 'url' => '/api/v1/reindex', 'verb' => 'POST'], + ['name' => 'recipe_api#category', 'url' => '/api/v1/category/{category}', 'verb' => 'GET'], + ['name' => 'recipe_api#tags', 'url' => '/api/v1/tags/{keywords}', 'verb' => 'GET'], + ['name' => 'recipe_api#search', 'url' => '/api/v1/search/{query}', 'verb' => 'GET'], + + ['name' => 'util_api#preflighted_cors', 'url' => '/api/{path}', 'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']], ], /* API resources */ 'resources' => [ - 'recipe' => ['url' => '/api/recipes'] + 'recipe' => ['url' => '/api/recipes'], + 'recipe_api' => ['url' => '/api/v1/recipes'], ] ]; diff --git a/lib/Controller/CategoryApiController.php b/lib/Controller/CategoryApiController.php new file mode 100644 index 000000000..31e74d6d0 --- /dev/null +++ b/lib/Controller/CategoryApiController.php @@ -0,0 +1,80 @@ +appName = $AppName; + $this->service = $service; + $this->dbCacheService = $dbCacheService; + $this->restParser = $restParameterParser; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + */ + public function categories() { + $this->dbCacheService->triggerCheck(); + + $categories = $this->service->getAllCategoriesInSearchIndex(); + return new JSONResponse($categories, 200); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + * @param JSONResponse $category + */ + public function rename($category) { + $this->dbCacheService->triggerCheck(); + + $json = $this->restParser->getParameters(); + if (!$json || !isset($json['name']) || !$json['name']) { + return new JSONResponse('New category name not found in data', 400); + } + + $category = urldecode($category); + try { + $recipes = $this->service->getRecipesByCategory($category); + foreach ($recipes as $recipe) { + $r = $this->service->getRecipeById($recipe['recipe_id']); + $r['recipeCategory'] = $json['name']; + $this->service->addRecipe($r); + } + // Update cache + $this->dbCacheService->updateCache(); + + return new JSONResponse($json['name'], Http::STATUS_OK); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 500); + } + } +} diff --git a/lib/Controller/ConfigApiController.php b/lib/Controller/ConfigApiController.php new file mode 100644 index 000000000..4032246b2 --- /dev/null +++ b/lib/Controller/ConfigApiController.php @@ -0,0 +1,91 @@ +service = $recipeService; + $this->dbCacheService = $dbCacheService; + $this->restParser = $restParser; + $this->userFolder = $userFolder; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + */ + public function list() { + $this->dbCacheService->triggerCheck(); + + return new JSONResponse([ + 'folder' => $this->userFolder->getPath(), + 'update_interval' => $this->dbCacheService->getSearchIndexUpdateInterval(), + 'print_image' => $this->service->getPrintImage(), + ], Http::STATUS_OK); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + */ + public function config() { + $data = $this->restParser->getParameters(); + + if (isset($data['folder'])) { + $this->userFolder->setPath($data['folder']); + $this->dbCacheService->updateCache(); + } + + if (isset($data['update_interval'])) { + $this->service->setSearchIndexUpdateInterval($data['update_interval']); + } + + if (isset($data['print_image'])) { + $this->service->setPrintImage((bool)$data['print_image']); + } + + $this->dbCacheService->triggerCheck(); + + return new JSONResponse('OK', Http::STATUS_OK); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + */ + public function reindex() { + $this->dbCacheService->updateCache(); + + return new JSONResponse('Search index rebuilt successfully', Http::STATUS_OK); + } +} diff --git a/lib/Controller/KeywordApiController.php b/lib/Controller/KeywordApiController.php new file mode 100644 index 000000000..42de9aba9 --- /dev/null +++ b/lib/Controller/KeywordApiController.php @@ -0,0 +1,41 @@ +service = $recipeService; + $this->dbCacheService = $dbCacheService; + } + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + */ + public function keywords() { + $this->dbCacheService->triggerCheck(); + + $keywords = $this->service->getAllKeywordsInSearchIndex(); + return new JSONResponse($keywords, 200); + } +} diff --git a/lib/Controller/RecipeApiController.php b/lib/Controller/RecipeApiController.php new file mode 100644 index 000000000..be9466a45 --- /dev/null +++ b/lib/Controller/RecipeApiController.php @@ -0,0 +1,348 @@ +dbCacheService->triggerCheck(); + + if (empty($_GET['keywords'])) { + $recipes = $this->service->getAllRecipesInSearchIndex(); + } else { + $recipes = $this->service->findRecipesInSearchIndex(isset($_GET['keywords']) ? $_GET['keywords'] : ''); + } + foreach ($recipes as $i => $recipe) { + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb']); + $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb16']); + } + return new JSONResponse($recipes, Http::STATUS_OK); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + * @param int $id + * @return JSONResponse + */ + public function show($id) { + $this->dbCacheService->triggerCheck(); + + $json = $this->service->getRecipeById($id); + + if (null === $json) { + return new JSONResponse($id, Http::STATUS_NOT_FOUND); + } + + $json['printImage'] = $this->service->getPrintImage(); + $json['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $json['id'], 'size' => 'full']); + + $json = $this->outputFilter->filter($json); + + return new JSONResponse($json, Http::STATUS_OK); + } + + /** + * Update an existing recipe. + * + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + * @param $id + * @return JSONResponse + * @todo Parameter id is never used. Fix that + */ + public function update($id) { + $this->dbCacheService->triggerCheck(); + + $recipeData = $this->restParser->getParameters(); + try { + $file = $this->service->addRecipe($recipeData); + } catch (NoRecipeNameGivenException $ex) { + $json = [ + 'msg' => $ex->getMessage(), + 'file' => $ex->getFile(), + 'line' => $ex->getLine(), + ]; + return new JSONResponse($json, Http::STATUS_UNPROCESSABLE_ENTITY); + } + $this->dbCacheService->addRecipe($file); + + return new JSONResponse($file->getParent()->getId(), Http::STATUS_OK); + } + + /** + * Create a new recipe. + * + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + * + * @return JSONResponse + */ + public function create() { + $this->dbCacheService->triggerCheck(); + + $recipeData = $this->restParser->getParameters(); + try { + $file = $this->service->addRecipe($recipeData); + $this->dbCacheService->addRecipe($file); + + return new JSONResponse($file->getParent()->getId(), Http::STATUS_OK); + } catch (RecipeExistsException $ex) { + $json = [ + 'msg' => $ex->getMessage(), + 'file' => $ex->getFile(), + 'line' => $ex->getLine(), + ]; + return new JSONResponse($json, Http::STATUS_CONFLICT); + } catch (NoRecipeNameGivenException $ex) { + $json = [ + 'msg' => $ex->getMessage(), + 'file' => $ex->getFile(), + 'line' => $ex->getLine(), + ]; + return new JSONResponse($json, Http::STATUS_UNPROCESSABLE_ENTITY); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + * @param int $id + * @return JSONResponse + */ + public function destroy($id) { + $this->dbCacheService->triggerCheck(); + + try { + $this->service->deleteRecipe($id); + return new JSONResponse('Recipe ' . $id . ' deleted successfully', Http::STATUS_OK); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 502); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + * @param $id + * @return JSONResponse|FileDisplayResponse|DataDisplayResponse + */ + public function image($id) { + $this->dbCacheService->triggerCheck(); + + $acceptHeader = $this->request->getHeader('Accept'); + $acceptedExtensions = $this->acceptHeaderParser->parseHeader($acceptHeader); + + $size = isset($_GET['size']) ? $_GET['size'] : null; + + try { + $file = $this->service->getRecipeImageFileByFolderId($id, $size); + + return new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/jpeg', 'Cache-Control' => 'public, max-age=604800']); + } catch (\Exception $e) { + if (array_search('svg', $acceptedExtensions, true) === false) { + // We may not serve a SVG image. Tell the client about the missing image. + $json = [ + 'msg' => $this->l->t('No image with the matching MIME type was found on the server.'), + ]; + return new JSONResponse($json, Http::STATUS_NOT_ACCEPTABLE); + } else { + // The client accepts the SVG file. Send it. + $file = file_get_contents(dirname(__FILE__) . '/../../img/recipe.svg'); + + return new DataDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']); + } + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + */ + public function import() { + $this->dbCacheService->triggerCheck(); + + $data = $this->restParser->getParameters(); + + if (!isset($data['url'])) { + return new JSONResponse('Field "url" is required', 400); + } + + try { + $recipe_file = $this->service->downloadRecipe($data['url']); + $recipe_json = $this->service->parseRecipeFile($recipe_file); + $this->dbCacheService->addRecipe($recipe_file); + + return new JSONResponse($recipe_json, Http::STATUS_OK); + } catch (RecipeExistsException $ex) { + $json = [ + 'msg' => $ex->getMessage(), + 'line' => $ex->getLine(), + 'file' => $ex->getFile(), + ]; + return new JSONResponse($json, Http::STATUS_CONFLICT); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 400); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + * @param string $query + * @return JSONResponse + */ + public function search($query) { + $this->dbCacheService->triggerCheck(); + + $query = urldecode($query); + try { + $recipes = $this->service->findRecipesInSearchIndex($query); + + foreach ($recipes as $i => $recipe) { + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb', + 't' => $this->service->getRecipeMTime($recipe['recipe_id']) + ] + ); + $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb16' + ] + ); + } + + return new JSONResponse($recipes, 200, ['Content-Type' => 'application/json']); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 500); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + * @param string $category + * @return JSONResponse + */ + public function category($category) { + $this->dbCacheService->triggerCheck(); + + $category = urldecode($category); + try { + $recipes = $this->service->getRecipesByCategory($category); + foreach ($recipes as $i => $recipe) { + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb', + 't' => $this->service->getRecipeMTime($recipe['recipe_id']) + ] + ); + $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb16' + ] + ); + } + + return new JSONResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 500); + } + } + + + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @CORS + * @param string $keywords + * @return JSONResponse + */ + public function tags($keywords) { + $this->dbCacheService->triggerCheck(); + $keywords = urldecode($keywords); + + try { + $recipes = $this->service->getRecipesByKeywords($keywords); + foreach ($recipes as $i => $recipe) { + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb', + 't' => $this->service->getRecipeMTime($recipe['recipe_id']) + ] + ); + $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb16' + ] + ); + } + + return new JSONResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); + } catch (\Exception $e) { + // error_log($e->getMessage()); + return new JSONResponse($e->getMessage(), 500); + } + } +} diff --git a/lib/Controller/UtilApiController.php b/lib/Controller/UtilApiController.php new file mode 100644 index 000000000..6070ce1cd --- /dev/null +++ b/lib/Controller/UtilApiController.php @@ -0,0 +1,26 @@ + [0, 9, 14], /* VERSION_TAG do not change this line manually */ + 'api_version' => [ + 'epoch' => 0, + 'major' => 1, + 'minor' => 0 + ] + ]; + return new JSONResponse($response, 200); + } +} From aa930fb073a779e8ea0a089089510be95288daea Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 2 Sep 2022 09:51:10 +0200 Subject: [PATCH 2/7] Migrate all Controllers to new structure Signed-off-by: Christian Wolf --- appinfo/routes.php | 43 +- lib/Controller/CategoryApiController.php | 66 +- lib/Controller/CategoryController.php | 45 ++ lib/Controller/ConfigApiController.php | 66 +- lib/Controller/ConfigController.php | 78 +- .../Implementation/CategoryImplementation.php | 71 ++ .../Implementation/ConfigImplementation.php | 93 +++ .../Implementation/KeywordImplementation.php | 33 + .../Implementation/RecipeImplementation.php | 352 +++++++++ lib/Controller/KeywordApiController.php | 22 +- lib/Controller/KeywordController.php | 34 + lib/Controller/MainController.php | 281 +------ lib/Controller/RecipeApiController.php | 244 +----- lib/Controller/RecipeController.php | 217 ++---- lib/Controller/UtilApiController.php | 6 + .../Controller/AbstractControllerTestCase.php | 67 ++ .../Controller/CategoryApiControllerTest.php | 33 + .../Controller/CategoryControllerTest.php | 33 + .../Controller/ConfigApiControllerTest.php | 43 + .../Unit/Controller/ConfigControllerTest.php | 172 +--- .../CategoryImplementationTest.php | 197 +++++ .../ConfigImplementationTest.php | 170 ++++ .../KeywordImplementationTest.php | 59 ++ .../RecipeImplementationTest.php | 736 ++++++++++++++++++ .../Controller/KeywordApiControllerTest.php | 35 + .../Unit/Controller/KeywordControllerTest.php | 34 + tests/Unit/Controller/MainControllerTest.php | 585 +------------- .../Controller/RecipeApiControllerTest.php | 63 ++ .../Unit/Controller/RecipeControllerTest.php | 426 +--------- .../Unit/Controller/UtilApiControllerTest.php | 69 ++ 30 files changed, 2378 insertions(+), 1995 deletions(-) create mode 100644 lib/Controller/CategoryController.php create mode 100644 lib/Controller/Implementation/CategoryImplementation.php create mode 100644 lib/Controller/Implementation/ConfigImplementation.php create mode 100644 lib/Controller/Implementation/KeywordImplementation.php create mode 100644 lib/Controller/Implementation/RecipeImplementation.php create mode 100644 lib/Controller/KeywordController.php create mode 100644 tests/Unit/Controller/AbstractControllerTestCase.php create mode 100644 tests/Unit/Controller/CategoryApiControllerTest.php create mode 100644 tests/Unit/Controller/CategoryControllerTest.php create mode 100644 tests/Unit/Controller/ConfigApiControllerTest.php create mode 100644 tests/Unit/Controller/Implementation/CategoryImplementationTest.php create mode 100644 tests/Unit/Controller/Implementation/ConfigImplementationTest.php create mode 100644 tests/Unit/Controller/Implementation/KeywordImplementationTest.php create mode 100644 tests/Unit/Controller/Implementation/RecipeImplementationTest.php create mode 100644 tests/Unit/Controller/KeywordApiControllerTest.php create mode 100644 tests/Unit/Controller/KeywordControllerTest.php create mode 100644 tests/Unit/Controller/RecipeApiControllerTest.php create mode 100644 tests/Unit/Controller/UtilApiControllerTest.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 0c54bd834..2546c21d6 100755 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -14,42 +14,53 @@ * If you add new features here, increase the minor version of the API. * If you change the behavior or remove functionality, increase the major version there. */ + + // The static HTML template ['name' => 'main#index', 'url' => '/', 'verb' => 'GET'], - ['name' => 'main#keywords', 'url' => '/keywords', 'verb' => 'GET'], - ['name' => 'main#categories', 'url' => '/categories', 'verb' => 'GET'], - ['name' => 'main#import', 'url' => '/import', 'verb' => 'POST'], - ['name' => 'recipe#image', 'url' => '/recipes/{id}/image', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], - ['name' => 'config#reindex', 'url' => '/reindex', 'verb' => 'POST'], - ['name' => 'config#list', 'url' => '/config', 'verb' => 'GET'], - ['name' => 'config#config', 'url' => '/config', 'verb' => 'POST'], - /* API routes */ - ['name' => 'main#category', 'url' => '/api/category/{category}', 'verb' => 'GET'], - ['name' => 'main#categoryUpdate', 'url' => '/api/category/{category}', 'verb' => 'PUT'], - ['name' => 'main#tags', 'url' => '/api/tags/{keywords}', 'verb' => 'GET'], - ['name' => 'main#search', 'url' => '/api/search/{query}', 'verb' => 'GET'], + + // The /webapp routes + ['name' => 'recipe#image', 'url' => '/webapp/recipes/{id}/image', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], + ['name' => 'recipe#import', 'url' => '/webapp/import', 'verb' => 'POST'], + ['name' => 'recipe#category', 'url' => '/webapp/category/{category}', 'verb' => 'GET'], + ['name' => 'recipe#tags', 'url' => '/webapp/tags/{keywords}', 'verb' => 'GET'], + ['name' => 'recipe#search', 'url' => '/webapp/search/{query}', 'verb' => 'GET'], + + ['name' => 'keyword#keywords', 'url' => '/webapp/keywords', 'verb' => 'GET'], + + ['name' => 'category#categories', 'url' => '/webapp/categories', 'verb' => 'GET'], + ['name' => 'category#rename', 'url' => '/webapp/category/{category}', 'verb' => 'PUT'], + + ['name' => 'config#list', 'url' => '/webapp/config', 'verb' => 'GET'], + ['name' => 'config#config', 'url' => '/webapp/config', 'verb' => 'POST'], + ['name' => 'config#reindex', 'url' => '/webapp/reindex', 'verb' => 'POST'], /* API routes */ + // Generic routes on /api ['name' => 'util_api#getApiVersion', 'url' => '/api/version', 'verb' => 'GET'], + // APIv1 routes under /api/v1 ['name' => 'recipe_api#image', 'url' => '/api/v1/recipes/{id}/image', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], ['name' => 'recipe_api#import', 'url' => '/api/v1/import', 'verb' => 'POST'], + ['name' => 'recipe_api#category', 'url' => '/api/v1/category/{category}', 'verb' => 'GET'], + ['name' => 'recipe_api#tags', 'url' => '/api/v1/tags/{keywords}', 'verb' => 'GET'], + ['name' => 'recipe_api#search', 'url' => '/api/v1/search/{query}', 'verb' => 'GET'], + ['name' => 'keyword_api#keywords', 'url' => '/api/v1/keywords', 'verb' => 'GET'], + ['name' => 'category_api#categories', 'url' => '/api/v1/categories', 'verb' => 'GET'], ['name' => 'category_api#rename', 'url' => '/api/v1/category/{category}', 'verb' => 'PUT'], + ['name' => 'config_api#list', 'url' => '/api/v1/config', 'verb' => 'GET'], ['name' => 'config_api#config', 'url' => '/api/v1/config', 'verb' => 'POST'], ['name' => 'config_api#reindex', 'url' => '/api/v1/reindex', 'verb' => 'POST'], - ['name' => 'recipe_api#category', 'url' => '/api/v1/category/{category}', 'verb' => 'GET'], - ['name' => 'recipe_api#tags', 'url' => '/api/v1/tags/{keywords}', 'verb' => 'GET'], - ['name' => 'recipe_api#search', 'url' => '/api/v1/search/{query}', 'verb' => 'GET'], ['name' => 'util_api#preflighted_cors', 'url' => '/api/{path}', 'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']], ], /* API resources */ 'resources' => [ - 'recipe' => ['url' => '/api/recipes'], + 'recipe' => ['url' => '/webapp/recipes'], 'recipe_api' => ['url' => '/api/v1/recipes'], ] ]; diff --git a/lib/Controller/CategoryApiController.php b/lib/Controller/CategoryApiController.php index 31e74d6d0..b72e1c35e 100644 --- a/lib/Controller/CategoryApiController.php +++ b/lib/Controller/CategoryApiController.php @@ -2,79 +2,47 @@ namespace OCA\Cookbook\Controller; -use OCA\Cookbook\Helper\RestParameterParser; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Service\RecipeService; -use OCP\AppFramework\Http; +use OCA\Cookbook\Controller\Implementation\CategoryImplementation; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; -class CategoryApiController extends ApiController { - protected $appName; - - /** @var RecipeService */ - private $service; - /** @var DbCacheService */ - private $dbCacheService; - /** @var RestParameterParser */ - private $restParser; +class CategoryApiController extends ApiController +{ + /** @var CategoryImplementation */ + private $impl; public function __construct( string $AppName, IRequest $request, - RecipeService $service, - DbCacheService $dbCacheService, - RestParameterParser $restParameterParser + CategoryImplementation $categoryImplementation ) { parent::__construct($AppName, $request); - $this->appName = $AppName; - $this->service = $service; - $this->dbCacheService = $dbCacheService; - $this->restParser = $restParameterParser; + $this->impl = $categoryImplementation; } /** * @NoAdminRequired * @NoCSRFRequired * @CORS + * + * @return JSONResponse */ - public function categories() { - $this->dbCacheService->triggerCheck(); - - $categories = $this->service->getAllCategoriesInSearchIndex(); - return new JSONResponse($categories, 200); + public function categories() + { + return $this->impl->index(); } /** * @NoAdminRequired * @NoCSRFRequired * @CORS - * @param JSONResponse $category + * @param string $category + * @return JSONResponse */ - public function rename($category) { - $this->dbCacheService->triggerCheck(); - - $json = $this->restParser->getParameters(); - if (!$json || !isset($json['name']) || !$json['name']) { - return new JSONResponse('New category name not found in data', 400); - } - - $category = urldecode($category); - try { - $recipes = $this->service->getRecipesByCategory($category); - foreach ($recipes as $recipe) { - $r = $this->service->getRecipeById($recipe['recipe_id']); - $r['recipeCategory'] = $json['name']; - $this->service->addRecipe($r); - } - // Update cache - $this->dbCacheService->updateCache(); - - return new JSONResponse($json['name'], Http::STATUS_OK); - } catch (\Exception $e) { - return new JSONResponse($e->getMessage(), 500); - } + public function rename($category) + { + return $this->impl->rename($category); } } diff --git a/lib/Controller/CategoryController.php b/lib/Controller/CategoryController.php new file mode 100644 index 000000000..a98565499 --- /dev/null +++ b/lib/Controller/CategoryController.php @@ -0,0 +1,45 @@ +impl = $categoryImplementation; + } + + /** + * @NoAdminRequired + * + * @return JSONResponse + */ + public function categories() + { + return $this->impl->index(); + } + + /** + * @NoAdminRequired + * @param string $category + * @return JSONResponse + */ + public function rename($category) + { + return $this->impl->rename($category); + } +} diff --git a/lib/Controller/ConfigApiController.php b/lib/Controller/ConfigApiController.php index 4032246b2..53e03e549 100644 --- a/lib/Controller/ConfigApiController.php +++ b/lib/Controller/ConfigApiController.php @@ -2,90 +2,56 @@ namespace OCA\Cookbook\Controller; +use OCA\Cookbook\Controller\Implementation\ConfigImplementation; use OCP\IRequest; -use OCP\AppFramework\Http; use OCP\AppFramework\ApiController; -use OCA\Cookbook\Service\RecipeService; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Helper\UserFolderHelper; -use OCA\Cookbook\Helper\RestParameterParser; use OCP\AppFramework\Http\JSONResponse; -class CategoryApiController extends ApiController { - /** @var RecipeService */ - private $service; - /** @var DbCacheService */ - private $dbCacheService; - /** @var RestParameterParser */ - private $restParser; - /** @var UserFolderHelper */ - private $userFolder; +class ConfigApiController extends ApiController { + + /** @var ConfigImplementation */ + private $implementation; public function __construct( $AppName, IRequest $request, - RecipeService $recipeService, - DbCacheService $dbCacheService, - RestParameterParser $restParser, - UserFolderHelper $userFolder + ConfigImplementation $configImplementation ) { parent::__construct($AppName, $request); - $this->service = $recipeService; - $this->dbCacheService = $dbCacheService; - $this->restParser = $restParser; - $this->userFolder = $userFolder; + $this->implementation = $configImplementation; } /** * @NoAdminRequired * @NoCSRFRequired * @CORS + * + * @return JSONResponse */ public function list() { - $this->dbCacheService->triggerCheck(); - - return new JSONResponse([ - 'folder' => $this->userFolder->getPath(), - 'update_interval' => $this->dbCacheService->getSearchIndexUpdateInterval(), - 'print_image' => $this->service->getPrintImage(), - ], Http::STATUS_OK); + return $this->implementation->list(); } /** * @NoAdminRequired * @NoCSRFRequired * @CORS + * + * @return JSONResponse */ public function config() { - $data = $this->restParser->getParameters(); - - if (isset($data['folder'])) { - $this->userFolder->setPath($data['folder']); - $this->dbCacheService->updateCache(); - } - - if (isset($data['update_interval'])) { - $this->service->setSearchIndexUpdateInterval($data['update_interval']); - } - - if (isset($data['print_image'])) { - $this->service->setPrintImage((bool)$data['print_image']); - } - - $this->dbCacheService->triggerCheck(); - - return new JSONResponse('OK', Http::STATUS_OK); + return $this->implementation->config(); } /** * @NoAdminRequired * @NoCSRFRequired * @CORS + * + * @return JSONResponse */ public function reindex() { - $this->dbCacheService->updateCache(); - - return new JSONResponse('Search index rebuilt successfully', Http::STATUS_OK); + return $this->implementation->reindex(); } } diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index 0d773aea7..178573f16 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -3,98 +3,48 @@ namespace OCA\Cookbook\Controller; use OCP\IRequest; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Controller; -use OCA\Cookbook\Service\RecipeService; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Helper\RestParameterParser; -use OCA\Cookbook\Helper\UserFolderHelper; +use OCA\Cookbook\Controller\Implementation\ConfigImplementation; class ConfigController extends Controller { - /** - * @var RecipeService - */ - private $service; - - /** - * @var DbCacheService - */ - private $dbCacheService; - - /** - * @var RestParameterParser - */ - private $restParser; - - /** - * @var UserFolderHelper - */ - private $userFolder; + /** @var ConfigImplementation */ + private $implementation; public function __construct( $AppName, IRequest $request, - RecipeService $recipeService, - DbCacheService $dbCacheService, - RestParameterParser $restParser, - UserFolderHelper $userFolder + ConfigImplementation $configImplementation ) { parent::__construct($AppName, $request); - $this->service = $recipeService; - $this->dbCacheService = $dbCacheService; - $this->restParser = $restParser; - $this->userFolder = $userFolder; + $this->implementation = $configImplementation; } /** * @NoAdminRequired - * @NoCSRFRequired + * + * @return JSONResponse */ public function list() { - $this->dbCacheService->triggerCheck(); - - return new DataResponse([ - 'folder' => $this->userFolder->getPath(), - 'update_interval' => $this->dbCacheService->getSearchIndexUpdateInterval(), - 'print_image' => $this->service->getPrintImage(), - ], Http::STATUS_OK); + return $this->implementation->list(); } /** * @NoAdminRequired - * @NoCSRFRequired + * + * @return JSONResponse */ public function config() { - $data = $this->restParser->getParameters(); - - if (isset($data['folder'])) { - $this->userFolder->setPath($data['folder']); - $this->dbCacheService->updateCache(); - } - - if (isset($data['update_interval'])) { - $this->service->setSearchIndexUpdateInterval($data['update_interval']); - } - - if (isset($data['print_image'])) { - $this->service->setPrintImage((bool)$data['print_image']); - } - - $this->dbCacheService->triggerCheck(); - - return new DataResponse('OK', Http::STATUS_OK); + return $this->implementation->config(); } /** * @NoAdminRequired - * @NoCSRFRequired + * + * @return JSONResponse */ public function reindex() { - $this->dbCacheService->updateCache(); - - return new DataResponse('Search index rebuilt successfully', Http::STATUS_OK); + return $this->implementation->reindex(); } } diff --git a/lib/Controller/Implementation/CategoryImplementation.php b/lib/Controller/Implementation/CategoryImplementation.php new file mode 100644 index 000000000..ac48fb0c0 --- /dev/null +++ b/lib/Controller/Implementation/CategoryImplementation.php @@ -0,0 +1,71 @@ +service = $service; + $this->dbCacheService = $dbCacheService; + $this->restParser = $restParameterParser; + } + + /** + * List all available categories. + * + * @return JSONResponse + */ + public function index() { + $this->dbCacheService->triggerCheck(); + + $categories = $this->service->getAllCategoriesInSearchIndex(); + return new JSONResponse($categories, 200); + } + + /** + * Rename a category. + * + * @param string $category + * @return JSONResponse + */ + public function rename($category) { + $this->dbCacheService->triggerCheck(); + + $json = $this->restParser->getParameters(); + if (!$json || !isset($json['name']) || !$json['name']) { + return new JSONResponse('New category name not found in data', 400); + } + + $category = urldecode($category); + try { + $recipes = $this->service->getRecipesByCategory($category); + foreach ($recipes as $recipe) { + $r = $this->service->getRecipeById($recipe['recipe_id']); + $r['recipeCategory'] = $json['name']; + $this->service->addRecipe($r); + } + // Update cache + $this->dbCacheService->updateCache(); + + return new JSONResponse($json['name'], Http::STATUS_OK); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 500); + } + } +} diff --git a/lib/Controller/Implementation/ConfigImplementation.php b/lib/Controller/Implementation/ConfigImplementation.php new file mode 100644 index 000000000..0ebe91c63 --- /dev/null +++ b/lib/Controller/Implementation/ConfigImplementation.php @@ -0,0 +1,93 @@ +service = $recipeService; + $this->dbCacheService = $dbCacheService; + $this->restParser = $restParser; + $this->userFolder = $userFolder; + } + + /** + * Get the current configuration of the app + * + * @return JSONResponse + */ + public function list() { + $this->dbCacheService->triggerCheck(); + + return new JSONResponse([ + 'folder' => $this->userFolder->getPath(), + 'update_interval' => $this->dbCacheService->getSearchIndexUpdateInterval(), + 'print_image' => $this->service->getPrintImage(), + ], Http::STATUS_OK); + } + + /** + * Store the configuration in the database. + * + * The value to be stored is extracted from the request directly. + * + * Note that only those values are stored, that are present in the parameter. + * All other configurations are not altered. + * + * @return JSONResponse + */ + public function config() { + $data = $this->restParser->getParameters(); + + if (isset($data['folder'])) { + $this->userFolder->setPath($data['folder']); + $this->dbCacheService->updateCache(); + } + + if (isset($data['update_interval'])) { + $this->service->setSearchIndexUpdateInterval($data['update_interval']); + } + + if (isset($data['print_image'])) { + $this->service->setPrintImage((bool)$data['print_image']); + } + + $this->dbCacheService->triggerCheck(); + + return new JSONResponse('OK', Http::STATUS_OK); + } + + /** + * Trigger a reindex/rescan of the current recipe folder. + * + * @return JSONResponse + */ + public function reindex() { + $this->dbCacheService->updateCache(); + + return new JSONResponse('Search index rebuilt successfully', Http::STATUS_OK); + } + +} diff --git a/lib/Controller/Implementation/KeywordImplementation.php b/lib/Controller/Implementation/KeywordImplementation.php new file mode 100644 index 000000000..c67b0bd09 --- /dev/null +++ b/lib/Controller/Implementation/KeywordImplementation.php @@ -0,0 +1,33 @@ +service = $recipeService; + $this->dbCacheService = $dbCacheService; + } + /** + * List all available keywords. + * + * @return JSONResponse + */ + public function index() { + $this->dbCacheService->triggerCheck(); + + $keywords = $this->service->getAllKeywordsInSearchIndex(); + return new JSONResponse($keywords, 200); + } +} diff --git a/lib/Controller/Implementation/RecipeImplementation.php b/lib/Controller/Implementation/RecipeImplementation.php new file mode 100644 index 000000000..6170bfa96 --- /dev/null +++ b/lib/Controller/Implementation/RecipeImplementation.php @@ -0,0 +1,352 @@ +request = $request; + $this->service = $recipeService; + $this->dbCacheService = $dbCacheService; + $this->urlGenerator = $iURLGenerator; + $this->restParser = $restParameterParser; + $this->outputFilter = $recipeJSONOutputFilter; + $this->acceptHeaderParser = $acceptHeaderParsingHelper; + $this->l = $iL10N; + } + + /** + * List all recipes as stubs + */ + public function index() { + $this->dbCacheService->triggerCheck(); + + if (empty($_GET['keywords'])) { + $recipes = $this->service->getAllRecipesInSearchIndex(); + } else { + $recipes = $this->service->findRecipesInSearchIndex(isset($_GET['keywords']) ? $_GET['keywords'] : ''); + } + foreach ($recipes as $i => $recipe) { + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb']); + $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb16']); + } + return new JSONResponse($recipes, Http::STATUS_OK); + } + + /** + * Fetch a single recipe + * + * @param int $id + * @return JSONResponse + */ + public function show($id) { + $this->dbCacheService->triggerCheck(); + + $json = $this->service->getRecipeById($id); + + if (null === $json) { + return new JSONResponse($id, Http::STATUS_NOT_FOUND); + } + + $json['printImage'] = $this->service->getPrintImage(); + $json['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $json['id'], 'size' => 'full']); + + $json = $this->outputFilter->filter($json); + + return new JSONResponse($json, Http::STATUS_OK); + } + + /** + * Update an existing recipe. + * + * @param $id The id of the recipe in question + * @return JSONResponse + * @todo Parameter id is never used. Fix that + */ + public function update($id) { + $this->dbCacheService->triggerCheck(); + + $recipeData = $this->restParser->getParameters(); + try { + $file = $this->service->addRecipe($recipeData); + } catch (NoRecipeNameGivenException $ex) { + $json = [ + 'msg' => $ex->getMessage(), + 'file' => $ex->getFile(), + 'line' => $ex->getLine(), + ]; + return new JSONResponse($json, Http::STATUS_UNPROCESSABLE_ENTITY); + } + $this->dbCacheService->addRecipe($file); + + return new JSONResponse($file->getParent()->getId(), Http::STATUS_OK); + } + + /** + * Create a new recipe. + * + * @return JSONResponse + */ + public function create() { + $this->dbCacheService->triggerCheck(); + + $recipeData = $this->restParser->getParameters(); + try { + $file = $this->service->addRecipe($recipeData); + $this->dbCacheService->addRecipe($file); + + return new JSONResponse($file->getParent()->getId(), Http::STATUS_OK); + } catch (RecipeExistsException $ex) { + $json = [ + 'msg' => $ex->getMessage(), + 'file' => $ex->getFile(), + 'line' => $ex->getLine(), + ]; + return new JSONResponse($json, Http::STATUS_CONFLICT); + } catch (NoRecipeNameGivenException $ex) { + $json = [ + 'msg' => $ex->getMessage(), + 'file' => $ex->getFile(), + 'line' => $ex->getLine(), + ]; + return new JSONResponse($json, Http::STATUS_UNPROCESSABLE_ENTITY); + } + } + + /** + * Remove a recipe + * + * @param int $id The ifd of the recipe in question + * @return JSONResponse + */ + public function destroy($id) { + $this->dbCacheService->triggerCheck(); + + try { + $this->service->deleteRecipe($id); + return new JSONResponse('Recipe ' . $id . ' deleted successfully', Http::STATUS_OK); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 502); + } + } + + /** + * Get the image associated with a recipe + * + * @param $id The id of the recipe + * @return JSONResponse|FileDisplayResponse|DataDisplayResponse + */ + public function image($id) { + $this->dbCacheService->triggerCheck(); + + $acceptHeader = $this->request->getHeader('Accept'); + $acceptedExtensions = $this->acceptHeaderParser->parseHeader($acceptHeader); + + $size = isset($_GET['size']) ? $_GET['size'] : null; + + try { + $file = $this->service->getRecipeImageFileByFolderId($id, $size); + + return new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/jpeg', 'Cache-Control' => 'public, max-age=604800']); + } catch (\Exception $e) { + if (array_search('svg', $acceptedExtensions, true) === false) { + // We may not serve a SVG image. Tell the client about the missing image. + $json = [ + 'msg' => $this->l->t('No image with the matching MIME type was found on the server.'), + ]; + return new JSONResponse($json, Http::STATUS_NOT_ACCEPTABLE); + } else { + // The client accepts the SVG file. Send it. + $file = file_get_contents(dirname(__FILE__) . '/../../../img/recipe.svg'); + + return new DataDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']); + } + } + } + + /** + * Trigger the import of a recipe. + * + * The URL is extracted from the request directly. + */ + public function import() { + $this->dbCacheService->triggerCheck(); + + $data = $this->restParser->getParameters(); + + if (!isset($data['url'])) { + return new JSONResponse('Field "url" is required', 400); + } + + try { + $recipe_file = $this->service->downloadRecipe($data['url']); + $recipe_json = $this->service->parseRecipeFile($recipe_file); + $this->dbCacheService->addRecipe($recipe_file); + + return new JSONResponse($recipe_json, Http::STATUS_OK); + } catch (RecipeExistsException $ex) { + $json = [ + 'msg' => $ex->getMessage(), + 'line' => $ex->getLine(), + 'file' => $ex->getFile(), + ]; + return new JSONResponse($json, Http::STATUS_CONFLICT); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 400); + } + } + + /** + * Search for a recipe + * + * @param string $query The query to search for + * @return JSONResponse + */ + public function search($query) { + $this->dbCacheService->triggerCheck(); + + $query = urldecode($query); + try { + $recipes = $this->service->findRecipesInSearchIndex($query); + + foreach ($recipes as $i => $recipe) { + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb', + 't' => $this->service->getRecipeMTime($recipe['recipe_id']) + ] + ); + $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb16' + ] + ); + } + + return new JSONResponse($recipes, 200, ['Content-Type' => 'application/json']); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 500); + } + } + + /** + * Get all recipes in a category + * + * @param string $category The category to filter the recipes by + * @return JSONResponse + */ + public function getAllInCategory($category) { + $this->dbCacheService->triggerCheck(); + + $category = urldecode($category); + try { + $recipes = $this->service->getRecipesByCategory($category); + foreach ($recipes as $i => $recipe) { + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb', + 't' => $this->service->getRecipeMTime($recipe['recipe_id']) + ] + ); + $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb16' + ] + ); + } + + return new JSONResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); + } catch (\Exception $e) { + return new JSONResponse($e->getMessage(), 500); + } + } + + + + /** + * Get all recipes with a tag associated + * + * The filtering is done such that a recipe is in the result if any keyword is attached. + * + * @param string $keywords The keywords to look for + * @return JSONResponse + */ + public function getAllWithTags($keywords) { + $this->dbCacheService->triggerCheck(); + $keywords = urldecode($keywords); + + try { + $recipes = $this->service->getRecipesByKeywords($keywords); + foreach ($recipes as $i => $recipe) { + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb', + 't' => $this->service->getRecipeMTime($recipe['recipe_id']) + ] + ); + $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb16' + ] + ); + } + + return new JSONResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); + } catch (\Exception $e) { + // error_log($e->getMessage()); + return new JSONResponse($e->getMessage(), 500); + } + } +} diff --git a/lib/Controller/KeywordApiController.php b/lib/Controller/KeywordApiController.php index 42de9aba9..b0f5523dd 100644 --- a/lib/Controller/KeywordApiController.php +++ b/lib/Controller/KeywordApiController.php @@ -2,6 +2,7 @@ namespace OCA\Cookbook\Controller; +use OCA\Cookbook\Controller\Implementation\KeywordImplementation; use OCA\Cookbook\Service\DbCacheService; use OCA\Cookbook\Service\RecipeService; use OCP\AppFramework\ApiController; @@ -9,33 +10,26 @@ use OCP\IRequest; class KeywordApiController extends ApiController { - protected $appName; - - /** @var RecipeService */ - private $service; - /** @var DbCacheService */ - private $dbCacheService; + /** @var KeywordImplementation */ + private $impl; public function __construct( string $AppName, IRequest $request, - RecipeService $recipeService, - DbCacheService $dbCacheService + KeywordImplementation $keywordImplementation ) { parent::__construct($AppName, $request); - $this->service = $recipeService; - $this->dbCacheService = $dbCacheService; + $this->impl = $keywordImplementation; } /** * @NoAdminRequired * @NoCSRFRequired * @CORS + * + * @return JSONResponse */ public function keywords() { - $this->dbCacheService->triggerCheck(); - - $keywords = $this->service->getAllKeywordsInSearchIndex(); - return new JSONResponse($keywords, 200); + return $this->impl->index(); } } diff --git a/lib/Controller/KeywordController.php b/lib/Controller/KeywordController.php new file mode 100644 index 000000000..e9f23fc9c --- /dev/null +++ b/lib/Controller/KeywordController.php @@ -0,0 +1,34 @@ +impl = $keywordImplementation; + } + /** + * @NoAdminRequired + * + * @return JSONResponse + */ + public function keywords() { + return $this->impl->index(); + } +} diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index e0ac1a6c3..c698e2e2d 100755 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -5,61 +5,33 @@ use OCP\IRequest; use OCP\IURLGenerator; use OCP\Util; -use OCP\AppFramework\Http; use OCP\AppFramework\Http\TemplateResponse; -use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Controller; use OCA\Cookbook\Service\RecipeService; use OCA\Cookbook\Service\DbCacheService; use OCA\Cookbook\Helper\RestParameterParser; use OCA\Cookbook\Exception\UserFolderNotWritableException; -use OCA\Cookbook\Exception\RecipeExistsException; use OCA\Cookbook\Exception\UserNotLoggedInException; use OCA\Cookbook\Helper\UserFolderHelper; -use OCP\AppFramework\Http\JSONResponse; class MainController extends Controller { + /** @var string */ protected $appName; - - /** - * @var RecipeService - */ - private $service; - /** - * @var DbCacheService - */ + /** @var DbCacheService */ private $dbCacheService; - /** - * @var IURLGenerator - */ - private $urlGenerator; - - /** - * @var RestParameterParser - */ - private $restParser; - - /** - * @var UserFolderHelper - */ + /** @var UserFolderHelper */ private $userFolder; public function __construct( string $AppName, IRequest $request, - RecipeService $recipeService, DbCacheService $dbCacheService, - IURLGenerator $urlGenerator, - RestParameterParser $restParser, UserFolderHelper $userFolder ) { parent::__construct($AppName, $request); - $this->service = $recipeService; - $this->urlGenerator = $urlGenerator; $this->appName = $AppName; $this->dbCacheService = $dbCacheService; - $this->restParser = $restParser; $this->userFolder = $userFolder; } @@ -89,251 +61,4 @@ public function index(): TemplateResponse { Util::addScript('cookbook', 'cookbook-main'); return new TemplateResponse($this->appName, 'index'); // templates/index.php } - - /** - * @NoAdminRequired - * @NoCSRFRequired - * @return DataResponse - */ - public function getApiVersion(): DataResponse { - $response = [ - 'cookbook_version' => [0, 9, 14], /* VERSION_TAG do not change this line manually */ - 'api_version' => [ - 'epoch' => 0, - 'major' => 0, - 'minor' => 4 - ] - ]; - return new DataResponse($response, 200, ['Content-Type' => 'application/json']); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function categories() { - $this->dbCacheService->triggerCheck(); - - $categories = $this->service->getAllCategoriesInSearchIndex(); - return new DataResponse($categories, 200, ['Content-Type' => 'application/json']); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function keywords() { - $this->dbCacheService->triggerCheck(); - - $keywords = $this->service->getAllKeywordsInSearchIndex(); - return new DataResponse($keywords, 200, ['Content-Type' => 'application/json']); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - * @param mixed $query - */ - public function search($query) { - $this->dbCacheService->triggerCheck(); - - $query = urldecode($query); - try { - $recipes = $this->service->findRecipesInSearchIndex($query); - - foreach ($recipes as $i => $recipe) { - $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb', - 't' => $this->service->getRecipeMTime($recipe['recipe_id']) - ] - ); - $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb16' - ] - ); - } - - return new DataResponse($recipes, 200, ['Content-Type' => 'application/json']); - } catch (\Exception $e) { - return new DataResponse($e->getMessage(), 500); - } - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - * @param mixed $category - */ - public function category($category) { - $this->dbCacheService->triggerCheck(); - - $category = urldecode($category); - try { - $recipes = $this->service->getRecipesByCategory($category); - foreach ($recipes as $i => $recipe) { - $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb', - 't' => $this->service->getRecipeMTime($recipe['recipe_id']) - ] - ); - $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb16' - ] - ); - } - - return new DataResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); - } catch (\Exception $e) { - return new DataResponse($e->getMessage(), 500); - } - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - * @param mixed $category - */ - public function categoryUpdate($category) { - $this->dbCacheService->triggerCheck(); - - $json = $this->restParser->getParameters(); - if (!$json || !isset($json['name']) || !$json['name']) { - return new DataResponse('New category name not found in data', 400); - } - - $category = urldecode($category); - try { - $recipes = $this->service->getRecipesByCategory($category); - foreach ($recipes as $recipe) { - $r = $this->service->getRecipeById($recipe['recipe_id']); - $r['recipeCategory'] = $json['name']; - $this->service->addRecipe($r); - } - // Update cache - $this->dbCacheService->updateCache(); - - return new DataResponse($json['name'], Http::STATUS_OK, ['Content-Type' => 'application/json']); - } catch (\Exception $e) { - return new DataResponse($e->getMessage(), 500); - } - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - * @param mixed $keywords - */ - public function tags($keywords) { - $this->dbCacheService->triggerCheck(); - $keywords = urldecode($keywords); - - try { - $recipes = $this->service->getRecipesByKeywords($keywords); - foreach ($recipes as $i => $recipe) { - $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb', - 't' => $this->service->getRecipeMTime($recipe['recipe_id']) - ] - ); - $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb16' - ] - ); - } - - return new DataResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); - } catch (\Exception $e) { - // error_log($e->getMessage()); - return new DataResponse($e->getMessage(), 500); - } - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function import() { - $this->dbCacheService->triggerCheck(); - - $data = $this->restParser->getParameters(); - - if (!isset($data['url'])) { - return new DataResponse('Field "url" is required', 400); - } - - try { - $recipe_file = $this->service->downloadRecipe($data['url']); - $recipe_json = $this->service->parseRecipeFile($recipe_file); - $this->dbCacheService->addRecipe($recipe_file); - - return new DataResponse($recipe_json, Http::STATUS_OK, ['Content-Type' => 'application/json']); - } catch (RecipeExistsException $ex) { - $json = [ - 'msg' => $ex->getMessage(), - 'line' => $ex->getLine(), - 'file' => $ex->getFile(), - ]; - return new JSONResponse($json, Http::STATUS_CONFLICT); - } catch (\Exception $e) { - return new DataResponse($e->getMessage(), 400); - } - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function new() { - $this->dbCacheService->triggerCheck(); - - try { - $recipe_data = $this->restParser->getParameters(); - $file = $this->service->addRecipe($recipe_data); - $this->dbCacheService->addRecipe($file); - - return new DataResponse($file->getParent()->getId()); - } catch (\Exception $e) { - return new DataResponse($e->getMessage(), 500); - } - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - * @param mixed $id - */ - public function update($id) { - $this->dbCacheService->triggerCheck(); - - try { - $recipe_data = $this->restParser->getParameters(); - - $recipe_data['id'] = $id; - - $file = $this->service->addRecipe($recipe_data); - $this->dbCacheService->addRecipe($file); - - return new DataResponse($id); - } catch (\Exception $e) { - return new DataResponse($e->getMessage(), 500); - } - } } diff --git a/lib/Controller/RecipeApiController.php b/lib/Controller/RecipeApiController.php index be9466a45..5f5e88d15 100644 --- a/lib/Controller/RecipeApiController.php +++ b/lib/Controller/RecipeApiController.php @@ -2,40 +2,25 @@ namespace OCA\Cookbook\Controller; +use OCA\Cookbook\Controller\Implementation\RecipeImplementation; use OCP\IRequest; -use OCP\AppFramework\Http; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\FileDisplayResponse; -use OCA\Cookbook\Exception\RecipeExistsException; -use OCA\Cookbook\Exception\NoRecipeNameGivenException; class RecipeApiController extends ApiController { - protected $appName; - - /** @var RecipeService */ - private $service; - /** @var DbCacheService */ - private $dbCacheService; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var RestParameterParser */ - private $restParser; - /** @var UserFolderHelper */ - private $userFolder; - /** @var RecipeJSONOutputFilter */ - private $outputFilter; - /** @var AcceptHeaderParsingHelper */ - private $acceptHeaderParser; - /** @var IL10N */ - private $l; + /** @var RecipeImplementation */ + private $impl; public function __construct( string $AppName, - IRequest $request + IRequest $request, + RecipeImplementation $recipeImplementation ) { parent::__construct($AppName, $request); + + $this->impl = $recipeImplementation; } /** @@ -44,18 +29,7 @@ public function __construct( * @CORS */ public function index() { - $this->dbCacheService->triggerCheck(); - - if (empty($_GET['keywords'])) { - $recipes = $this->service->getAllRecipesInSearchIndex(); - } else { - $recipes = $this->service->findRecipesInSearchIndex(isset($_GET['keywords']) ? $_GET['keywords'] : ''); - } - foreach ($recipes as $i => $recipe) { - $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb']); - $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb16']); - } - return new JSONResponse($recipes, Http::STATUS_OK); + return $this->impl->index(); } /** @@ -66,20 +40,7 @@ public function index() { * @return JSONResponse */ public function show($id) { - $this->dbCacheService->triggerCheck(); - - $json = $this->service->getRecipeById($id); - - if (null === $json) { - return new JSONResponse($id, Http::STATUS_NOT_FOUND); - } - - $json['printImage'] = $this->service->getPrintImage(); - $json['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $json['id'], 'size' => 'full']); - - $json = $this->outputFilter->filter($json); - - return new JSONResponse($json, Http::STATUS_OK); + return $this->impl->show($id); } /** @@ -93,22 +54,7 @@ public function show($id) { * @todo Parameter id is never used. Fix that */ public function update($id) { - $this->dbCacheService->triggerCheck(); - - $recipeData = $this->restParser->getParameters(); - try { - $file = $this->service->addRecipe($recipeData); - } catch (NoRecipeNameGivenException $ex) { - $json = [ - 'msg' => $ex->getMessage(), - 'file' => $ex->getFile(), - 'line' => $ex->getLine(), - ]; - return new JSONResponse($json, Http::STATUS_UNPROCESSABLE_ENTITY); - } - $this->dbCacheService->addRecipe($file); - - return new JSONResponse($file->getParent()->getId(), Http::STATUS_OK); + return $this->impl->update($id); } /** @@ -121,29 +67,7 @@ public function update($id) { * @return JSONResponse */ public function create() { - $this->dbCacheService->triggerCheck(); - - $recipeData = $this->restParser->getParameters(); - try { - $file = $this->service->addRecipe($recipeData); - $this->dbCacheService->addRecipe($file); - - return new JSONResponse($file->getParent()->getId(), Http::STATUS_OK); - } catch (RecipeExistsException $ex) { - $json = [ - 'msg' => $ex->getMessage(), - 'file' => $ex->getFile(), - 'line' => $ex->getLine(), - ]; - return new JSONResponse($json, Http::STATUS_CONFLICT); - } catch (NoRecipeNameGivenException $ex) { - $json = [ - 'msg' => $ex->getMessage(), - 'file' => $ex->getFile(), - 'line' => $ex->getLine(), - ]; - return new JSONResponse($json, Http::STATUS_UNPROCESSABLE_ENTITY); - } + return $this->impl->create(); } /** @@ -154,14 +78,7 @@ public function create() { * @return JSONResponse */ public function destroy($id) { - $this->dbCacheService->triggerCheck(); - - try { - $this->service->deleteRecipe($id); - return new JSONResponse('Recipe ' . $id . ' deleted successfully', Http::STATUS_OK); - } catch (\Exception $e) { - return new JSONResponse($e->getMessage(), 502); - } + return $this->impl->destroy($id); } /** @@ -172,31 +89,7 @@ public function destroy($id) { * @return JSONResponse|FileDisplayResponse|DataDisplayResponse */ public function image($id) { - $this->dbCacheService->triggerCheck(); - - $acceptHeader = $this->request->getHeader('Accept'); - $acceptedExtensions = $this->acceptHeaderParser->parseHeader($acceptHeader); - - $size = isset($_GET['size']) ? $_GET['size'] : null; - - try { - $file = $this->service->getRecipeImageFileByFolderId($id, $size); - - return new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/jpeg', 'Cache-Control' => 'public, max-age=604800']); - } catch (\Exception $e) { - if (array_search('svg', $acceptedExtensions, true) === false) { - // We may not serve a SVG image. Tell the client about the missing image. - $json = [ - 'msg' => $this->l->t('No image with the matching MIME type was found on the server.'), - ]; - return new JSONResponse($json, Http::STATUS_NOT_ACCEPTABLE); - } else { - // The client accepts the SVG file. Send it. - $file = file_get_contents(dirname(__FILE__) . '/../../img/recipe.svg'); - - return new DataDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']); - } - } + return $this->impl->image($id); } /** @@ -205,30 +98,7 @@ public function image($id) { * @CORS */ public function import() { - $this->dbCacheService->triggerCheck(); - - $data = $this->restParser->getParameters(); - - if (!isset($data['url'])) { - return new JSONResponse('Field "url" is required', 400); - } - - try { - $recipe_file = $this->service->downloadRecipe($data['url']); - $recipe_json = $this->service->parseRecipeFile($recipe_file); - $this->dbCacheService->addRecipe($recipe_file); - - return new JSONResponse($recipe_json, Http::STATUS_OK); - } catch (RecipeExistsException $ex) { - $json = [ - 'msg' => $ex->getMessage(), - 'line' => $ex->getLine(), - 'file' => $ex->getFile(), - ]; - return new JSONResponse($json, Http::STATUS_CONFLICT); - } catch (\Exception $e) { - return new JSONResponse($e->getMessage(), 400); - } + return $this->impl->import(); } /** @@ -239,34 +109,7 @@ public function import() { * @return JSONResponse */ public function search($query) { - $this->dbCacheService->triggerCheck(); - - $query = urldecode($query); - try { - $recipes = $this->service->findRecipesInSearchIndex($query); - - foreach ($recipes as $i => $recipe) { - $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb', - 't' => $this->service->getRecipeMTime($recipe['recipe_id']) - ] - ); - $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb16' - ] - ); - } - - return new JSONResponse($recipes, 200, ['Content-Type' => 'application/json']); - } catch (\Exception $e) { - return new JSONResponse($e->getMessage(), 500); - } + return $this->impl->search($query); } /** @@ -277,33 +120,7 @@ public function search($query) { * @return JSONResponse */ public function category($category) { - $this->dbCacheService->triggerCheck(); - - $category = urldecode($category); - try { - $recipes = $this->service->getRecipesByCategory($category); - foreach ($recipes as $i => $recipe) { - $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb', - 't' => $this->service->getRecipeMTime($recipe['recipe_id']) - ] - ); - $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb16' - ] - ); - } - - return new JSONResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); - } catch (\Exception $e) { - return new JSONResponse($e->getMessage(), 500); - } + return $this->impl->getAllInCategory($category); } @@ -316,33 +133,6 @@ public function category($category) { * @return JSONResponse */ public function tags($keywords) { - $this->dbCacheService->triggerCheck(); - $keywords = urldecode($keywords); - - try { - $recipes = $this->service->getRecipesByKeywords($keywords); - foreach ($recipes as $i => $recipe) { - $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb', - 't' => $this->service->getRecipeMTime($recipe['recipe_id']) - ] - ); - $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute( - 'cookbook.recipe.image', - [ - 'id' => $recipe['recipe_id'], - 'size' => 'thumb16' - ] - ); - } - - return new JSONResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); - } catch (\Exception $e) { - // error_log($e->getMessage()); - return new JSONResponse($e->getMessage(), 500); - } + return $this->impl->getAllWithTags($keywords); } } diff --git a/lib/Controller/RecipeController.php b/lib/Controller/RecipeController.php index e84ff964f..4b591e6de 100755 --- a/lib/Controller/RecipeController.php +++ b/lib/Controller/RecipeController.php @@ -2,235 +2,122 @@ namespace OCA\Cookbook\Controller; -use OCA\Cookbook\Exception\NoRecipeNameGivenException; +use OCA\Cookbook\Controller\Implementation\RecipeImplementation; use OCP\IRequest; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\DataDisplayResponse; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Controller; use OCA\Cookbook\Service\RecipeService; use OCP\IURLGenerator; use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Exception\RecipeExistsException; use OCA\Cookbook\Helper\AcceptHeaderParsingHelper; use OCA\Cookbook\Helper\Filter\RecipeJSONOutputFilter; use OCA\Cookbook\Helper\RestParameterParser; -use OCP\AppFramework\Http\JSONResponse; use OCP\IL10N; class RecipeController extends Controller { - /** - * @var RecipeService - */ - private $service; - /** - * @var IURLGenerator - */ - private $urlGenerator; - - /** - * @var DbCacheService - */ - private $dbCacheService; - - /** @var RecipeJSONOutputFilter */ - private $outputFilter; - - /** - * @var RestParameterParser - */ - private $restParser; - - /** - * @var AcceptHeaderParsingHelper - */ - private $acceptHeaderParser; - - /** - * @var IL10N - */ - private $l; + /** @var RecipeImplementation */ + private $impl; public function __construct( $AppName, IRequest $request, - IURLGenerator $urlGenerator, - RecipeService $recipeService, - DbCacheService $dbCacheService, - RecipeJSONOutputFilter $outputFilter, - RestParameterParser $restParser, - AcceptHeaderParsingHelper $acceptHeaderParser, - IL10N $l + RecipeImplementation $recipeImplementation ) { parent::__construct($AppName, $request); - $this->service = $recipeService; - $this->urlGenerator = $urlGenerator; - $this->dbCacheService = $dbCacheService; - $this->outputFilter = $outputFilter; - $this->restParser = $restParser; - $this->acceptHeaderParser = $acceptHeaderParser; - $this->l = $l; + $this->impl = $recipeImplementation; } /** * @NoAdminRequired - * @NoCSRFRequired */ public function index() { - $this->dbCacheService->triggerCheck(); - - if (empty($_GET['keywords'])) { - $recipes = $this->service->getAllRecipesInSearchIndex(); - } else { - $recipes = $this->service->findRecipesInSearchIndex(isset($_GET['keywords']) ? $_GET['keywords'] : ''); - } - foreach ($recipes as $i => $recipe) { - $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb']); - $recipes[$i]['imagePlaceholderUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb16']); - } - return new DataResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); + return $this->impl->index(); } /** * @NoAdminRequired - * @NoCSRFRequired * @param int $id - * @return DataResponse + * @return JSONResponse */ public function show($id) { - $this->dbCacheService->triggerCheck(); - - $json = $this->service->getRecipeById($id); - - if (null === $json) { - return new DataResponse($id, Http::STATUS_NOT_FOUND, ['Content-Type' => 'application/json']); - } - - $json['printImage'] = $this->service->getPrintImage(); - $json['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $json['id'], 'size' => 'full']); - - $json = $this->outputFilter->filter($json); - - return new DataResponse($json, Http::STATUS_OK, ['Content-Type' => 'application/json']); + return $this->impl->show($id); } /** * Update an existing recipe. * * @NoAdminRequired - * @NoCSRFRequired - * * @param $id - * - * @return DataResponse + * @return JSONResponse * @todo Parameter id is never used. Fix that */ public function update($id) { - $this->dbCacheService->triggerCheck(); - - $recipeData = $this->restParser->getParameters(); - try { - $file = $this->service->addRecipe($recipeData); - } catch (NoRecipeNameGivenException $ex) { - $json = [ - 'msg' => $ex->getMessage(), - 'file' => $ex->getFile(), - 'line' => $ex->getLine(), - ]; - return new JSONResponse($json, Http::STATUS_UNPROCESSABLE_ENTITY); - } - $this->dbCacheService->addRecipe($file); - - return new DataResponse($file->getParent()->getId(), Http::STATUS_OK, ['Content-Type' => 'application/json']); + return $this->impl->update($id); } /** * Create a new recipe. * * @NoAdminRequired - * @NoCSRFRequired * - * @param $id - * - * @return DataResponse + * @return JSONResponse */ public function create() { - $this->dbCacheService->triggerCheck(); - - $recipeData = $this->restParser->getParameters(); - try { - $file = $this->service->addRecipe($recipeData); - $this->dbCacheService->addRecipe($file); - - return new DataResponse($file->getParent()->getId(), Http::STATUS_OK, ['Content-Type' => 'application/json']); - } catch (RecipeExistsException $ex) { - $json = [ - 'msg' => $ex->getMessage(), - 'file' => $ex->getFile(), - 'line' => $ex->getLine(), - ]; - return new JSONResponse($json, Http::STATUS_CONFLICT); - } catch (NoRecipeNameGivenException $ex) { - $json = [ - 'msg' => $ex->getMessage(), - 'file' => $ex->getFile(), - 'line' => $ex->getLine(), - ]; - return new JSONResponse($json, Http::STATUS_UNPROCESSABLE_ENTITY); - } + return $this->impl->create(); } /** * @NoAdminRequired - * @NoCSRFRequired * @param int $id - * @return DataResponse + * @return JSONResponse */ public function destroy($id) { - $this->dbCacheService->triggerCheck(); - - try { - $this->service->deleteRecipe($id); - return new DataResponse('Recipe ' . $id . ' deleted successfully', Http::STATUS_OK); - } catch (\Exception $e) { - return new DataResponse($e->getMessage(), 502); - } + return $this->impl->destroy($id); } /** * @NoAdminRequired - * @NoCSRFRequired * @param $id - * @return DataResponse|FileDisplayResponse + * @return JSONResponse|FileDisplayResponse|DataDisplayResponse */ public function image($id) { - $this->dbCacheService->triggerCheck(); - - $acceptHeader = $this->request->getHeader('Accept'); - $acceptedExtensions = $this->acceptHeaderParser->parseHeader($acceptHeader); - - $size = isset($_GET['size']) ? $_GET['size'] : null; - - try { - $file = $this->service->getRecipeImageFileByFolderId($id, $size); - - return new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/jpeg', 'Cache-Control' => 'public, max-age=604800']); - } catch (\Exception $e) { - if (array_search('svg', $acceptedExtensions, true) === false) { - // We may not serve a SVG image. Tell the client about the missing image. - $json = [ - 'msg' => $this->l->t('No image with the matching MIME type was found on the server.'), - ]; - return new JSONResponse($json, Http::STATUS_NOT_ACCEPTABLE); - } else { - // The client accepts the SVG file. Send it. - $file = file_get_contents(dirname(__FILE__) . '/../../img/recipe.svg'); - - return new DataDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']); - } - } + return $this->impl->image($id); + } + + /** + * @NoAdminRequired + */ + public function import() { + return $this->impl->import(); + } + + /** + * @NoAdminRequired + * @param string $query + * @return JSONResponse + */ + public function search($query) { + return $this->impl->search($query); + } + + /** + * @NoAdminRequired + * @param string $category + * @return JSONResponse + */ + public function category($category) { + return $this->impl->getAllInCategory($category); + } + + + + /** + * @NoAdminRequired + * @param string $keywords + * @return JSONResponse + */ + public function tags($keywords) { + return $this->impl->getAllWithTags($keywords); } } diff --git a/lib/Controller/UtilApiController.php b/lib/Controller/UtilApiController.php index 6070ce1cd..a08b9f126 100644 --- a/lib/Controller/UtilApiController.php +++ b/lib/Controller/UtilApiController.php @@ -4,8 +4,14 @@ use OCP\AppFramework\ApiController; use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; class UtilApiController extends ApiController { + public function __construct($AppName, IRequest $request) + { + parent::__construct($AppName, $request); + } + /** * @NoAdminRequired * @NoCSRFRequired diff --git a/tests/Unit/Controller/AbstractControllerTestCase.php b/tests/Unit/Controller/AbstractControllerTestCase.php new file mode 100644 index 000000000..2ee49400c --- /dev/null +++ b/tests/Unit/Controller/AbstractControllerTestCase.php @@ -0,0 +1,67 @@ +getMethodsAndParameters(); + foreach($data as $row) { + $methodName = $row['name']; + + $implName = isset($row['implName']) ? $row['implName'] : $methodName; + + $once = isset($row['once']) ? $row['once'] : false; + + if (isset($row['args'])){ + foreach($row['args'] as $args) { + yield [$methodName, $args, $implName, $once]; + } + } else { + yield [$methodName, [], $implName, $once]; + } + } + } + + /** @dataProvider dpMethodNames */ + public function testMethod($methodName, $args, $implName, $once) { + $request = $this->createStub(IRequest::class); + $impl = $this->createMock($this->getImplementationClassName()); + + $cn = $this->getClassName(); + $class = new ReflectionClass($cn); + $dut = $class->newInstance('cookbook', $request, $impl); + + $expected = $this->createStub(JSONResponse::class); + + if($once) { + $impl->expects($this->once())->method($implName)->with(...$args)->willReturn($expected); + } else { + $impl->method($implName)->with(...$args)->willReturn($expected); + } + + $method = new ReflectionMethod($dut, $methodName); + $result = $method->invokeArgs($dut, $args); + + $this->assertSame($expected, $result); + } +} diff --git a/tests/Unit/Controller/CategoryApiControllerTest.php b/tests/Unit/Controller/CategoryApiControllerTest.php new file mode 100644 index 000000000..6484b61b3 --- /dev/null +++ b/tests/Unit/Controller/CategoryApiControllerTest.php @@ -0,0 +1,33 @@ + 'categories', 'implName' => 'index'], + ['name' => 'rename', 'args' => [['my category']], 'once' => true], + ]; + } +} diff --git a/tests/Unit/Controller/CategoryControllerTest.php b/tests/Unit/Controller/CategoryControllerTest.php new file mode 100644 index 000000000..9857e0b15 --- /dev/null +++ b/tests/Unit/Controller/CategoryControllerTest.php @@ -0,0 +1,33 @@ + 'categories', 'implName' => 'index'], + ['name' => 'rename', 'args' => [['my category']], 'once' => true], + ]; + } +} diff --git a/tests/Unit/Controller/ConfigApiControllerTest.php b/tests/Unit/Controller/ConfigApiControllerTest.php new file mode 100644 index 000000000..2e5a3b3bd --- /dev/null +++ b/tests/Unit/Controller/ConfigApiControllerTest.php @@ -0,0 +1,43 @@ + 'list'], + ['name' => 'reindex'], + ['name' => 'config', 'once' => true], + ]; + } +} diff --git a/tests/Unit/Controller/ConfigControllerTest.php b/tests/Unit/Controller/ConfigControllerTest.php index 992c7d795..e4bb9d61f 100644 --- a/tests/Unit/Controller/ConfigControllerTest.php +++ b/tests/Unit/Controller/ConfigControllerTest.php @@ -2,6 +2,10 @@ namespace OCA\Cookbook\tests\Unit\Controller; +require_once(__DIR__ . '/AbstractControllerTestCase.php'); + +namespace OCA\Cookbook\tests\Unit\Controller; + use OCP\IRequest; use PHPUnit\Framework\TestCase; use OCA\Cookbook\Service\RecipeService; @@ -9,174 +13,32 @@ use OCA\Cookbook\Helper\RestParameterParser; use PHPUnit\Framework\MockObject\MockObject; use OCA\Cookbook\Controller\ConfigController; +use OCA\Cookbook\Controller\Implementation\ConfigImplementation; use OCA\Cookbook\Helper\UserFolderHelper; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use ReflectionProperty; /** - * @coversDefaultClass OCA\Cookbook\Controller\ConfigController - * @covers :: - * @covers :: + * @covers OCA\Cookbook\Controller\ConfigController */ -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 UserFolderHelper|MockObject - */ - private $userFolder; - /** - * @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->userFolder = $this->createMock(UserFolderHelper::class); - - $this->sut = new ConfigController('cookbook', $this->request, $this->recipeService, $this->dbCacheService, $this->restParser, $this->userFolder); - } - - /** - * @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->userFolder->method('getPath')->willReturn($folder); - $this->dbCacheService->method('getSearchIndexUpdateInterval')->willReturn($interval); - $this->recipeService->method('getPrintImage')->willReturn($printImage); - - /** - * @var DataResponse $response - */ - $response = $this->sut->list(); +class ConfigControllerTest extends AbstractControllerTestCase { - $this->assertEquals(200, $response->getStatus()); - $this->assertEquals($expectedData, $response->getData()); + protected function getClassName(): string { + return ConfigController::class; } - /** - * @dataProvider dataProviderConfig - * @covers ::config - * @param mixed $data - * @param mixed $folderPath - * @param mixed $interval - * @param mixed $printImage - */ - 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->userFolder->expects($this->never())->method('setPath'); - $this->dbCacheService->expects($this->never())->method('updateCache'); - } else { - $this->userFolder->expects($this->once())->method('setPath')->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()); + protected function getImplementationClassName(): string { + return ConfigImplementation::class; } - public function dataProviderConfig() { + protected function getMethodsAndParameters(): array { 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 - ], + ['name' => 'list'], + ['name' => 'reindex'], + ['name' => 'config', 'once' => true], ]; } + } diff --git a/tests/Unit/Controller/Implementation/CategoryImplementationTest.php b/tests/Unit/Controller/Implementation/CategoryImplementationTest.php new file mode 100644 index 000000000..e70f1f8c4 --- /dev/null +++ b/tests/Unit/Controller/Implementation/CategoryImplementationTest.php @@ -0,0 +1,197 @@ +recipeService = $this->createMock(RecipeService::class); + $this->dbCacheService = $this->createMock(DbCacheService::class); + $this->restParser = $this->createMock(RestParameterParser::class); + + $this->sut = new CategoryImplementation( + $this->recipeService, + $this->dbCacheService, + $this->restParser + ); + } + + 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(CategoryImplementation::class, $name); + $property->setAccessible(true); + $this->assertSame($val, $property->getValue($this->sut)); + } + + private function ensureCacheCheckTriggered(): void { + $this->dbCacheService->expects($this->once())->method('triggerCheck'); + } + + public function testGetCategories(): void { + $this->ensureCacheCheckTriggered(); + + $cat = ['Foo', 'Bar', 'Baz']; + $this->recipeService->method('getAllCategoriesInSearchIndex')->willReturn($cat); + + $ret = $this->sut->index(); + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($cat, $ret->getData()); + } + + + + + /** + * @dataProvider dataProviderCategoryUpdateNoName + * @param mixed $requestParams + */ + public function testCategoryUpdateNoName($requestParams): void { + $this->ensureCacheCheckTriggered(); + + $this->restParser->expects($this->once())->method('getParameters')->willReturn($requestParams); + + $ret = $this->sut->rename(''); + + $this->assertEquals(400, $ret->getStatus()); + } + + public function dataProviderCategoryUpdateNoName() { + yield [[]]; + yield [[ + 'some', 'variable' + ]]; + yield [['name' => null]]; + yield [['name' => '']]; + } + + /** + * @dataProvider dpCategoryUpdate + * @todo No business logic in controller + * @param mixed $cat + * @param mixed $oldCat + * @param mixed $recipes + */ + 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->rename(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, + ], + ] + ], + ]; + } + + 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->rename(urlencode($oldCat)); + + $this->assertEquals(500, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()); + } +} diff --git a/tests/Unit/Controller/Implementation/ConfigImplementationTest.php b/tests/Unit/Controller/Implementation/ConfigImplementationTest.php new file mode 100644 index 000000000..c3ba2c763 --- /dev/null +++ b/tests/Unit/Controller/Implementation/ConfigImplementationTest.php @@ -0,0 +1,170 @@ +recipeService = $this->createMock(RecipeService::class); + $this->dbCacheService = $this->createMock(DbCacheService::class); + $this->restParser = $this->createMock(RestParameterParser::class); + $this->userFolder = $this->createMock(UserFolderHelper::class); + + $this->sut = new ConfigImplementation( + $this->recipeService, + $this->dbCacheService, + $this->restParser, + $this->userFolder + ); + } + + 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(ConfigImplementation::class, $name); + $property->setAccessible(true); + $this->assertSame($val, $property->getValue($this->sut)); + } + + public function testReindex(): void { + $this->dbCacheService->expects($this->once())->method('updateCache'); + + /** + * @var Response $response + */ + $response = $this->sut->reindex(); + + $this->assertEquals(200, $response->getStatus()); + } + + 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->userFolder->method('getPath')->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 + * @param mixed $data + * @param mixed $folderPath + * @param mixed $interval + * @param mixed $printImage + */ + 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->userFolder->expects($this->never())->method('setPath'); + $this->dbCacheService->expects($this->never())->method('updateCache'); + } else { + $this->userFolder->expects($this->once())->method('setPath')->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 JSONResponse $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/Implementation/KeywordImplementationTest.php b/tests/Unit/Controller/Implementation/KeywordImplementationTest.php new file mode 100644 index 000000000..cd5ab7b7c --- /dev/null +++ b/tests/Unit/Controller/Implementation/KeywordImplementationTest.php @@ -0,0 +1,59 @@ +recipeService = $this->createMock(RecipeService::class); + $this->dbCacheService = $this->createMock(DbCacheService::class); + + $this->sut = new KeywordImplementation( + $this->recipeService, + $this->dbCacheService + ); + } + + private function ensureCacheCheckTriggered(): void { + $this->dbCacheService->expects($this->once())->method('triggerCheck'); + } + + + + public function testGetKeywords(): void { + $this->ensureCacheCheckTriggered(); + + $kw = ['Foo', 'Bar', 'Baz']; + $this->recipeService->method('getAllKeywordsInSearchIndex')->willReturn($kw); + + $ret = $this->sut->index(); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($kw, $ret->getData()); + } + +} diff --git a/tests/Unit/Controller/Implementation/RecipeImplementationTest.php b/tests/Unit/Controller/Implementation/RecipeImplementationTest.php new file mode 100644 index 000000000..9311c2f4f --- /dev/null +++ b/tests/Unit/Controller/Implementation/RecipeImplementationTest.php @@ -0,0 +1,736 @@ +request = $this->createMock(IRequest::class); + $this->recipeService = $this->createMock(RecipeService::class); + $this->dbCacheService = $this->createMock(DbCacheService::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->restParser = $this->createMock(RestParameterParser::class); + $this->recipeFilter = $this->createMock(RecipeJSONOutputFilter::class); + $this->acceptHeaderParser = $this->createMock(AcceptHeaderParsingHelper::class); + + /** @var IL10N|Stub */ + $l = $this->createStub(IL10N::class); + $l->method('t')->willReturnArgument(0); + + $this->sut = new RecipeImplementation( + $this->request, + $this->recipeService, + $this->dbCacheService, + $this->urlGenerator, + $this->restParser, + $this->recipeFilter, + $this->acceptHeaderParser, + $l + ); + } + + // 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'); + } + + + + public function testImportFailed(): void { + $this->ensureCacheCheckTriggered(); + + $this->restParser->method('getParameters')->willReturn([]); + + /** + * @var JSONResponse $ret + */ + $ret = $this->sut->import(); + + $this->assertEquals(400, $ret->getStatus()); + } + + 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 JSONResponse $ret + */ + $ret = $this->sut->import(); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($json, $ret->getData()); + } + + 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()); + } + + 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()); + } + + /** + * @dataProvider dataProviderCategory + * @param mixed $cat + * @param mixed $recipes + */ + public function testCategory($cat, $recipes): void { + $this->ensureCacheCheckTriggered(); + + $this->recipeService->method('getRecipesByCategory')->with($cat)->willReturn($recipes); + + $expected = $this->getExpectedRecipes($recipes); + + /** + * @var JSONResponse $ret + */ + $ret = $this->sut->getAllInCategory(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, + ], + ] + ], + ]; + } + + 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 JSONResponse $ret + */ + $ret = $this->sut->getAllInCategory(urlencode($cat)); + + $this->assertEquals(500, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()); + } + + /** + * @dataProvider dataProviderTags + * @param mixed $keywords + * @param mixed $recipes + */ + public function testTags($keywords, $recipes): void { + $this->ensureCacheCheckTriggered(); + + $this->recipeService->method('getRecipesByKeywords')->with($keywords)->willReturn($recipes); + + $expected = $this->getExpectedRecipes($recipes); + + /** + * @var JSONResponse $ret + */ + $ret = $this->sut->getAllWithTags(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, + ], + ] + ], + ]; + } + + 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 JSONResponse $ret + */ + $ret = $this->sut->getAllWithTags(urlencode($keywords)); + + $this->assertEquals(500, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()); + } + + /** + * @dataProvider dpSearch + * @todo no implementation in controller + * @param mixed $query + * @param mixed $recipes + */ + public function testSearch($query, $recipes): void { + $this->ensureCacheCheckTriggered(); + + $this->recipeService->expects($this->once())->method('findRecipesInSearchIndex')->with($query)->willReturn($recipes); + + $expected = $this->getExpectedRecipes($recipes); + + /** + * @var JSONResponse $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, + ], + ], + ], + ]; + } + + 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 JSONResponse $res + */ + $res = $this->sut->search(urlencode($query)); + + $this->assertEquals(500, $res->getStatus()); + $this->assertEquals($errorMsg, $res->getData()); + } + + 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()); + } + + public function testUpdateNoName(): void { + $this->ensureCacheCheckTriggered(); + + $data = ['a', 'new', 'array']; + + $errorMsg = "No name was given for the recipe."; + $ex = new NoRecipeNameGivenException($errorMsg); + + $this->restParser->method('getParameters')->willReturn($data); + $this->recipeService->expects($this->once())->method('addRecipe')->with($data)->willThrowException($ex); + $this->dbCacheService->expects($this->never())->method('addRecipe'); + + $ret = $this->sut->update(1); + + $this->assertEquals(422, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()['msg']); + } + + 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()); + } + + public function testCreateNoName(): void { + $this->ensureCacheCheckTriggered(); + + $recipe = ['a', 'recipe', 'as', 'array']; + $this->restParser->method('getParameters')->willReturn($recipe); + + $errorMsg = "The error that was triggered"; + $ex = new NoRecipeNameGivenException($errorMsg); + + $this->recipeService->expects($this->once())->method('addRecipe')->with($recipe)->willThrowException($ex); + + $this->dbCacheService->expects($this->never())->method('addRecipe'); + + $ret = $this->sut->create(); + + $this->assertEquals(422, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()['msg']); + } + + 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()); + } + + 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; + + $this->recipeFilter->method('filter')->willReturnArgument(0); + + /** + * @var JSONResponse $ret + */ + $ret = $this->sut->show($id); + + $this->assertEquals(200, $ret->getStatus()); + $this->assertEquals($expected, $ret->getData()); + } + + public function testShowFailure(): void { + $this->ensureCacheCheckTriggered(); + + $id = 123; + $this->recipeService->method('getRecipeById')->with($id)->willReturn(null); + + /** + * @var JSONResponse $ret + */ + $ret = $this->sut->show($id); + + $this->assertEquals(404, $ret->getStatus()); + } + + public function testDestroy(): void { + $this->ensureCacheCheckTriggered(); + $id = 123; + + $this->recipeService->expects($this->once())->method('deleteRecipe')->with($id); + /** + * @var JSONResponse $ret + */ + $ret = $this->sut->destroy($id); + + $this->assertEquals(200, $ret->getStatus()); + } + + 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 JSONResponse $ret + */ + $ret = $this->sut->destroy($id); + + $this->assertEquals(502, $ret->getStatus()); + $this->assertEquals($errorMsg, $ret->getData()); + } + + /** + * @dataProvider dataProviderImage + * @todo Assert on image data/file name + * @todo Avoid business code in controller + * @param mixed $setSize + * @param mixed $size + */ + public function testImage($setSize, $size): void { + $this->ensureCacheCheckTriggered(); + + if ($setSize) { + $_GET['size'] = $size; + } + + /** @var File|Stub */ + $file = $this->createStub(File::class); + $id = 123; + $this->recipeService->method('getRecipeImageFileByFolderId')->with($id, $size)->willReturn($file); + + // Make the tests stable against PHP deprecation warnings + $file->method('getMTime')->willReturn(100); + $file->method('getName')->willReturn('image.jpg'); + + /** + * @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'], + ]; + } + + public function dpImageNotFound() { + yield [['jpg', 'png'], 406]; + yield [['jpg', 'png', 'svg'], 200]; + } + + /** + * @dataProvider dpImageNotFound + * @param mixed $accept + * @param mixed $expectedStatus + */ + public function testImageNotFound($accept, $expectedStatus) { + $id = 123; + + $ex = new Exception(); + $this->recipeService->method('getRecipeImageFileByFolderId')->willThrowException($ex); + + $headerContent = 'The content of the header as supposed by teh framework'; + $this->request->method('getHeader')->with('Accept')->willReturn($headerContent); + $this->acceptHeaderParser->method('parseHeader')->willReturnMap([ + [$headerContent, $accept], + ]); + + $ret = $this->sut->image($id); + + $this->assertEquals($expectedStatus, $ret->getStatus()); + } + + /** + * @dataProvider dataProviderIndex + * @todo no work on controller + * @param mixed $recipes + * @param mixed $setKeywords + * @param mixed $keywords + */ + 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 JSONResponse $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', + ], + ]; + } +} diff --git a/tests/Unit/Controller/KeywordApiControllerTest.php b/tests/Unit/Controller/KeywordApiControllerTest.php new file mode 100644 index 000000000..cac6b6b3d --- /dev/null +++ b/tests/Unit/Controller/KeywordApiControllerTest.php @@ -0,0 +1,35 @@ + 'keywords', 'implName' => 'index'], + ]; + } + +} diff --git a/tests/Unit/Controller/KeywordControllerTest.php b/tests/Unit/Controller/KeywordControllerTest.php new file mode 100644 index 000000000..ab8c499ca --- /dev/null +++ b/tests/Unit/Controller/KeywordControllerTest.php @@ -0,0 +1,34 @@ + 'keywords', 'implName' => 'index'], + ]; + } +} diff --git a/tests/Unit/Controller/MainControllerTest.php b/tests/Unit/Controller/MainControllerTest.php index f65521947..e841fcf99 100644 --- a/tests/Unit/Controller/MainControllerTest.php +++ b/tests/Unit/Controller/MainControllerTest.php @@ -18,28 +18,17 @@ use OCA\Cookbook\Exception\RecipeExistsException; use OCA\Cookbook\Exception\UserFolderNotWritableException; use OCA\Cookbook\Helper\UserFolderHelper; +use OCP\Files\Folder; /** * @covers \OCA\Cookbook\Controller\MainController * @covers \OCA\Cookbook\Exception\UserFolderNotWritableException */ 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 UserFolderHelper|MockObject */ @@ -53,27 +42,16 @@ class MainControllerTest extends TestCase { public function setUp(): void { parent::setUp(); - $this->recipeService = $this->createMock(RecipeService::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); + $request = $this->createStub(IRequest::class); $this->dbCacheService = $this->createMock(DbCacheService::class); - $this->restParser = $this->createMock(RestParameterParser::class); $this->userFolder = $this->createMock(UserFolderHelper::class); - $request = $this->createStub(IRequest::class); - - $this->sut = new MainController('cookbook', $request, $this->recipeService, $this->dbCacheService, $this->urlGenerator, $this->restParser, $this->userFolder); - } - - 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)); + $this->sut = new MainController( + 'cookbook', + $request, + $this->dbCacheService, + $this->userFolder + ); } private function ensureCacheCheckTriggered(): void { @@ -82,7 +60,11 @@ private function ensureCacheCheckTriggered(): void { public function testIndex(): void { $this->ensureCacheCheckTriggered(); + $userFolder = $this->createStub(Folder::class); + $this->userFolder->method('getFolder')->willReturn($userFolder); + $ret = $this->sut->index(); + $this->assertEquals(200, $ret->getStatus()); $this->assertEquals('index', $ret->getTemplateName()); } @@ -94,549 +76,6 @@ public function testIndexInvalidUser(): void { $this->assertEquals('invalid_guest', $ret->getTemplateName()); } - public function testGetAPIVersion(): void { - $ret = $this->sut->getApiVersion(); - - $this->assertEquals(200, $ret->getStatus()); - - $retData = $ret->getData(); - $this->assertTrue(isset($retData['cookbook_version'])); - $this->assertTrue(count($retData['cookbook_version']) === 3 || count($retData['cookbook_version']) === 4); - $this->assertTrue(is_int($retData['cookbook_version'][0])); - $this->assertTrue(is_int($retData['cookbook_version'][1])); - $this->assertTrue(is_int($retData['cookbook_version'][2])); - - $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'])); - } - - 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()); - } - - 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()); - } - - /** - * @dataProvider dataProviderNew - * @param mixed $data - * @param mixed $id - */ - 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 - ], - ]; - } - - /** - * @dataProvider dataProviderNew - * @param mixed $data - * @param mixed $id - */ - 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()); - } - - /** - * @dataProvider dataProviderUpdate - * @param mixed $data - * @param mixed $id - */ - 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 - ], - ]; - } - - /** - * @dataProvider dataProviderUpdate - * @param mixed $data - * @param mixed $id - */ - 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()); - } - - public function testImportFailed(): void { - $this->ensureCacheCheckTriggered(); - - $this->restParser->method('getParameters')->willReturn([]); - - /** - * @var DataResponse $ret - */ - $ret = $this->sut->import(); - - $this->assertEquals(400, $ret->getStatus()); - } - - 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()); - } - - 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()); - } - - 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()); - } - - /** - * @dataProvider dataProviderCategory - * @param mixed $cat - * @param mixed $recipes - */ - 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, - ], - ] - ], - ]; - } - - 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()); - } - - /** - * @dataProvider dataProviderTags - * @param mixed $keywords - * @param mixed $recipes - */ - 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, - ], - ] - ], - ]; - } - - 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()); - } - - /** - * @dataProvider dpSearch - * @todo no implementation in controller - * @param mixed $query - * @param mixed $recipes - */ - 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, - ], - ], - ], - ]; - } - 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()); - } - - /** - * @dataProvider dataProviderCategoryUpdateNoName - * @param mixed $requestParams - */ - 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' => '']]; - } - - /** - * @dataProvider dpCategoryUpdate - * @todo No business logic in controller - * @param mixed $cat - * @param mixed $oldCat - * @param mixed $recipes - */ - 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, - ], - ] - ], - ]; - } - - 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/RecipeApiControllerTest.php b/tests/Unit/Controller/RecipeApiControllerTest.php new file mode 100644 index 000000000..4e1a88dbf --- /dev/null +++ b/tests/Unit/Controller/RecipeApiControllerTest.php @@ -0,0 +1,63 @@ + 'index'], + ['name' => 'show', 'args' => [[123], ['123']]], + ['name' => 'update', 'args' => [[123], ['123']]], + ['name' => 'create'], + ['name' => 'destroy', 'args' => [[123], ['123']]], + ['name' => 'image', 'args' => [[123], ['123']]], + ['name' => 'import'], + ['name' => 'search', 'args' => [['The search']]], + ['name' => 'category', 'args' => [['The category']], 'implName' => 'getAllInCategory'], + ['name' => 'tags', 'args' => [['one keyword'], ['one keyword,another one']], 'implName' => 'getAllWithTags'], + ]; + } + + +} diff --git a/tests/Unit/Controller/RecipeControllerTest.php b/tests/Unit/Controller/RecipeControllerTest.php index c45dfd793..d36161e16 100644 --- a/tests/Unit/Controller/RecipeControllerTest.php +++ b/tests/Unit/Controller/RecipeControllerTest.php @@ -2,7 +2,12 @@ namespace OCA\Cookbook\tests\Unit\Controller; +require_once(__DIR__ . '/AbstractControllerTestCase.php'); + +namespace OCA\Cookbook\tests\Unit\Controller; + use Exception; +use OCA\Cookbook\Controller\Implementation\RecipeImplementation; use OCP\IRequest; use OCP\Files\File; use OCP\IURLGenerator; @@ -28,417 +33,30 @@ * @covers \OCA\Cookbook\Controller\RecipeController * @covers \OCA\Cookbook\Exception\NoRecipeNameGivenException */ -class RecipeControllerTest extends TestCase { - /** - * @var RecipeService|MockObject - */ - private $recipeService; - /** - * @var IURLGenerator|MockObject - */ - private $urlGenerator; - /** - * @var DbCacheService|MockObject - */ - private $dbCacheService; - - /** @var RecipeJSONOutputFilter|Stub */ - private $recipeJSONOutputFilter; - /** - * @var RestParameterParser|MockObject - */ - private $restParser; - - /** - * @var RecipeController - */ - private $sut; - - /** - * @var IRequest|MockObject - */ - private $request; - - /** - * @var AcceptHeaderParsingHelper|Stub - */ - private $acceptHeaderParser; +class RecipeControllerTest extends AbstractControllerTestCase { - 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->recipeJSONOutputFilter = $this->createStub(RecipeJSONOutputFilter::class); - $this->restParser = $this->createMock(RestParameterParser::class); - $this->request = $this->createMock(IRequest::class); - $this->acceptHeaderParser = $this->createStub(AcceptHeaderParsingHelper::class); - - $this->recipeJSONOutputFilter->method('filter')->willReturnArgument(0); - - /** - * @var Stub|IL10N $l - */ - $l = $this->createStub(IL10N::class); - $l->method('t')->willReturnArgument(0); - - $this->sut = new RecipeController('cookbook', $this->request, $this->urlGenerator, $this->recipeService, $this->dbCacheService, $this->recipeJSONOutputFilter, $this->restParser, $this->acceptHeaderParser, $l); + protected function getClassName(): string { + return RecipeController::class; } - public function testConstructor(): void { - $this->ensurePropertyIsCorrect('urlGenerator', $this->urlGenerator); - $this->ensurePropertyIsCorrect('service', $this->recipeService); - $this->ensurePropertyIsCorrect('dbCacheService', $this->dbCacheService); - $this->ensurePropertyIsCorrect('restParser', $this->restParser); + protected function getImplementationClassName(): string { + return RecipeImplementation::class; } - 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'); - } - - 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()); - } - - public function testUpdateNoName(): void { - $this->ensureCacheCheckTriggered(); - - $data = ['a', 'new', 'array']; - - $errorMsg = "No name was given for the recipe."; - $ex = new NoRecipeNameGivenException($errorMsg); - - $this->restParser->method('getParameters')->willReturn($data); - $this->recipeService->expects($this->once())->method('addRecipe')->with($data)->willThrowException($ex); - $this->dbCacheService->expects($this->never())->method('addRecipe'); - - $ret = $this->sut->update(1); - - $this->assertEquals(422, $ret->getStatus()); - $this->assertEquals($errorMsg, $ret->getData()['msg']); - } - - 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()); - } - - public function testCreateNoName(): void { - $this->ensureCacheCheckTriggered(); - - $recipe = ['a', 'recipe', 'as', 'array']; - $this->restParser->method('getParameters')->willReturn($recipe); - - $errorMsg = "The error that was triggered"; - $ex = new NoRecipeNameGivenException($errorMsg); - - $this->recipeService->expects($this->once())->method('addRecipe')->with($recipe)->willThrowException($ex); - - $this->dbCacheService->expects($this->never())->method('addRecipe'); - - $ret = $this->sut->create(); - - $this->assertEquals(422, $ret->getStatus()); - $this->assertEquals($errorMsg, $ret->getData()['msg']); - } - - 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()); - } - - 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()); - } - - 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()); - } - - 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()); - } - - 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()); - } - - /** - * @dataProvider dataProviderImage - * @todo Assert on image data/file name - * @todo Avoid business code in controller - * @param mixed $setSize - * @param mixed $size - */ - public function testImage($setSize, $size): void { - $this->ensureCacheCheckTriggered(); - - if ($setSize) { - $_GET['size'] = $size; - } - - /** @var File|Stub */ - $file = $this->createStub(File::class); - $id = 123; - $this->recipeService->method('getRecipeImageFileByFolderId')->with($id, $size)->willReturn($file); - - // Make teh tests stable against PHP deprecation warnings - $file->method('getMTime')->willReturn(100); - $file->method('getName')->willReturn('image.jpg'); - - /** - * @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 { + protected function getMethodsAndParameters(): array { return [ - [false, null], - [true, null], - [true, 'full'], + ['name' => 'index'], + ['name' => 'show', 'args' => [[123], ['123']]], + ['name' => 'update', 'args' => [[123], ['123']]], + ['name' => 'create'], + ['name' => 'destroy', 'args' => [[123], ['123']]], + ['name' => 'image', 'args' => [[123], ['123']]], + ['name' => 'import'], + ['name' => 'search', 'args' => [['The search']]], + ['name' => 'category', 'args' => [['The category']], 'implName' => 'getAllInCategory'], + ['name' => 'tags', 'args' => [['one keyword'], ['one keyword,another one']], 'implName' => 'getAllWithTags'], ]; } - public function dpImageNotFound() { - yield [['jpg', 'png'], 406]; - yield [['jpg', 'png', 'svg'], 200]; - } - - /** - * @dataProvider dpImageNotFound - * @param mixed $accept - * @param mixed $expectedStatus - */ - public function testImageNotFound($accept, $expectedStatus) { - $id = 123; - - $ex = new Exception(); - $this->recipeService->method('getRecipeImageFileByFolderId')->willThrowException($ex); - - $headerContent = 'The content of the header as supposed by teh framework'; - $this->request->method('getHeader')->with('Accept')->willReturn($headerContent); - $this->acceptHeaderParser->method('parseHeader')->willReturnMap([ - [$headerContent, $accept], - ]); - - $ret = $this->sut->image($id); - - $this->assertEquals($expectedStatus, $ret->getStatus()); - } - - /** - * @dataProvider dataProviderIndex - * @todo no work on controller - * @param mixed $recipes - * @param mixed $setKeywords - * @param mixed $keywords - */ - 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', - ], - ]; - } + } diff --git a/tests/Unit/Controller/UtilApiControllerTest.php b/tests/Unit/Controller/UtilApiControllerTest.php new file mode 100644 index 000000000..d01983e30 --- /dev/null +++ b/tests/Unit/Controller/UtilApiControllerTest.php @@ -0,0 +1,69 @@ +createStub(IRequest::class); + + $this->sut = new UtilApiController( + 'cookbook', + $request + ); + } + + public function testGetAPIVersion(): void { + $ret = $this->sut->getApiVersion(); + + $this->assertEquals(200, $ret->getStatus()); + + $retData = $ret->getData(); + $this->assertArrayHasKey('cookbook_version', $retData); + $this->assertGreaterThanOrEqual(3, count($retData['cookbook_version'])); + $this->assertLessThanOrEqual(4, count($retData['cookbook_version'])); + $this->assertIsInt($retData['cookbook_version'][0]); + $this->assertIsInt($retData['cookbook_version'][1]); + $this->assertIsInt($retData['cookbook_version'][2]); + if(count($retData['cookbook_version']) === 4) { + $this->assertIsString($retData['cookbook_version'][3]); + } + + $this->assertArrayHasKey('api_version', $retData); + $this->assertArrayHasKey('epoch', $retData['api_version']); + $this->assertArrayHasKey('major', $retData['api_version']); + $this->assertArrayHasKey('minor', $retData['api_version']); + } + + + +} From edd80fdd2e29307250f0ce5341f800ef358101ec Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 2 Sep 2022 12:03:43 +0200 Subject: [PATCH 3/7] Iron out some wrinkels Signed-off-by: Christian Wolf --- appinfo/routes.php | 14 +++++------ lib/Controller/CategoryApiController.php | 11 ++++----- lib/Controller/CategoryController.php | 12 ++++------ lib/Controller/ConfigApiController.php | 7 +++--- lib/Controller/ConfigController.php | 6 ++--- .../Implementation/CategoryImplementation.php | 4 ++-- .../Implementation/ConfigImplementation.php | 13 ++++------ .../Implementation/KeywordImplementation.php | 2 +- .../Implementation/RecipeImplementation.php | 16 ++++++------- lib/Controller/KeywordApiController.php | 4 +--- lib/Controller/KeywordController.php | 5 +--- lib/Controller/MainController.php | 3 --- lib/Controller/RecipeController.php | 8 ------- lib/Controller/UtilApiController.php | 5 ++-- .../Controller/AbstractControllerTestCase.php | 23 ++++++++---------- .../Controller/CategoryApiControllerTest.php | 3 --- .../Controller/CategoryControllerTest.php | 3 --- .../Controller/ConfigApiControllerTest.php | 12 ---------- .../Unit/Controller/ConfigControllerTest.php | 13 ---------- .../CategoryImplementationTest.php | 6 ----- .../ConfigImplementationTest.php | 1 - .../KeywordImplementationTest.php | 1 - .../RecipeImplementationTest.php | 20 ++-------------- .../Controller/KeywordApiControllerTest.php | 6 ----- .../Unit/Controller/KeywordControllerTest.php | 5 ---- tests/Unit/Controller/MainControllerTest.php | 12 ---------- .../Controller/RecipeApiControllerTest.php | 24 ------------------- .../Unit/Controller/RecipeControllerTest.php | 23 ------------------ .../Unit/Controller/UtilApiControllerTest.php | 20 +--------------- .../Unit/Service/HtmlDownloadServiceTest.php | 2 ++ 30 files changed, 56 insertions(+), 228 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 2546c21d6..fd29166f1 100755 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -14,7 +14,7 @@ * If you add new features here, increase the minor version of the API. * If you change the behavior or remove functionality, increase the major version there. */ - + // The static HTML template ['name' => 'main#index', 'url' => '/', 'verb' => 'GET'], @@ -24,12 +24,12 @@ ['name' => 'recipe#category', 'url' => '/webapp/category/{category}', 'verb' => 'GET'], ['name' => 'recipe#tags', 'url' => '/webapp/tags/{keywords}', 'verb' => 'GET'], ['name' => 'recipe#search', 'url' => '/webapp/search/{query}', 'verb' => 'GET'], - + ['name' => 'keyword#keywords', 'url' => '/webapp/keywords', 'verb' => 'GET'], - + ['name' => 'category#categories', 'url' => '/webapp/categories', 'verb' => 'GET'], ['name' => 'category#rename', 'url' => '/webapp/category/{category}', 'verb' => 'PUT'], - + ['name' => 'config#list', 'url' => '/webapp/config', 'verb' => 'GET'], ['name' => 'config#config', 'url' => '/webapp/config', 'verb' => 'POST'], ['name' => 'config#reindex', 'url' => '/webapp/reindex', 'verb' => 'POST'], @@ -45,12 +45,12 @@ ['name' => 'recipe_api#category', 'url' => '/api/v1/category/{category}', 'verb' => 'GET'], ['name' => 'recipe_api#tags', 'url' => '/api/v1/tags/{keywords}', 'verb' => 'GET'], ['name' => 'recipe_api#search', 'url' => '/api/v1/search/{query}', 'verb' => 'GET'], - + ['name' => 'keyword_api#keywords', 'url' => '/api/v1/keywords', 'verb' => 'GET'], - + ['name' => 'category_api#categories', 'url' => '/api/v1/categories', 'verb' => 'GET'], ['name' => 'category_api#rename', 'url' => '/api/v1/category/{category}', 'verb' => 'PUT'], - + ['name' => 'config_api#list', 'url' => '/api/v1/config', 'verb' => 'GET'], ['name' => 'config_api#config', 'url' => '/api/v1/config', 'verb' => 'POST'], ['name' => 'config_api#reindex', 'url' => '/api/v1/reindex', 'verb' => 'POST'], diff --git a/lib/Controller/CategoryApiController.php b/lib/Controller/CategoryApiController.php index b72e1c35e..677cad736 100644 --- a/lib/Controller/CategoryApiController.php +++ b/lib/Controller/CategoryApiController.php @@ -7,8 +7,7 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; -class CategoryApiController extends ApiController -{ +class CategoryApiController extends ApiController { /** @var CategoryImplementation */ private $impl; @@ -26,11 +25,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @CORS - * + * * @return JSONResponse */ - public function categories() - { + public function categories() { return $this->impl->index(); } @@ -41,8 +39,7 @@ public function categories() * @param string $category * @return JSONResponse */ - public function rename($category) - { + public function rename($category) { return $this->impl->rename($category); } } diff --git a/lib/Controller/CategoryController.php b/lib/Controller/CategoryController.php index a98565499..da5234329 100644 --- a/lib/Controller/CategoryController.php +++ b/lib/Controller/CategoryController.php @@ -4,12 +4,10 @@ use OCP\IRequest; use OCP\AppFramework\Controller; -use OCP\AppFramework\ApiController; use OCP\AppFramework\Http\JSONResponse; use OCA\Cookbook\Controller\Implementation\CategoryImplementation; -class CategoryController extends Controller -{ +class CategoryController extends Controller { /** @var CategoryImplementation */ private $impl; @@ -25,11 +23,10 @@ public function __construct( /** * @NoAdminRequired - * + * * @return JSONResponse */ - public function categories() - { + public function categories() { return $this->impl->index(); } @@ -38,8 +35,7 @@ public function categories() * @param string $category * @return JSONResponse */ - public function rename($category) - { + public function rename($category) { return $this->impl->rename($category); } } diff --git a/lib/Controller/ConfigApiController.php b/lib/Controller/ConfigApiController.php index 53e03e549..1f0711387 100644 --- a/lib/Controller/ConfigApiController.php +++ b/lib/Controller/ConfigApiController.php @@ -8,7 +8,6 @@ use OCP\AppFramework\Http\JSONResponse; class ConfigApiController extends ApiController { - /** @var ConfigImplementation */ private $implementation; @@ -26,7 +25,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @CORS - * + * * @return JSONResponse */ public function list() { @@ -37,7 +36,7 @@ public function list() { * @NoAdminRequired * @NoCSRFRequired * @CORS - * + * * @return JSONResponse */ public function config() { @@ -48,7 +47,7 @@ public function config() { * @NoAdminRequired * @NoCSRFRequired * @CORS - * + * * @return JSONResponse */ public function reindex() { diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index 178573f16..6e8ef7693 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -23,7 +23,7 @@ public function __construct( /** * @NoAdminRequired - * + * * @return JSONResponse */ public function list() { @@ -32,7 +32,7 @@ public function list() { /** * @NoAdminRequired - * + * * @return JSONResponse */ public function config() { @@ -41,7 +41,7 @@ public function config() { /** * @NoAdminRequired - * + * * @return JSONResponse */ public function reindex() { diff --git a/lib/Controller/Implementation/CategoryImplementation.php b/lib/Controller/Implementation/CategoryImplementation.php index ac48fb0c0..9ed7c72de 100644 --- a/lib/Controller/Implementation/CategoryImplementation.php +++ b/lib/Controller/Implementation/CategoryImplementation.php @@ -28,7 +28,7 @@ public function __construct( /** * List all available categories. - * + * * @return JSONResponse */ public function index() { @@ -40,7 +40,7 @@ public function index() { /** * Rename a category. - * + * * @param string $category * @return JSONResponse */ diff --git a/lib/Controller/Implementation/ConfigImplementation.php b/lib/Controller/Implementation/ConfigImplementation.php index 0ebe91c63..25dbb1cf1 100644 --- a/lib/Controller/Implementation/ConfigImplementation.php +++ b/lib/Controller/Implementation/ConfigImplementation.php @@ -2,7 +2,6 @@ namespace OCA\Cookbook\Controller\Implementation; -use OCP\IRequest; use OCP\AppFramework\Http; use OCA\Cookbook\Service\RecipeService; use OCP\AppFramework\Http\JSONResponse; @@ -11,7 +10,6 @@ use OCA\Cookbook\Helper\RestParameterParser; class ConfigImplementation { - /** @var RecipeService */ private $service; /** @var DbCacheService */ @@ -35,7 +33,7 @@ public function __construct( /** * Get the current configuration of the app - * + * * @return JSONResponse */ public function list() { @@ -50,12 +48,12 @@ public function list() { /** * Store the configuration in the database. - * + * * The value to be stored is extracted from the request directly. - * + * * Note that only those values are stored, that are present in the parameter. * All other configurations are not altered. - * + * * @return JSONResponse */ public function config() { @@ -81,7 +79,7 @@ public function config() { /** * Trigger a reindex/rescan of the current recipe folder. - * + * * @return JSONResponse */ public function reindex() { @@ -89,5 +87,4 @@ public function reindex() { return new JSONResponse('Search index rebuilt successfully', Http::STATUS_OK); } - } diff --git a/lib/Controller/Implementation/KeywordImplementation.php b/lib/Controller/Implementation/KeywordImplementation.php index c67b0bd09..35254e07f 100644 --- a/lib/Controller/Implementation/KeywordImplementation.php +++ b/lib/Controller/Implementation/KeywordImplementation.php @@ -21,7 +21,7 @@ public function __construct( } /** * List all available keywords. - * + * * @return JSONResponse */ public function index() { diff --git a/lib/Controller/Implementation/RecipeImplementation.php b/lib/Controller/Implementation/RecipeImplementation.php index 6170bfa96..19068bcc6 100644 --- a/lib/Controller/Implementation/RecipeImplementation.php +++ b/lib/Controller/Implementation/RecipeImplementation.php @@ -75,7 +75,7 @@ public function index() { /** * Fetch a single recipe - * + * * @param int $id * @return JSONResponse */ @@ -155,7 +155,7 @@ public function create() { /** * Remove a recipe - * + * * @param int $id The ifd of the recipe in question * @return JSONResponse */ @@ -172,7 +172,7 @@ public function destroy($id) { /** * Get the image associated with a recipe - * + * * @param $id The id of the recipe * @return JSONResponse|FileDisplayResponse|DataDisplayResponse */ @@ -206,7 +206,7 @@ public function image($id) { /** * Trigger the import of a recipe. - * + * * The URL is extracted from the request directly. */ public function import() { @@ -238,7 +238,7 @@ public function import() { /** * Search for a recipe - * + * * @param string $query The query to search for * @return JSONResponse */ @@ -275,7 +275,7 @@ public function search($query) { /** * Get all recipes in a category - * + * * @param string $category The category to filter the recipes by * @return JSONResponse */ @@ -313,9 +313,9 @@ public function getAllInCategory($category) { /** * Get all recipes with a tag associated - * + * * The filtering is done such that a recipe is in the result if any keyword is attached. - * + * * @param string $keywords The keywords to look for * @return JSONResponse */ diff --git a/lib/Controller/KeywordApiController.php b/lib/Controller/KeywordApiController.php index b0f5523dd..f410f440d 100644 --- a/lib/Controller/KeywordApiController.php +++ b/lib/Controller/KeywordApiController.php @@ -3,8 +3,6 @@ namespace OCA\Cookbook\Controller; use OCA\Cookbook\Controller\Implementation\KeywordImplementation; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Service\RecipeService; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; @@ -26,7 +24,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @CORS - * + * * @return JSONResponse */ public function keywords() { diff --git a/lib/Controller/KeywordController.php b/lib/Controller/KeywordController.php index e9f23fc9c..a2c1815dc 100644 --- a/lib/Controller/KeywordController.php +++ b/lib/Controller/KeywordController.php @@ -4,10 +4,7 @@ use OCP\IRequest; use OCP\AppFramework\Controller; -use OCP\AppFramework\ApiController; -use OCA\Cookbook\Service\RecipeService; use OCP\AppFramework\Http\JSONResponse; -use OCA\Cookbook\Service\DbCacheService; use OCA\Cookbook\Controller\Implementation\KeywordImplementation; class KeywordController extends Controller { @@ -25,7 +22,7 @@ public function __construct( } /** * @NoAdminRequired - * + * * @return JSONResponse */ public function keywords() { diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index c698e2e2d..2cd5d147f 100755 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -3,13 +3,10 @@ namespace OCA\Cookbook\Controller; use OCP\IRequest; -use OCP\IURLGenerator; use OCP\Util; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Controller; -use OCA\Cookbook\Service\RecipeService; use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Helper\RestParameterParser; use OCA\Cookbook\Exception\UserFolderNotWritableException; use OCA\Cookbook\Exception\UserNotLoggedInException; use OCA\Cookbook\Helper\UserFolderHelper; diff --git a/lib/Controller/RecipeController.php b/lib/Controller/RecipeController.php index 4b591e6de..46034ac94 100755 --- a/lib/Controller/RecipeController.php +++ b/lib/Controller/RecipeController.php @@ -6,14 +6,6 @@ use OCP\IRequest; use OCP\AppFramework\Controller; -use OCA\Cookbook\Service\RecipeService; -use OCP\IURLGenerator; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Helper\AcceptHeaderParsingHelper; -use OCA\Cookbook\Helper\Filter\RecipeJSONOutputFilter; -use OCA\Cookbook\Helper\RestParameterParser; -use OCP\IL10N; - class RecipeController extends Controller { /** @var RecipeImplementation */ private $impl; diff --git a/lib/Controller/UtilApiController.php b/lib/Controller/UtilApiController.php index a08b9f126..a7756f190 100644 --- a/lib/Controller/UtilApiController.php +++ b/lib/Controller/UtilApiController.php @@ -7,11 +7,10 @@ use OCP\IRequest; class UtilApiController extends ApiController { - public function __construct($AppName, IRequest $request) - { + public function __construct($AppName, IRequest $request) { parent::__construct($AppName, $request); } - + /** * @NoAdminRequired * @NoCSRFRequired diff --git a/tests/Unit/Controller/AbstractControllerTestCase.php b/tests/Unit/Controller/AbstractControllerTestCase.php index 2ee49400c..f47fb5718 100644 --- a/tests/Unit/Controller/AbstractControllerTestCase.php +++ b/tests/Unit/Controller/AbstractControllerTestCase.php @@ -9,31 +9,28 @@ use ReflectionMethod; abstract class AbstractControllerTestCase extends TestCase { - protected abstract function getClassName(): string; - protected abstract function getImplementationClassName(): string; - protected abstract function getMethodsAndParameters(): array; + abstract protected function getClassName(): string; + abstract protected function getImplementationClassName(): string; + abstract protected function getMethodsAndParameters(): array; protected $dut; protected $impl; - protected function setUp(): void - { + protected function setUp(): void { parent::setUp(); - - } public function dpMethodNames() { $data = $this->getMethodsAndParameters(); - foreach($data as $row) { + foreach ($data as $row) { $methodName = $row['name']; - + $implName = isset($row['implName']) ? $row['implName'] : $methodName; $once = isset($row['once']) ? $row['once'] : false; - - if (isset($row['args'])){ - foreach($row['args'] as $args) { + + if (isset($row['args'])) { + foreach ($row['args'] as $args) { yield [$methodName, $args, $implName, $once]; } } else { @@ -53,7 +50,7 @@ public function testMethod($methodName, $args, $implName, $once) { $expected = $this->createStub(JSONResponse::class); - if($once) { + if ($once) { $impl->expects($this->once())->method($implName)->with(...$args)->willReturn($expected); } else { $impl->method($implName)->with(...$args)->willReturn($expected); diff --git a/tests/Unit/Controller/CategoryApiControllerTest.php b/tests/Unit/Controller/CategoryApiControllerTest.php index 6484b61b3..95bd4ce48 100644 --- a/tests/Unit/Controller/CategoryApiControllerTest.php +++ b/tests/Unit/Controller/CategoryApiControllerTest.php @@ -8,9 +8,6 @@ use OCA\Cookbook\Controller\CategoryApiController; use OCA\Cookbook\Controller\Implementation\CategoryImplementation; -use OCP\IRequest; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; /** * @covers \OCA\Cookbook\Controller\CategoryApiController diff --git a/tests/Unit/Controller/CategoryControllerTest.php b/tests/Unit/Controller/CategoryControllerTest.php index 9857e0b15..baf47a4c0 100644 --- a/tests/Unit/Controller/CategoryControllerTest.php +++ b/tests/Unit/Controller/CategoryControllerTest.php @@ -8,9 +8,6 @@ use OCA\Cookbook\Controller\CategoryController; use OCA\Cookbook\Controller\Implementation\CategoryImplementation; -use OCP\IRequest; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; /** * @covers \OCA\Cookbook\Controller\CategoryController diff --git a/tests/Unit/Controller/ConfigApiControllerTest.php b/tests/Unit/Controller/ConfigApiControllerTest.php index 2e5a3b3bd..fbc4eb279 100644 --- a/tests/Unit/Controller/ConfigApiControllerTest.php +++ b/tests/Unit/Controller/ConfigApiControllerTest.php @@ -6,18 +6,6 @@ namespace OCA\Cookbook\tests\Unit\Controller; -use OCP\IRequest; -use ReflectionProperty; -use PHPUnit\Framework\TestCase; -use OCP\AppFramework\Http\Response; -use OCA\Cookbook\Service\RecipeService; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\JSONResponse; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Helper\UserFolderHelper; -use OCA\Cookbook\Helper\RestParameterParser; -use PHPUnit\Framework\MockObject\MockObject; -use OCA\Cookbook\Controller\ConfigController; use OCA\Cookbook\Controller\ConfigApiController; use OCA\Cookbook\Controller\Implementation\ConfigImplementation; diff --git a/tests/Unit/Controller/ConfigControllerTest.php b/tests/Unit/Controller/ConfigControllerTest.php index e4bb9d61f..aafc5b85a 100644 --- a/tests/Unit/Controller/ConfigControllerTest.php +++ b/tests/Unit/Controller/ConfigControllerTest.php @@ -6,25 +6,13 @@ namespace OCA\Cookbook\tests\Unit\Controller; -use OCP\IRequest; -use PHPUnit\Framework\TestCase; -use OCA\Cookbook\Service\RecipeService; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Helper\RestParameterParser; -use PHPUnit\Framework\MockObject\MockObject; use OCA\Cookbook\Controller\ConfigController; use OCA\Cookbook\Controller\Implementation\ConfigImplementation; -use OCA\Cookbook\Helper\UserFolderHelper; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\JSONResponse; -use OCP\AppFramework\Http\Response; -use ReflectionProperty; /** * @covers OCA\Cookbook\Controller\ConfigController */ class ConfigControllerTest extends AbstractControllerTestCase { - protected function getClassName(): string { return ConfigController::class; } @@ -40,5 +28,4 @@ protected function getMethodsAndParameters(): array { ['name' => 'config', 'once' => true], ]; } - } diff --git a/tests/Unit/Controller/Implementation/CategoryImplementationTest.php b/tests/Unit/Controller/Implementation/CategoryImplementationTest.php index e70f1f8c4..75ed10667 100644 --- a/tests/Unit/Controller/Implementation/CategoryImplementationTest.php +++ b/tests/Unit/Controller/Implementation/CategoryImplementationTest.php @@ -4,20 +4,14 @@ use Exception; use OCA\Cookbook\Controller\Implementation\CategoryImplementation; -use OCP\IRequest; -use OCP\Files\File; use OCP\IURLGenerator; use ReflectionProperty; use PHPUnit\Framework\TestCase; use OCA\Cookbook\Service\RecipeService; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\JSONResponse; use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Controller\MainController; use OCA\Cookbook\Helper\RestParameterParser; use PHPUnit\Framework\MockObject\MockObject; -use OCA\Cookbook\Exception\RecipeExistsException; -use OCA\Cookbook\Exception\UserFolderNotWritableException; use OCA\Cookbook\Helper\UserFolderHelper; /** diff --git a/tests/Unit/Controller/Implementation/ConfigImplementationTest.php b/tests/Unit/Controller/Implementation/ConfigImplementationTest.php index c3ba2c763..88348da01 100644 --- a/tests/Unit/Controller/Implementation/ConfigImplementationTest.php +++ b/tests/Unit/Controller/Implementation/ConfigImplementationTest.php @@ -2,7 +2,6 @@ namespace OCA\Cookbook\tests\Unit\Controller\Implementation; -use OCP\IRequest; use ReflectionProperty; use PHPUnit\Framework\TestCase; use OCP\AppFramework\Http\Response; diff --git a/tests/Unit/Controller/Implementation/KeywordImplementationTest.php b/tests/Unit/Controller/Implementation/KeywordImplementationTest.php index cd5ab7b7c..8e8f48c41 100644 --- a/tests/Unit/Controller/Implementation/KeywordImplementationTest.php +++ b/tests/Unit/Controller/Implementation/KeywordImplementationTest.php @@ -55,5 +55,4 @@ public function testGetKeywords(): void { $this->assertEquals(200, $ret->getStatus()); $this->assertEquals($kw, $ret->getData()); } - } diff --git a/tests/Unit/Controller/Implementation/RecipeImplementationTest.php b/tests/Unit/Controller/Implementation/RecipeImplementationTest.php index 9311c2f4f..0054f0d35 100644 --- a/tests/Unit/Controller/Implementation/RecipeImplementationTest.php +++ b/tests/Unit/Controller/Implementation/RecipeImplementationTest.php @@ -7,29 +7,26 @@ use OCP\IRequest; use OCP\Files\File; use OCP\IURLGenerator; -use ReflectionProperty; use OCP\AppFramework\Http; use PHPUnit\Framework\TestCase; use OCP\AppFramework\Http\IOutput; use PHPUnit\Framework\MockObject\Stub; use OCA\Cookbook\Service\RecipeService; -use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\JSONResponse; use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Helper\UserFolderHelper; -use OCA\Cookbook\Controller\MainController; use OCA\Cookbook\Helper\RestParameterParser; use PHPUnit\Framework\MockObject\MockObject; use OCA\Cookbook\Exception\RecipeExistsException; use OCA\Cookbook\Helper\AcceptHeaderParsingHelper; use OCA\Cookbook\Exception\NoRecipeNameGivenException; use OCA\Cookbook\Helper\Filter\RecipeJSONOutputFilter; -use OCA\Cookbook\Exception\UserFolderNotWritableException; use OCA\Cookbook\Controller\Implementation\RecipeImplementation; /** * @covers \OCA\Cookbook\Controller\Implementation\RecipeImplementation * @covers \OCA\Cookbook\Exception\UserFolderNotWritableException + * @covers \OCA\Cookbook\Exception\RecipeExistsException + * @covers \OCA\Cookbook\Exception\NoRecipeNameGivenException */ class RecipeImplementationTest extends TestCase { /** @var IRequest|MockObject */ @@ -79,19 +76,6 @@ public function setUp(): 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) { - // $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'); } diff --git a/tests/Unit/Controller/KeywordApiControllerTest.php b/tests/Unit/Controller/KeywordApiControllerTest.php index cac6b6b3d..25a3f2cb5 100644 --- a/tests/Unit/Controller/KeywordApiControllerTest.php +++ b/tests/Unit/Controller/KeywordApiControllerTest.php @@ -8,16 +8,11 @@ use OCA\Cookbook\Controller\Implementation\KeywordImplementation; use OCA\Cookbook\Controller\KeywordApiController; -use OCP\AppFramework\Http\JSONResponse; -use OCP\IRequest; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; /** * @covers \OCA\Cookbook\Controller\KeywordApiController */ class KeywordApiControllerTest extends AbstractControllerTestCase { - protected function getClassName(): string { return KeywordApiController::class; } @@ -31,5 +26,4 @@ protected function getMethodsAndParameters(): array { ['name' => 'keywords', 'implName' => 'index'], ]; } - } diff --git a/tests/Unit/Controller/KeywordControllerTest.php b/tests/Unit/Controller/KeywordControllerTest.php index ab8c499ca..86c4d26f2 100644 --- a/tests/Unit/Controller/KeywordControllerTest.php +++ b/tests/Unit/Controller/KeywordControllerTest.php @@ -8,16 +8,11 @@ use OCA\Cookbook\Controller\Implementation\KeywordImplementation; use OCA\Cookbook\Controller\KeywordController; -use OCP\AppFramework\Http\JSONResponse; -use OCP\IRequest; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; /** * @covers \OCA\Cookbook\Controller\KeywordController */ class KeywordControllerTest extends AbstractControllerTestCase { - protected function getClassName(): string { return KeywordController::class; } diff --git a/tests/Unit/Controller/MainControllerTest.php b/tests/Unit/Controller/MainControllerTest.php index e841fcf99..f7f352aea 100644 --- a/tests/Unit/Controller/MainControllerTest.php +++ b/tests/Unit/Controller/MainControllerTest.php @@ -2,20 +2,11 @@ namespace OCA\Cookbook\tests\Unit\Controller; -use Exception; use OCP\IRequest; -use OCP\Files\File; -use OCP\IURLGenerator; -use ReflectionProperty; use PHPUnit\Framework\TestCase; -use OCA\Cookbook\Service\RecipeService; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\JSONResponse; use OCA\Cookbook\Service\DbCacheService; use OCA\Cookbook\Controller\MainController; -use OCA\Cookbook\Helper\RestParameterParser; use PHPUnit\Framework\MockObject\MockObject; -use OCA\Cookbook\Exception\RecipeExistsException; use OCA\Cookbook\Exception\UserFolderNotWritableException; use OCA\Cookbook\Helper\UserFolderHelper; use OCP\Files\Folder; @@ -75,7 +66,4 @@ public function testIndexInvalidUser(): void { $this->assertEquals(200, $ret->getStatus()); $this->assertEquals('invalid_guest', $ret->getTemplateName()); } - - - } diff --git a/tests/Unit/Controller/RecipeApiControllerTest.php b/tests/Unit/Controller/RecipeApiControllerTest.php index 4e1a88dbf..74b97ca74 100644 --- a/tests/Unit/Controller/RecipeApiControllerTest.php +++ b/tests/Unit/Controller/RecipeApiControllerTest.php @@ -6,28 +6,7 @@ namespace OCA\Cookbook\tests\Unit\Controller; -use Exception; -use OCP\IL10N; -use OCP\IRequest; -use OCP\Files\File; -use OCP\IURLGenerator; -use ReflectionProperty; -use OCP\AppFramework\Http; -use PHPUnit\Framework\TestCase; -use OCP\AppFramework\Http\IOutput; -use PHPUnit\Framework\MockObject\Stub; -use OCA\Cookbook\Service\RecipeService; -use OCP\AppFramework\Http\DataResponse; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Helper\RestParameterParser; -use PHPUnit\Framework\MockObject\MockObject; -use OCA\Cookbook\Controller\RecipeController; -use OCP\AppFramework\Http\FileDisplayResponse; use OCA\Cookbook\Controller\RecipeApiController; -use OCA\Cookbook\Exception\RecipeExistsException; -use OCA\Cookbook\Helper\AcceptHeaderParsingHelper; -use OCA\Cookbook\Exception\NoRecipeNameGivenException; -use OCA\Cookbook\Helper\Filter\RecipeJSONOutputFilter; use OCA\Cookbook\Controller\Implementation\RecipeImplementation; /** @@ -35,7 +14,6 @@ * @covers \OCA\Cookbook\Exception\NoRecipeNameGivenException */ class RecipeApiControllerTest extends AbstractControllerTestCase { - protected function getClassName(): string { return RecipeApiController::class; } @@ -58,6 +36,4 @@ protected function getMethodsAndParameters(): array { ['name' => 'tags', 'args' => [['one keyword'], ['one keyword,another one']], 'implName' => 'getAllWithTags'], ]; } - - } diff --git a/tests/Unit/Controller/RecipeControllerTest.php b/tests/Unit/Controller/RecipeControllerTest.php index d36161e16..68340b4c0 100644 --- a/tests/Unit/Controller/RecipeControllerTest.php +++ b/tests/Unit/Controller/RecipeControllerTest.php @@ -6,35 +6,14 @@ namespace OCA\Cookbook\tests\Unit\Controller; -use Exception; use OCA\Cookbook\Controller\Implementation\RecipeImplementation; -use OCP\IRequest; -use OCP\Files\File; -use OCP\IURLGenerator; -use ReflectionProperty; -use OCP\AppFramework\Http; -use PHPUnit\Framework\TestCase; -use OCP\AppFramework\Http\IOutput; -use OCA\Cookbook\Service\RecipeService; -use OCP\AppFramework\Http\DataResponse; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Helper\RestParameterParser; -use PHPUnit\Framework\MockObject\MockObject; use OCA\Cookbook\Controller\RecipeController; -use OCA\Cookbook\Exception\NoRecipeNameGivenException; -use OCP\AppFramework\Http\FileDisplayResponse; -use OCA\Cookbook\Exception\RecipeExistsException; -use OCA\Cookbook\Helper\AcceptHeaderParsingHelper; -use OCA\Cookbook\Helper\Filter\RecipeJSONOutputFilter; -use OCP\IL10N; -use PHPUnit\Framework\MockObject\Stub; /** * @covers \OCA\Cookbook\Controller\RecipeController * @covers \OCA\Cookbook\Exception\NoRecipeNameGivenException */ class RecipeControllerTest extends AbstractControllerTestCase { - protected function getClassName(): string { return RecipeController::class; } @@ -57,6 +36,4 @@ protected function getMethodsAndParameters(): array { ['name' => 'tags', 'args' => [['one keyword'], ['one keyword,another one']], 'implName' => 'getAllWithTags'], ]; } - - } diff --git a/tests/Unit/Controller/UtilApiControllerTest.php b/tests/Unit/Controller/UtilApiControllerTest.php index d01983e30..4b8f0395d 100644 --- a/tests/Unit/Controller/UtilApiControllerTest.php +++ b/tests/Unit/Controller/UtilApiControllerTest.php @@ -2,30 +2,15 @@ namespace OCA\Cookbook\tests\Unit\Controller; -use Exception; use OCP\IRequest; -use OCP\Files\File; -use OCP\IURLGenerator; -use ReflectionProperty; use PHPUnit\Framework\TestCase; -use OCA\Cookbook\Service\RecipeService; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\JSONResponse; -use OCA\Cookbook\Service\DbCacheService; -use OCA\Cookbook\Controller\MainController; use OCA\Cookbook\Controller\UtilApiController; -use OCA\Cookbook\Helper\RestParameterParser; -use PHPUnit\Framework\MockObject\MockObject; -use OCA\Cookbook\Exception\RecipeExistsException; -use OCA\Cookbook\Exception\UserFolderNotWritableException; -use OCA\Cookbook\Helper\UserFolderHelper; /** * @covers \OCA\Cookbook\Controller\UtilApiController * @covers \OCA\Cookbook\Exception\UserFolderNotWritableException */ class UtilApiControllerTest extends TestCase { - /** * @var UtilApiController */ @@ -54,7 +39,7 @@ public function testGetAPIVersion(): void { $this->assertIsInt($retData['cookbook_version'][0]); $this->assertIsInt($retData['cookbook_version'][1]); $this->assertIsInt($retData['cookbook_version'][2]); - if(count($retData['cookbook_version']) === 4) { + if (count($retData['cookbook_version']) === 4) { $this->assertIsString($retData['cookbook_version'][3]); } @@ -63,7 +48,4 @@ public function testGetAPIVersion(): void { $this->assertArrayHasKey('major', $retData['api_version']); $this->assertArrayHasKey('minor', $retData['api_version']); } - - - } diff --git a/tests/Unit/Service/HtmlDownloadServiceTest.php b/tests/Unit/Service/HtmlDownloadServiceTest.php index 6d9c6c333..843dc247d 100644 --- a/tests/Unit/Service/HtmlDownloadServiceTest.php +++ b/tests/Unit/Service/HtmlDownloadServiceTest.php @@ -20,6 +20,8 @@ /** * @covers \OCA\Cookbook\Service\HtmlDownloadService + * @covers \OCA\Cookbook\Exception\NoDownloadWasCarriedOutException + * @covers \OCA\Cookbook\Exception\ImportException */ class HtmlDownloadServiceTest extends TestCase { /** From 6940ecfa474531f55757d892d676f27b2b73bdb4 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 2 Sep 2022 15:27:44 +0200 Subject: [PATCH 4/7] Fix webapp paths in Frontend code Signed-off-by: Christian Wolf --- src/js/api-interface.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/js/api-interface.js b/src/js/api-interface.js index 32a0ad792..0f032d9a5 100644 --- a/src/js/api-interface.js +++ b/src/js/api-interface.js @@ -2,46 +2,46 @@ import axios from "@nextcloud/axios" import { generateUrl } from "@nextcloud/router" -const baseUrl = generateUrl("apps/cookbook") +const baseUrl = `${generateUrl("apps/cookbook")}/webapp` function createNewRecipe(recipe) { return axios({ method: "POST", - url: `${baseUrl}/api/recipes`, + url: `${baseUrl}/recipes`, data: recipe, }) } function getRecipe(id) { - return axios.get(`${baseUrl}/api/recipes/${id}`) + return axios.get(`${baseUrl}/recipes/${id}`) } function getAllRecipes() { - return axios.get(`${baseUrl}/api/recipes`) + return axios.get(`${baseUrl}/recipes`) } function getAllRecipesOfCategory(categoryName) { - return axios.get(`${baseUrl}/api/category/${categoryName}`) + return axios.get(`${baseUrl}/category/${categoryName}`) } function getAllRecipesWithTag(tags) { - return axios.get(`${baseUrl}/api/tags/${tags}`) + return axios.get(`${baseUrl}/tags/${tags}`) } function searchRecipes(search) { - return axios.get(`${baseUrl}/api/search/${search}`) + return axios.get(`${baseUrl}/search/${search}`) } function updateRecipe(id, recipe) { return axios({ method: "PUT", - url: `${baseUrl}/api/recipes/${id}`, + url: `${baseUrl}/recipes/${id}`, data: recipe, }) } function deleteRecipe(id) { - return axios.delete(`${baseUrl}/api/recipes/${id}`) + return axios.delete(`${baseUrl}/recipes/${id}`) } function importRecipe(url) { @@ -59,7 +59,7 @@ function getAllCategories() { function updateCategoryName(oldName, newName) { return axios({ method: "PUT", - url: `${baseUrl}/api/category/${encodeURIComponent(oldName)}`, + url: `${baseUrl}/category/${encodeURIComponent(oldName)}`, data: { name: newName }, }) } From 05f7829773a0b3bb581eb3299dc951ec43fd4899 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 2 Sep 2022 15:36:38 +0200 Subject: [PATCH 5/7] Remove CSRF requirement on loading images Signed-off-by: Christian Wolf --- lib/Controller/RecipeController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Controller/RecipeController.php b/lib/Controller/RecipeController.php index 46034ac94..99c16f38a 100755 --- a/lib/Controller/RecipeController.php +++ b/lib/Controller/RecipeController.php @@ -70,6 +70,7 @@ public function destroy($id) { /** * @NoAdminRequired + * @NoCSRFRequired * @param $id * @return JSONResponse|FileDisplayResponse|DataDisplayResponse */ From 7474e7a097426adba16648bcb6ebd26d2502b23f Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 2 Sep 2022 15:45:04 +0200 Subject: [PATCH 6/7] Added legacy routes as deprecated ones Signed-off-by: Christian Wolf --- appinfo/routes.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/appinfo/routes.php b/appinfo/routes.php index fd29166f1..c24b58965 100755 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -55,6 +55,25 @@ ['name' => 'config_api#config', 'url' => '/api/v1/config', 'verb' => 'POST'], ['name' => 'config_api#reindex', 'url' => '/api/v1/reindex', 'verb' => 'POST'], + // DEPRECATED ROUTES + // These routes are here only to avoid breaking the current 3rd party apps. They will be removed with the next release. + + ['name' => 'recipe_api#import', 'url' => '/import', 'verb' => 'POST', 'postfix' => '_legacy'], + ['name' => 'recipe_api#image', 'url' => '/recipes/{id}/image', 'verb' => 'GET', 'requirements' => ['id' => '\d+'], 'postfix' => '_legacy'], + ['name' => 'recipe_api#category', 'url' => '/api/category/{category}', 'verb' => 'GET', 'postfix' => '_legacy'], + ['name' => 'recipe_api#tags', 'url' => '/api/tags/{keywords}', 'verb' => 'GET', 'postfix' => '_legacy'], + ['name' => 'recipe_api#search', 'url' => '/api/search/{query}', 'verb' => 'GET', 'postfix' => '_legacy'], + + ['name' => 'keyword_api#keywords', 'url' => '/keywords', 'verb' => 'GET', 'postfix' => '_legacy'], + + ['name' => 'category_api#categories', 'url' => '/categories', 'verb' => 'GET', 'postfix' => '_legacy'], + ['name' => 'category_api#rename', 'url' => '/api/category/{category}', 'verb' => 'PUT', 'postfix' => '_legacy'], + + ['name' => 'config_api#list', 'url' => '/config', 'verb' => 'GET', 'postfix' => '_legacy'], + ['name' => 'config_api#config', 'url' => '/config', 'verb' => 'POST', 'postfix' => '_legacy'], + ['name' => 'config_api#reindex', 'url' => '/reindex', 'verb' => 'POST', 'postfix' => '_legacy'], + + // Preflight option for CORS API ['name' => 'util_api#preflighted_cors', 'url' => '/api/{path}', 'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']], ], From f05d678e680982a14e6c0584de392f69a9ad8c90 Mon Sep 17 00:00:00 2001 From: Christian Wolf Date: Fri, 2 Sep 2022 15:47:58 +0200 Subject: [PATCH 7/7] Add changelog Signed-off-by: Christian Wolf --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857fa3bd1..34c97a639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +### Fixed +- Close security issue by enabling CSRF protection on most endpoints + [#1190](https://github.com/nextcloud/cookbook/pull/1190) @christianlupus + ### Documentation - Defining new API interface to fix security issue [#1186](https://github.com/nextcloud/cookbook/pull/1186) @christianlupus