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

This commit is contained in:
Szabo Mónika
2025-01-30 09:35:06 +01:00
committed by Johann Hoffmann
parent 37fa899540
commit 664796c69f
13 changed files with 558 additions and 83 deletions
+7
View File
@@ -0,0 +1,7 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
// broaden the allowed URI characters just for tests
$config['permitted_uri_chars'] = 'a-z A-Z 0-9~%.:_\-';
// ensure we read REQUEST_URI
$config['uri_protocol'] = 'REQUEST_URI';
@@ -17,6 +17,27 @@
*/
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
* @SWG\Info(
* title="Bookmark API",
* version="1.0.0"
* )
*/
/**
* @SWG\Swagger(
* schemes={"https"},
* basePath="/fhcompletecis4/cis.php/api/frontend/v1/Bookmark/"
* )
*/
/**
* @SWG\SecurityScheme(
* securityDefinition="basicAuth",
* type="basic"
* )
*/
class Bookmark extends FHCAPI_Controller
{
@@ -31,6 +52,7 @@ class Bookmark extends FHCAPI_Controller
'delete' => 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";
}
}
+2 -1
View File
@@ -481,6 +481,7 @@
"phpmd/phpmd": "2.*",
"phpmetrics/phpmetrics": "2.*",
"sebastian/phpcpd": "3.*",
"phpunit/phpunit": "^6"
"phpunit/phpunit": "^6",
"zircote/swagger-php": "^2.0"
}
}
+19 -28
View File
@@ -1,39 +1,30 @@
<?php
// Set environment
define('ENVIRONMENT', 'testing');
$_SERVER['CI_ENV'] = 'testing';
// Setup CI core constants
$system_path = __DIR__ . '/vendor/codeigniter/framework/system';
$application_folder = __DIR__ . '/application';
$view_folder = $application_folder . '/views';
$view_folder = '';
if (!defined('ICONV_ENABLED')) define('ICONV_ENABLED', function_exists('iconv'));
if (!defined('MB_ENABLED')) define('MB_ENABLED', extension_loaded('mbstring'));
if (($_temp = realpath($system_path)) !== FALSE) {
$system_path = $_temp . '/';
} else {
$system_path = rtrim($system_path, '/') . '/';
}
define('BASEPATH', str_replace("\\", "/", $system_path));
define('APPPATH', str_replace("\\", "/", realpath($application_folder)) . '/');
if (!empty($view_folder) && is_dir($view_folder)) {
$view_folder = realpath($view_folder) . '/';
} elseif (is_dir(APPPATH . 'views/')) {
$view_folder = APPPATH . 'views/';
} else {
exit('Your view folder path is invalid');
}
define('VIEWPATH', $view_folder);
define('SELF', pathinfo(__FILE__, PATHINFO_BASENAME));
define('BASEPATH', rtrim(realpath($system_path), '/') . '/');
define('APPPATH', rtrim(realpath($application_folder), '/') . '/');
define('VIEWPATH', rtrim(realpath($view_folder), '/') . '/');
define('FCPATH', __DIR__ . '/');
// Autoload Composer and project config
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/config/global.config.inc.php';
require_once __DIR__ . '/config/vilesci.config.inc.php';
require_once __DIR__ . '/application/config/core_includes.php';
// Load base CI functionality manually, without routing
require_once BASEPATH . 'core/Common.php';
$CFG =& load_class('Config', 'core');
$UNI =& load_class('Utf8', 'core');
$URI =& load_class('URI', 'core');
$RTR =& load_class('Router', 'core'); // Optional: Don't route
$OUT =& load_class('Output', 'core');
require_once BASEPATH . 'core/Controller.php';
//require_once BASEPATH . 'core/CodeIgniter.php';
function &get_instance() { return CI_Controller::get_instance(); }
// Manually load your custom controller later in tests
require_once BASEPATH . 'core/CodeIgniter.php';
+3 -5
View File
@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<phpunit bootstrap="index.phpunit.php">
<phpunit bootstrap="testbootstrap.php">
<testsuites>
<testsuite name="BookmarkTest">
<directory>system/UnitTests/api/</directory>
<directory>./system/IntegrationTests/api/</directory>
</testsuite>
</testsuites>
<php>
<ini name="display_errors" value="1"/>
<ini name="display_startup_errors" value="1" />
<const name="PHPUNIT_TEST" value="1" />
<server name="CI_ENV" value="testing"/>
</php>
</phpunit>
+1 -1
View File
@@ -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}`
,{}
);
+1 -1
View File
@@ -24,7 +24,7 @@ export default {
},
delete(bookmark_id) {
return {
method: 'get',
method: 'post',
url: `/api/frontend/v1/Bookmark/delete/${bookmark_id}`
};
},
@@ -0,0 +1,55 @@
<?php
// tests/Integration/CIIntegrationTestCase.php
abstract class CIIntegrationTestCase extends PHPUnit\Framework\TestCase
{
/** @var CI_Controller */
protected $CI;
protected function setUp()
{
// grab the existing CI “superobject”
$this->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 CIs front controller.
*
* @param string $method GET|POST|PUT|DELETE
* @param string $uri URI string, e.g. "bookmark/getBookmarks"
* @param array $params queryparams if GET, postdata 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();
}
}
@@ -0,0 +1,149 @@
<?php
// tests/Integration/BookmarkControllerTest.php
require_once __DIR__ . '/../../CIIntegrationTestCase.php';
class BookmarkControllerTest extends CIIntegrationTestCase
{
/** @var PHPUnit_Framework_MockObject_MockObject */
protected $bookmarkModel;
protected function setUp()
{
parent::setUp();
// 1) build a mock for the Bookmark_model
$this->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);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
use PHPUnit\Framework\TestCase;
class CI_TestCase extends TestCase
{
public $_ci;
public function setUp()
{
$this->_ci =& get_instance();
}
public function __get($name)
{
return $this->_ci->$name;
}
}
@@ -0,0 +1,80 @@
<?php
// tests/Integration/ApiControllerTest.php
use PHPUnit\Framework\TestCase;
class BookmarkIntegrationTest extends TestCase
{
/** @var CI_Controller */
protected $CI;
public function setUp()
{
// Grab the global CI instance
$this->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 finegrained 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());
}
}
@@ -1,48 +1,28 @@
<?php
namespace BookmarkTest;
echo "BookmarkTest loaded\n";
echo "Resolved path: " . APPPATH.'controllers/api/frontend/v1/Bookmark.php' . "\n";
//require_once(APPPATH.'controllers/api/frontend/v1/Bookmark.php');
//require_once(APPPATH.'libraries/AuthLib.php');
use Bookmark;
use PHPUnit\Framework\TestCase;
class BookmarkTest extends TestCase
{
private $_ci;
// public function setUp(): void
//
//echo "BookmarkTest loaded\n";
//require_once __DIR__ . '/../../CI_TestCase.php';
//
//class BookmarkTest extends CI_TestCase
//{
//
// public function setUp()
// {
// error_reporting(E_ALL & ~E_STRICT & ~E_DEPRECATED);
// parent::setUp();
//
// echo 'in the setUp';
// // Load CodeIgniter instance
// $this->_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);
}
}
//
// /** @test */
// public function test_true()
// {
// echo "expected response";
//
// }
//
//
//}
+41
View File
@@ -0,0 +1,41 @@
<?php
// tests/Bootstrap.php
if (! defined('CI_BOOTSTRAPPED'))
{
define('CI_BOOTSTRAPPED', true);
// 1) point these at your project
define('ENVIRONMENT', 'testing');
$_SERVER['CI_ENV'] = 'testing';
$system_path = __DIR__ . '/vendor/codeigniter/framework/system';
$application_folder = __DIR__ . '/application';
$view_folder = '';
if (($_temp = realpath($system_path)) !== FALSE) {
$system_path = $_temp . '/';
} else {
$system_path = rtrim($system_path, '/') . '/';
}
define('BASEPATH', str_replace("\\", "/", $system_path));
define('APPPATH', str_replace("\\", "/", realpath($application_folder)) . '/');
if (!empty($view_folder) && is_dir($view_folder)) {
$view_folder = realpath($view_folder) . '/';
} elseif (is_dir(APPPATH . 'views/')) {
$view_folder = APPPATH . 'views/';
} else {
exit('Your view folder path is invalid');
}
define('VIEWPATH', $view_folder);
define('SELF', pathinfo(__FILE__, PATHINFO_BASENAME));
define('FCPATH', __DIR__ . '/');
// 2) show all errors while testing
error_reporting(-1);
ini_set('display_errors', 1);
// 3) bring in the core (but we wont hit any controller until we call it)
require_once __DIR__ . '/vendor/autoload.php';
require_once BASEPATH . 'core/CodeIgniter.php';
}