From 664796c69f37c3e21d1f37845019fd1f7af557e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabo=20M=C3=B3nika?= Date: Thu, 30 Jan 2025 09:35:06 +0100 Subject: [PATCH] using phpunit to test controllers which are running in codeigniter3 requires further request/response mocking; setup of swagger-ui and swagger openapi doc generation for bookmark.php; WIP integration tests with ci3 & phpunit, maybe annotation generation solution for swagger annotations --- application/config/test/config.php | 7 + .../controllers/api/frontend/v1/Bookmark.php | 162 +++++++++++++++++- composer.json | 3 +- index.phpunit.php | 47 ++--- phpunit.xml | 8 +- public/js/api/bookmark.js | 2 +- public/js/api/factory/widget/bookmark.js | 2 +- .../CIIntegrationTestCase.php | 55 ++++++ .../api/BookmarkTest/BookmarkTest.php | 149 ++++++++++++++++ system/UnitTests/CI_TestCase.php | 17 ++ .../BookmarkTest/BookmarkIntegrationTest.php | 80 +++++++++ .../api/BookmarkTest/BookmarkTest.php | 68 +++----- testbootstrap.php | 41 +++++ 13 files changed, 558 insertions(+), 83 deletions(-) create mode 100644 application/config/test/config.php create mode 100644 system/IntegrationTests/CIIntegrationTestCase.php create mode 100644 system/IntegrationTests/api/BookmarkTest/BookmarkTest.php create mode 100644 system/UnitTests/CI_TestCase.php create mode 100644 system/UnitTests/api/BookmarkTest/BookmarkIntegrationTest.php create mode 100644 testbootstrap.php diff --git a/application/config/test/config.php b/application/config/test/config.php new file mode 100644 index 000000000..724f285b6 --- /dev/null +++ b/application/config/test/config.php @@ -0,0 +1,7 @@ + self::PERM_LOGGED, 'insert' => self::PERM_LOGGED, 'update' => self::PERM_LOGGED, + 'test_true' => self::PERM_LOGGED ]); $this->load->model('dashboard/Bookmark_model', 'BookmarkModel'); @@ -48,6 +70,21 @@ class Bookmark extends FHCAPI_Controller * gets the bookmarks associated to a user * @access public * @return void + * @SWG\Get( + * path="/getBookmarks", + * security={{"basicAuth":{}}}, + * tags={"bookmarks"}, + * summary="Get user's bookmarks", + * description="Returns all bookmarks associated with the authenticated user.", + * @SWG\Response( + * response=200, + * description="List of bookmarks" + * ), + * @SWG\Response( + * response=401, + * description="Unauthorized" + * ) + * ) */ public function getBookmarks() { @@ -63,6 +100,31 @@ class Bookmark extends FHCAPI_Controller * deletes bookmark from associated user * @access public * @return void + * @SWG\Post( + * path="/delete/{bookmark_id}", + * security={{"basicAuth":{}}}, + * tags={"bookmarks"}, + * summary="Delete a bookmark", + * description="Deletes a bookmark if the user is the owner or an admin.", + * @SWG\Parameter( + * name="bookmark_id", + * in="path", + * required=true, + * type="integer" + * ), + * @SWG\Response( + * response=200, + * description="Bookmark deleted successfully" + * ), + * @SWG\Response( + * response=403, + * description="Forbidden - not the owner" + * ), + * @SWG\Response( + * response=404, + * description="Bookmark not found" + * ) + * ) */ public function delete($bookmark_id) { @@ -87,6 +149,44 @@ class Bookmark extends FHCAPI_Controller * inserts new bookmark into the bookmark table * @access public * @return void + * @SWG\Post( + * path="/insert", + * security={{"basicAuth":{}}}, + * tags={"bookmarks"}, + * summary="Insert a new bookmark", + * @SWG\Parameter( + * name="body", + * in="body", + * required=true, + * @SWG\Schema( + * type="object", + * required={"url", "title"}, + * @SWG\Property( + * property="url", + * type="string", + * example="https://github.com/swagger-api/swagger-codegen" + * ), + * @SWG\Property( + * property="title", + * type="string", + * example="Swagger Codegen" + * ), + * @SWG\Property( + * property="tag", + * type="string", + * example="API" + * ) + * ) + * ), + * @SWG\Response( + * response=201, + * description="Bookmark created" + * ), + * @SWG\Response( + * response=400, + * description="Validation error" + * ) + * ) */ public function insert() { @@ -109,9 +209,47 @@ class Bookmark extends FHCAPI_Controller } /** - * updates bookmark in the bookmark table - * @access public - * @return void + * @SWG\Post( + * path="/update/{bookmark_id}", + * security={{"basicAuth":{}}}, + * tags={"bookmarks"}, + * summary="Update a bookmark", + * description="Updates a bookmark's URL and title for the given ID.", + * @SWG\Parameter( + * name="bookmark_id", + * in="path", + * required=true, + * type="integer", + * description="ID of the bookmark to update" + * ), + * @SWG\Parameter( + * name="body", + * in="body", + * required=true, + * @SWG\Schema( + * type="object", + * required={"url", "title"}, + * @SWG\Property( + * property="url", + * type="string", + * example="https://updated-url.com" + * ), + * @SWG\Property( + * property="title", + * type="string", + * example="Updated Title" + * ) + * ) + * ), + * @SWG\Response( + * response=200, + * description="Bookmark updated" + * ), + * @SWG\Response( + * response=400, + * description="Validation error" + * ) + * ) */ public function update($bookmark_id) { @@ -134,5 +272,23 @@ class Bookmark extends FHCAPI_Controller $this->terminateWithSuccess($update_result); } + + /** + * @SWG\Get( + * path="/test_true", + * security={{"basicAuth":{}}}, + * tags={"bookmarks"}, + * summary="Test endpoint", + * description="Simple test endpoint that returns 'expected response'.", + * @SWG\Response( + * response=200, + * description="Expected response" + * ) + * ) + */ + public function test_true() + { + echo "expected response"; + } } diff --git a/composer.json b/composer.json index f4b6923e1..c096dc820 100644 --- a/composer.json +++ b/composer.json @@ -481,6 +481,7 @@ "phpmd/phpmd": "2.*", "phpmetrics/phpmetrics": "2.*", "sebastian/phpcpd": "3.*", - "phpunit/phpunit": "^6" + "phpunit/phpunit": "^6", + "zircote/swagger-php": "^2.0" } } diff --git a/index.phpunit.php b/index.phpunit.php index 449e2a0ab..d32f6f495 100644 --- a/index.phpunit.php +++ b/index.phpunit.php @@ -1,39 +1,30 @@ - + - system/UnitTests/api/ + ./system/IntegrationTests/api/ - - - + diff --git a/public/js/api/bookmark.js b/public/js/api/bookmark.js index 5cef0a46b..9694336d5 100644 --- a/public/js/api/bookmark.js +++ b/public/js/api/bookmark.js @@ -8,7 +8,7 @@ export default { }, delete: function (bookmark_id) { - return this.$fhcApi.get( + return this.$fhcApi.post( `/api/frontend/v1/Bookmark/delete/${bookmark_id}` ,{} ); diff --git a/public/js/api/factory/widget/bookmark.js b/public/js/api/factory/widget/bookmark.js index 9768e25ac..53a8c615c 100644 --- a/public/js/api/factory/widget/bookmark.js +++ b/public/js/api/factory/widget/bookmark.js @@ -24,7 +24,7 @@ export default { }, delete(bookmark_id) { return { - method: 'get', + method: 'post', url: `/api/frontend/v1/Bookmark/delete/${bookmark_id}` }; }, diff --git a/system/IntegrationTests/CIIntegrationTestCase.php b/system/IntegrationTests/CIIntegrationTestCase.php new file mode 100644 index 000000000..ec4abcf3e --- /dev/null +++ b/system/IntegrationTests/CIIntegrationTestCase.php @@ -0,0 +1,55 @@ +CI =& get_instance(); + + // ensure any old URI/router state is cleared + $this->CI->uri->uri_string = ''; + $this->CI->router->_set_routing([]); + } + + /** + * Simulate an HTTP request through CI’s front controller. + * + * @param string $method GET|POST|PUT|DELETE + * @param string $uri URI string, e.g. "bookmark/getBookmarks" + * @param array $params query‐params if GET, post‐data otherwise + * @return string raw response body + */ + protected function request(string $method, string $uri, array $params = []): string + { + // 1) fake the server vars + $_SERVER['REQUEST_METHOD'] = strtoupper($method); + $_SERVER['PATH_INFO'] = '/' . ltrim($uri, '/'); + if (strtoupper($method) === 'GET') { + $_GET = $params; + $_POST = []; + } else { + $_POST = $params; + $_GET = []; + } + + // 2) re-initialize routing/URI so CI picks up our fake PATH_INFO + $this->CI->router->_set_routing(); + $this->CI->uri->_set_uri_string($_SERVER['PATH_INFO']); + // pull class/method out of the path + $segments = explode('/', trim($_SERVER['PATH_INFO'], '/')); + $class = array_shift($segments) ?: 'welcome'; + $method = array_shift($segments) ?: 'index'; + $this->CI->router->set_class($class); + $this->CI->router->set_method($method); + + // 3) capture output + ob_start(); + require BASEPATH . 'core/CodeIgniter.php'; + return ob_get_clean(); + } +} diff --git a/system/IntegrationTests/api/BookmarkTest/BookmarkTest.php b/system/IntegrationTests/api/BookmarkTest/BookmarkTest.php new file mode 100644 index 000000000..07b592037 --- /dev/null +++ b/system/IntegrationTests/api/BookmarkTest/BookmarkTest.php @@ -0,0 +1,149 @@ +bookmarkModel = $this + ->getMockBuilder('Bookmark_model') + ->setMethods(['loadWhere','load','insert','update','delete']) + ->getMock(); + + // 2) inject it into CI + $this->CI->load->model('dashboard/Bookmark_model', 'BookmarkModel'); + $this->CI->BookmarkModel = $this->bookmarkModel; + + // 3) force a known user ID + $this->CI->uid = 42; + } + + public function test_test_true_endpoint() + { + $out = $this->request('GET', 'bookmark/test_true'); + $this->assertEquals('expected response', trim($out)); + } + + public function test_getBookmarks_returns_ordered_array() + { + $dummy = [ + (object)['bookmark_id'=>1,'url'=>'a','uid'=>42], + (object)['bookmark_id'=>2,'url'=>'b','uid'=>42], + ]; + // expect loadWhere() → our dummy + $this->bookmarkModel + ->expects($this->once()) + ->method('loadWhere') + ->with(['uid'=>42]) + ->willReturn($dummy); + + $out = $this->request('GET', 'bookmark/getBookmarks'); + $data = json_decode($out); + $this->assertCount(2, $data); + $this->assertEquals(1, $data[0]->bookmark_id); + } + + public function test_delete_own_bookmark_succeeds() + { + // load() → record owned by our uid + $this->bookmarkModel + ->expects($this->once()) + ->method('load') + ->with(5) + ->willReturn([(object)['bookmark_id'=>5,'uid'=>42]]); + + // delete() → true + $this->bookmarkModel + ->expects($this->once()) + ->method('delete') + ->with(5) + ->willReturn(true); + + $out = $this->request('DELETE', 'bookmark/delete/5'); + $this->assertJsonStringEqualsJsonString('true', $out); + } + + public function test_delete_not_owned_forbidden() + { + // load() → record owned by someone else + $this->bookmarkModel + ->method('load') + ->willReturn([(object)['bookmark_id'=>8,'uid'=>99]]); + + $out = $this->request('DELETE', 'bookmark/delete/8'); + // your controller does → $this->_outputAuthError(), typically 403 + $this->assertStringContainsString('403', $out); + } + + public function test_insert_validation_error() + { + // send invalid data + $out = $this->request('POST', 'bookmark/insert', [ + 'url' => 'not-a-url', + 'title' => '' + ]); + // expecting JSON validation errors + $json = json_decode($out, true); + $this->assertArrayHasKey('url', $json); + $this->assertArrayHasKey('title', $json); + } + + public function test_insert_success() + { + $input = [ + 'url' => 'https://example.org', + 'title' => 'Example', + 'tag' => 'phpunit', + ]; + // insert(...) → new ID 99 + $this->bookmarkModel + ->expects($this->once()) + ->method('insert') + ->with($this->callback(function($args) use($input) { + return $args['url'] === $input['url'] + && $args['title'] === $input['title'] + && $args['uid'] === 42; + })) + ->willReturn(99); + + $out = $this->request('POST', 'bookmark/insert', $input); + $this->assertJsonStringEqualsJsonString('99', $out); + } + + public function test_update_validation_error() + { + $out = $this->request('POST', 'bookmark/update/3', [ + 'url' => 'bad', + 'title' => '' + ]); + $json = json_decode($out, true); + $this->assertArrayHasKey('url', $json); + $this->assertArrayHasKey('title', $json); + } + + public function test_update_success() + { + $this->bookmarkModel + ->expects($this->once()) + ->method('update') + ->with(3, $this->callback(function($d){ + return filter_var($d['url'], FILTER_VALIDATE_URL) + && isset($d['updateamum']); + })) + ->willReturn(true); + + $out = $this->request('POST', 'bookmark/update/3', [ + 'url' => 'https://ci.org', + 'title' => 'CI3', + ]); + $this->assertJsonStringEqualsJsonString('true', $out); + } +} diff --git a/system/UnitTests/CI_TestCase.php b/system/UnitTests/CI_TestCase.php new file mode 100644 index 000000000..bc9b9d62f --- /dev/null +++ b/system/UnitTests/CI_TestCase.php @@ -0,0 +1,17 @@ +_ci =& get_instance(); + } + + public function __get($name) + { + return $this->_ci->$name; + } +} diff --git a/system/UnitTests/api/BookmarkTest/BookmarkIntegrationTest.php b/system/UnitTests/api/BookmarkTest/BookmarkIntegrationTest.php new file mode 100644 index 000000000..c8ea39c1e --- /dev/null +++ b/system/UnitTests/api/BookmarkTest/BookmarkIntegrationTest.php @@ -0,0 +1,80 @@ +CI =& get_instance(); + + // Make sure we're in a clean state: + // - no lingering URI segments + // - fresh router rules + $this->CI->uri->uri_string = ''; + $this->CI->router->_set_routing([]); + } + + /** + * Simulate a GET to /api/users and assert JSON list + */ + public function testGetUsersReturnsJsonArray() + { + // 1) Fake server variables + $_SERVER['REQUEST_METHOD'] = 'GET'; + // you can use PATH_INFO or REQUEST_URI depending on your config + $_SERVER['PATH_INFO'] = '/api/users'; + $_GET = []; $_POST = []; + + // 2) Re-run routing so CI picks up our fake path + $this->CI->router->_set_routing(); + $this->CI->uri->_set_uri_string('/api/users'); + $this->CI->router->set_class('api'); + $this->CI->router->set_method('users'); + + // 3) Capture the output + ob_start(); + // This is the entry point: + // core/CodeIgniter.php will look at $RTR->class/method + // instantiate your Api controller, call ->users() + require BASEPATH . 'core/CodeIgniter.php'; + $output = ob_get_clean(); + + // 4) Assert JSON shape, status header, etc. + $this->assertNotEmpty($output, 'No output at all'); + $decoded = json_decode($output, true); + $this->assertIsArray($decoded, 'Expected JSON array'); + // more fine‐grained assertions… + } + + /** + * Simulate a POST to /api/users + */ + public function testCreateUser() + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['PATH_INFO'] = '/api/users'; + $_POST = [ + 'name' => 'Alice', + 'email' => 'alice@example.com', + ]; + $_GET = []; + + $this->CI->router->_set_routing(); + $this->CI->uri->_set_uri_string('/api/users'); + $this->CI->router->set_class('api'); + $this->CI->router->set_method('users'); + + ob_start(); + require BASEPATH . 'core/CodeIgniter.php'; + $out = ob_get_clean(); + + $this->assertStringContainsString('"id":', $out); + $this->assertMatchesRegularExpression('/201 Created/', xdebug_get_headers()); + } +} diff --git a/system/UnitTests/api/BookmarkTest/BookmarkTest.php b/system/UnitTests/api/BookmarkTest/BookmarkTest.php index 1b96033d6..89d507f08 100644 --- a/system/UnitTests/api/BookmarkTest/BookmarkTest.php +++ b/system/UnitTests/api/BookmarkTest/BookmarkTest.php @@ -1,48 +1,28 @@ _ci = &get_instance(); -// -// if (!is_object($this->_ci)) { -// throw new \Exception('CI instance is not available'); -// } -// -// $this->_ci->load->library('AuthLib', null, 'AuthLib'); -// -// $this->_ci->AuthLib->loginLDAP('horauer', 'FHCompleteDemo42!'); -// $this->_ci->load->controller('api/v1/Bookmark'); -// -// $this->obj = new Bookmark(); +//// require_once APPPATH . 'core/FHC_Controller.php'; +//// require_once APPPATH . 'core/Auth_Controller.php'; +//// require_once APPPATH . 'core/FHCAPI_Controller.php'; +// require_once APPPATH . 'controllers/api/frontend/v1/Bookmark.php'; +// $this->bookmark = new Bookmark(); // or $this->CI->bookmark // } - - /** @test */ - public function test_true() - { - echo 'in the test_true case'; - $this->assertTrue(true); - } - - -} \ No newline at end of file +// +// /** @test */ +// public function test_true() +// { +// echo "expected response"; +// +// } +// +// +//} \ No newline at end of file diff --git a/testbootstrap.php b/testbootstrap.php new file mode 100644 index 000000000..87d495210 --- /dev/null +++ b/testbootstrap.php @@ -0,0 +1,41 @@ +