diff --git a/.dev/docker-compose.tests.yaml b/.dev/docker-compose.tests.yaml index d76ddd46b..bb422c1bc 100644 --- a/.dev/docker-compose.tests.yaml +++ b/.dev/docker-compose.tests.yaml @@ -14,7 +14,7 @@ services: - "44302:443" #Using different ports for testing, so we don't have to worry about collision (available range is 44300–44399) environment: LEAN_DB_HOST: 'db' - LEAN_DB_USER: 'leantime' + LEAN_DB_USER: 'root' LEAN_DB_PASSWORD: 'leantime' LEAN_DB_DATABASE: 'leantime_test' LEAN_DB_PORT: '3306' diff --git a/.dev/test.env b/.dev/test.env index b900f7007..e8d816c33 100644 --- a/.dev/test.env +++ b/.dev/test.env @@ -10,10 +10,10 @@ LEAN_APP_DIR = '' # Base of application without trai LEAN_DEBUG = 1 # Debug flag # Database -LEAN_DB_HOST='localhost' # Database host +LEAN_DB_HOST='db' # Database host LEAN_DB_USER='root' # Database username LEAN_DB_PASSWORD='test' # Database password -LEAN_DB_DATABASE='leantime' # Database name +LEAN_DB_DATABASE='leantime_test' # Database name LEAN_DB_PORT='3306' # Database port ## Optional Configuraiton, you may ommit these from your .env file diff --git a/.idea/leantime-oss.iml b/.idea/leantime-oss.iml index 690b94654..83f6ab48d 100644 --- a/.idea/leantime-oss.iml +++ b/.idea/leantime-oss.iml @@ -4,8 +4,6 @@ - - diff --git a/README.md b/README.md index 8eeedb37c..87aede871 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,21 @@ The dev environment provides a MySQL server, mail server, s3 server, and should Additionally, Xdebug is enabled, but you will have to modify your IDE key in the ``.dev/xdebug.ini`` file(or alternatively, on your IDE). You also need to have port 9003 temporarily open on your firewall so you can utilize it effectively. This is because connections from docker to the host will count as external inbound connections

+ +### Run Tests + +Static Analysis `make phpstan`
+Code Style `make test-code-style` (to fix code style automatically use `make fix-code-style`)
+Unit Tests `make unit-test`
+Acceptance Tests `make acceptance-test`
(requires docker) + +You can test individual acceptance test groups directly using:
+For api:
+`docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept run -g api --steps`
+For timesheets:
+`docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept run -g timesheet --steps`
+ + ### 🏗 Update ### #### Manual diff --git a/app/Core/Db/Db.php b/app/Core/Db/Db.php index 49fa3acdf..71fdbf3e7 100644 --- a/app/Core/Db/Db.php +++ b/app/Core/Db/Db.php @@ -70,8 +70,6 @@ public function __construct($connection = 'mysql') } catch (\PDOException $e) { Log::error("Can't connect to database"); - Log::error($e); - throw new \Exception($e); } } diff --git a/app/Core/Http/ApiRequest.php b/app/Core/Http/ApiRequest.php index 19c45a78d..d8249447b 100644 --- a/app/Core/Http/ApiRequest.php +++ b/app/Core/Http/ApiRequest.php @@ -57,7 +57,7 @@ public function getAuthorizationHeader(): string */ public function getAPIKey(): string { - return $this->headers->get('x-api-key'); + return $this->headers->get('x-api-key') ?? ''; } /** diff --git a/app/Core/Http/HttpKernel.php b/app/Core/Http/HttpKernel.php index 839a536e4..2415b2ab6 100644 --- a/app/Core/Http/HttpKernel.php +++ b/app/Core/Http/HttpKernel.php @@ -41,11 +41,8 @@ class HttpKernel extends Kernel \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Leantime\Core\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, - \Leantime\Core\Middleware\Auth::class, - \Leantime\Core\Middleware\ApiAuth::class, \Leantime\Core\Middleware\SetCacheHeaders::class, \Leantime\Core\Middleware\Localization::class, - \Leantime\Core\Middleware\CurrentProject::class, \Leantime\Core\Middleware\LoadPlugins::class, ]; @@ -57,7 +54,6 @@ class HttpKernel extends Kernel protected $middlewareGroups = [ 'web' => [ \Leantime\Core\Middleware\Auth::class, - \Leantime\Core\Middleware\Localization::class, \Leantime\Core\Middleware\CurrentProject::class, ], 'api' => [ @@ -65,7 +61,6 @@ class HttpKernel extends Kernel ], 'hx' => [ \Leantime\Core\Middleware\Auth::class, - \Leantime\Core\Middleware\Localization::class, \Leantime\Core\Middleware\CurrentProject::class, ], ]; @@ -103,6 +98,14 @@ protected function sendRequestThroughRouter($request) //Can savely assume events are available here. self::dispatch_event('request_started', ['request' => $request]); + if ($request instanceof ApiRequest) { + + array_splice($this->middleware, 5, 0, $this->middlewareGroups['api']); + + } else { + array_splice($this->middleware, 5, 0, $this->middlewareGroups['web']); + } + //This filter only works for system plugins //Regular plugins are not available until after install verification $this->middleware = self::dispatch_filter('middleware', $this->middleware, ['request' => $request]); diff --git a/app/Core/Http/IncomingRequest.php b/app/Core/Http/IncomingRequest.php index 3d014c06a..19e213052 100644 --- a/app/Core/Http/IncomingRequest.php +++ b/app/Core/Http/IncomingRequest.php @@ -56,6 +56,8 @@ public static function capture() //static::$httpMethodParameterOverride = false; //parent::enableHttpMethodParameterOverride(); + //static::enableHttpMethodParameterOverride(); + $headers = collect(getallheaders()) ->mapWithKeys(fn ($val, $key) => [ strtolower($key) => match (true) { @@ -66,9 +68,11 @@ public static function capture() ]) ->all(); + $requestUriTest = strtolower($_SERVER['REQUEST_URI'] ?? ''); + $request = match (true) { isset($headers['hx-request']) => HtmxRequest::createFromGlobals(), - isset($headers['x-api-key']) => ApiRequest::createFromGlobals(), + (isset($headers['x-api-key']) || str_starts_with($requestUriTest, '/api/jsonrpc')) => ApiRequest::createFromGlobals(), defined('LEAN_CLI') && LEAN_CLI => CliRequest::createFromGlobals(), default => parent::createFromGlobals(), }; diff --git a/app/Core/Middleware/ApiAuth.php b/app/Core/Middleware/ApiAuth.php index c3af63bc6..1536ddbe6 100644 --- a/app/Core/Middleware/ApiAuth.php +++ b/app/Core/Middleware/ApiAuth.php @@ -30,7 +30,11 @@ public function handle(IncomingRequest $request, Closure $next): Response self::dispatchEvent('before_api_request', ['application' => app()]); $apiKey = $request->getAPIKey(); - $apiUser = app()->make(ApiService::class)->getAPIKeyUser($apiKey); + $apiUser = false; + + if ($apiKey !== null) { + $apiUser = app()->make(ApiService::class)->getAPIKeyUser($apiKey); + } if (! $apiUser) { return new Response(json_encode(['error' => 'Invalid API Key']), 401); diff --git a/app/Domain/Api/Controllers/Jsonrpc.php b/app/Domain/Api/Controllers/Jsonrpc.php index 7b0627ca4..afc478a04 100644 --- a/app/Domain/Api/Controllers/Jsonrpc.php +++ b/app/Domain/Api/Controllers/Jsonrpc.php @@ -26,8 +26,9 @@ class Jsonrpc extends Controller */ public function init(): void { - if ($_SERVER['REQUEST_METHOD'] === 'POST' && empty($_POST)) { - $this->json_data = json_decode(file_get_contents('php://input'), JSON_OBJECT_AS_ARRAY); + if ($this->incomingRequest->server('REQUEST_METHOD') === 'POST' && empty($_POST)) { + $bodyContent = json_decode($this->incomingRequest->getContent(), JSON_OBJECT_AS_ARRAY); + $this->json_data = $bodyContent ?? ''; } } @@ -41,7 +42,7 @@ public function init(): void */ public function post(array $params): Response { - if (empty($params)) { + if (empty($params) || isset($params['act'])) { $params = $this->json_data; } @@ -129,8 +130,11 @@ private function executeApiRequest(array $params): Response * @see https://jsonrpc.org/specification#batch */ if (array_keys($params) == range(0, count($params) - 1)) { + return $this->tpl->displayJson(array_map( - fn ($requestParams) => json_decode($this->executeApiRequest($requestParams)->getContent()), + function ($requestParams) { + return json_decode($this->executeApiRequest($requestParams)->getContent()); + }, $params )); } diff --git a/app/Domain/Comments/Repositories/Comments.php b/app/Domain/Comments/Repositories/Comments.php index e69982faf..17c105036 100644 --- a/app/Domain/Comments/Repositories/Comments.php +++ b/app/Domain/Comments/Repositories/Comments.php @@ -204,16 +204,19 @@ public function getAllAccountComments(?int $projectId, ?int $moduleId): array|fa comment.moduleId, comment.userId, comment.commentParent, - comment.status + comment.status, + zp_projects.id AS projectId FROM zp_comment as comment LEFT JOIN zp_tickets ON comment.moduleId = zp_tickets.id LEFT JOIN zp_canvas_items ON comment.moduleId = zp_tickets.id LEFT JOIN zp_canvas ON zp_canvas.id = zp_canvas_items.canvasId LEFT JOIN zp_projects ON zp_canvas.projectId = zp_projects.id OR zp_tickets.projectId = zp_projects.id - WHERE zp_projects.id IN (SELECT projectId FROM zp_relationuserproject WHERE zp_relationuserproject.userId = :userId) + WHERE + + (zp_projects.id IN (SELECT projectId FROM zp_relationuserproject WHERE zp_relationuserproject.userId = :userId) OR zp_projects.psettings = 'all' OR (zp_projects.psettings = 'client' AND zp_projects.clientId = :clientId) - OR (:requesterRole = 'admin' OR :requesterRole = 'manager') "; + OR (:requesterRole = 'admin' OR :requesterRole = 'manager')) "; if (isset($projectId) && $projectId > 0) { $sql .= ' AND (zp_projects.id = :projectId)'; diff --git a/app/Language/de-DE.ini b/app/Language/de-DE.ini index 6c114904a..3096db3d7 100644 --- a/app/Language/de-DE.ini +++ b/app/Language/de-DE.ini @@ -2402,22 +2402,27 @@ menu.customfields_premium=" Custom Fields menu.whiteboards_premium=" Whiteboards Premium" #3.0.7= -input.placeholders.jobtitle="Job Title" - -headlines.your_account="Your Account" -text.welcome_to_leantime_2="Hi There, 👋
We're excited to see you! We just need a little more information to get your account ready for you." -text.things_get_organized="Things are about to get more organized!" -text.project_hub_intro="This is your project hub. All the projects you are currently assigned to or have favorited are here." -text.no_favorites="You don't have any favorites. 😿" -text.all_assigned_projects="🗂️ All Assigned Projects" -text.get_organized_with_projects="We're excited to show you around and get your work organized.

Leantime manages your work in \"projects\". The easiest way to define a project is to think about something you would like to accomplish. This can include metrics you'd like to reach, a client project or something you would like to create. Projects are then broken down into Goals with metrics and Milestones.

" -label.start_with_project_title="Let's start by giving your project a title" -headlines.make_it_happen="Make it happen" - -text.structured_project_thinking="Accomplishing your goals and staying focused requires you to define a foundation for your project. All too often we just 'brain dump' the latest priorities without questioning whether they wiil move the needle for us. Let's ask ourselves a few simple questions to get a better understanding of what we are working towards." -label.what_are_you_trying_to_accomplish="What are you trying to accomplish?" -label.how_does_the_world_look_like="What does the world, your business or your customers journey look like when you're done?" -label.why_is_this_important="Why is this important?" +input.placeholders.jobtitle="Jobtitel" + +headlines.your_account="Dein Account" +text.welcome_to_leantime_2="Hallo, 👋
Wir freuen uns, Dich zu sehen! Wir brauchen nur noch ein paar Informationen, um Dein Konto für Dich vorzubereiten." +text.things_get_organized="Die Dinge werden bald besser organisiert!" +text.project_hub_intro="Dies ist Dein Projekt-Hub. Alle Projekte, die Dir derzeit zugewiesen sind oder die Du als Favoriten markiert hast, findest Du hier." +text.no_favorites="Du hast keine Favoriten. 😿" +text.all_assigned_projects="🗂️ Alle zugewiesenen Projekte" +text.get_organized_with_projects="Wir freuen uns, Dir alles zu zeigen und Deine Arbeit zu organisieren.

Leantime verwaltet Deine Arbeit in \"Projekten\". Der einfachste Weg, ein Projekt zu definieren, besteht darin, über etwas nachzudenken, das Du erreichen möchtest. Dies kann Kennzahlen umfassen, die Du erreichen möchtest, ein Kundenprojekt oder etwas, das Du erstellen möchtest. Projekte werden dann in Ziele mit Kennzahlen und Meilensteinen unterteilt.

" +label.start_with_project_title="Beginnen wir damit, Deinem Projekt einen Titel zu geben" +headlines.make_it_happen="Mach es wahr" + +text.structured_project_thinking="Um Deine Ziele zu erreichen und konzentriert zu bleiben, musst du eine Grundlage für Ihr Projekt definieren. Allzu oft werfen wir die neuesten Prioritäten einfach aus dem Kopf, ohne uns zu fragen, ob sie uns weiterbringen. Stellen wir uns ein paar einfache Fragen, um besser zu verstehen, worauf wir hinarbeiten" +label.what_are_you_trying_to_accomplish="Was versuchst Du zu erreichen?" +label.how_does_the_world_look_like="Wie sieht die Welt, Dein Unternehmen oder die Customer Journey aus, wenn Du fertig bist?" +label.why_is_this_important="Warum ist dies wichtig?" + +#3.0.8= +text.ical_title="Leantime Kalender" +text.ical.todo_is_due="Eine Leantime Aufgabe ist bald fällig." +text.ical.todo_start_alert="Eine Leantime Aufgabe ist für die baldige Bearbeitung angesetzt" #3.0.8= text.ical_title="Leantime Calendar" diff --git a/app/Language/en-US.ini b/app/Language/en-US.ini index f680441b3..a1816b08a 100644 --- a/app/Language/en-US.ini +++ b/app/Language/en-US.ini @@ -2584,3 +2584,4 @@ widget.descriptions.implementation_intentions = "Daily Intentions are a strategy titles.account_details = "😀 Setting Account Details" titles.determine_visual_experience = "👀 Determining A Visual Experience" text.choose_a_theme_and_font_easy_to_read = "You can choose a theme and font that is easy on your brain." + diff --git a/codeception.yml b/codeception.yml index 54a4fb141..996662da7 100644 --- a/codeception.yml +++ b/codeception.yml @@ -5,8 +5,6 @@ paths: output: tests/_output data: tests/Support/Data support: tests/Support - envs: tests/_envs - log: tests/_log actor_suffix: Tester extensions: enabled: @@ -21,5 +19,3 @@ coverage: include: - app/* -params: - - tests/_envs/.env.test diff --git a/makefile b/makefile index 01d2e1859..82f7de964 100644 --- a/makefile +++ b/makefile @@ -62,10 +62,10 @@ package: clean build # Removing unneeded items for release rm -rf $(TARGET_DIR)/public/dist/images/Screenshots - # Removing js directories + # Removing javascript directories find $(TARGET_DIR)/app/Domain/ -depth -maxdepth 2 -name "js" -exec rm -rf {} \; - # Removing un-compiled js files + # Removing un-compiled javascript files find $(TARGET_DIR)/public/dist/js/ -depth -mindepth 1 ! -name "*compiled*" -exec rm -rf {} \; #create zip files @@ -107,12 +107,19 @@ run-dev: build-dev acceptance-test: build-dev docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml up --detach --build --remove-orphans - docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept run Acceptance -vvv + docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept clean + docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept build + docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept run Acceptance --steps unit-test: build-dev docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml up --detach --build --remove-orphans docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept build - docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept run Unit -vv + docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept run Unit --steps + +api-test: build-dev + docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml up --detach --build --remove-orphans + docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept build + docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml exec leantime-dev php vendor/bin/codecept run Api --steps acceptance-test-ci: build-dev docker compose --file .dev/docker-compose.yaml --file .dev/docker-compose.tests.yaml up --detach --build --remove-orphans @@ -148,4 +155,3 @@ clear-cache: rm -rf ./storage/framework/views/*.php .PHONY: install-deps build-js build package clean run-dev - diff --git a/tests/Acceptance.suite.yml b/tests/Acceptance.suite.yml index 8e9ddab13..e4f68436e 100644 --- a/tests/Acceptance.suite.yml +++ b/tests/Acceptance.suite.yml @@ -8,6 +8,9 @@ bootstrap: bootstrap.php modules: enabled: - \Tests\Support\Helper\Acceptance + - REST: + url: 'https://leantime-dev' + depends: PhpBrowser - WebDriver: url: 'https://leantime-dev' host: selenium @@ -19,10 +22,9 @@ modules: acceptInsecureCerts: true goog:chromeOptions: args: [ "--headless" ] - - Db: dsn: "mysql:host=db;port=3306;dbname=leantime_test" - user: leantime + user: root password: leantime populate: true cleanup: true diff --git a/tests/Acceptance/API/ApiCest.php b/tests/Acceptance/API/ApiCest.php new file mode 100644 index 000000000..7db1e151c --- /dev/null +++ b/tests/Acceptance/API/ApiCest.php @@ -0,0 +1,149 @@ +loginPage = $loginPage; + $this->installPage = $installPage; + + // Ensure database is installed before running API tests + $this->installPage->install( + 'test@leantime.io', + 'test', + 'John', + 'Smith', + 'Smith & Co' + ); + } + + #[Group('api')] + #[Depends('Acceptance\LoginCest:loginSuccessfully')] + public function createAPIKey(AcceptanceTester $I) + { + + $this->loginPage->login('test@leantime.io', 'test'); + + // Generate API key if not exists + $I->amOnPage('setting/editCompanySettings#/api/newApiKey'); + $I->waitForElementVisible('#firstname', 120); + + $I->fillField(['id' => 'firstname'], 'APIUser'); + $I->selectOption(['id' => 'role'], 'Administrator'); + $I->checkOption('Leantime Onboarding'); + $I->clickWithRetry('#save'); + + $I->waitForElement('#apiKey'); + + $this->apiKey = $I->grabValueFrom('#apiKey'); + + $I->resetCookie('leantime_session', []); + $I->deleteSessionSnapshot('leantime_session'); + } + + #[Group('api')] + #[Depends('createAPIKey')] + public function testJsonRpcEndpoint(AcceptanceTester $I) + { + + $I->haveHttpHeader('Content-Type', 'application/json'); + $I->haveHttpHeader('x-api-key', $this->apiKey); + + $I->sendPost('/api/jsonrpc', [ + 'jsonrpc' => '2.0', + 'method' => 'leantime.rpc.Comments.pollComments', + 'params' => ['projectId' => 1], + 'id' => 1, + ]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + $I->seeResponseMatchesJsonType([ + 'jsonrpc' => 'string', + 'result' => 'array', + 'id' => 'string', + ]); + } + + #[Group('api')] + #[Depends('createAPIKey')] + public function testInvalidJsonRpcRequest(AcceptanceTester $I) + { + $I->haveHttpHeader('Content-Type', 'application/json'); + $I->haveHttpHeader('x-api-key', $this->apiKey); + + $I->sendPost('/api/jsonrpc', [ + 'jsonrpc' => '2.0', + 'method' => 'invalid.method', + 'params' => ['projectId' => 1], + 'id' => 1, + ]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + $I->seeResponseMatchesJsonType([ + 'jsonrpc' => 'string', + 'error' => [ + 'code' => 'integer', + 'message' => 'string', + 'data' => 'string', + ], + 'id' => 'integer', + ]); + + } + + #[Group('api')] + #[Depends('createAPIKey')] + public function testValidReturnId(AcceptanceTester $I) + { + $I->haveHttpHeader('Content-Type', 'application/json'); + $I->haveHttpHeader('x-api-key', $this->apiKey); + + $I->sendPost('/api/jsonrpc', [ + 'jsonrpc' => '2.0', + 'method' => 'leantime.rpc.Comments.pollComments', + 'params' => ['projectId' => 1], + 'id' => 123, + ]); + + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson([ + 'jsonrpc' => '2.0', + 'method' => 'leantime.rpc.Comments.pollComments', + 'params' => ['projectId' => 1], + 'id' => 'integer', + ]); + + } + + #[Group('api')] + #[Depends('createAPIKey')] + public function testMissingApiKey(AcceptanceTester $I) + { + $I->haveHttpHeader('Content-Type', 'application/json'); + + $I->sendPost('/api/jsonrpc', [ + 'jsonrpc' => '2.0', + 'method' => 'leantime.rpc.Comments.pollComments', + 'params' => ['projectId' => 1], + 'id' => 1, + ]); + + $I->seeResponseCodeIs(401); + } +} diff --git a/tests/Acceptance/InstallCest.php b/tests/Acceptance/InstallCest.php index 83cb87096..4fd239c1f 100644 --- a/tests/Acceptance/InstallCest.php +++ b/tests/Acceptance/InstallCest.php @@ -11,7 +11,7 @@ class InstallCest { public function _before(AcceptanceTester $I) {} - #[Group('install')] + #[Group('install, api')] public function installPageWorks(AcceptanceTester $I): void { $I->amOnPage('/install'); @@ -20,7 +20,7 @@ public function installPageWorks(AcceptanceTester $I): void $I->see('Install'); } - #[Group('install')] + #[Group('install, api')] #[Depends('installPageWorks')] public function createDBSuccessfully(AcceptanceTester $I, Install $installPage): void { diff --git a/tests/Acceptance/bootstrap.php b/tests/Acceptance/bootstrap.php index 325fb0dc0..62303a38b 100644 --- a/tests/Acceptance/bootstrap.php +++ b/tests/Acceptance/bootstrap.php @@ -11,7 +11,6 @@ } if (! file_exists($composer = __DIR__.'/../../vendor/autoload.php')) { - dd($composer); throw new RuntimeException('Please run "make build-dev" to run tests.'); } diff --git a/tests/Support/ApiTester.php b/tests/Support/ApiTester.php new file mode 100644 index 000000000..ad0e77b24 --- /dev/null +++ b/tests/Support/ApiTester.php @@ -0,0 +1,22 @@ +app = require dirname(__DIR__, 2).'/bootstrap.php'; if (! defined('BASE_URL')) { - define('BASE_URL', 'http://localhost'); + define('BASE_URL', 'https://leantime-dev'); } } diff --git a/tests/Support/Helper/Api.php b/tests/Support/Helper/Api.php new file mode 100644 index 000000000..cfcbf1878 --- /dev/null +++ b/tests/Support/Helper/Api.php @@ -0,0 +1,27 @@ +app = require dirname(__DIR__, 2).'/bootstrap.php'; + + } + + public function getApplication(): Application + { + return $this->app; + } + + public function haveHttpHeader($header, $value) + { + $this->getModule('REST')->haveHttpHeader($header, $value); + } +} diff --git a/tests/Unit/app/Domain/Api/Controllers/JsonrpcTest.php b/tests/Unit/app/Domain/Api/Controllers/JsonrpcTest.php new file mode 100644 index 000000000..7ae5b2a3f --- /dev/null +++ b/tests/Unit/app/Domain/Api/Controllers/JsonrpcTest.php @@ -0,0 +1,138 @@ +app = new Application(APP_ROOT); + $this->app->boot(); + $this->app['view'] = $this->createMock(\Illuminate\View\Factory::class); + $this->app['session'] = $this->createMock(\Illuminate\Session\SessionManager::class); + $this->app->bootstrapWith([LoadConfig::class, SetRequestForConsole::class]); + + $this->template = $this->createMock(Template::class); + $this->template->method('displayJson')->willReturn(response()->json([])); + $language = $this->createMock(Language::class); + $this->controller = new Jsonrpc($this->app['request'], $this->template, $language); + $_SERVER['REQUEST_METHOD'] = 'post'; + } + + public function testMethodStringParsing() + { + $params = [ + 'method' => 'leantime.rpc.Comments.pollComments', + 'params' => ['projectId' => 1], + 'id' => 1, + 'jsonrpc' => '2.0', + ]; + + $this->template->expects($this->once()) + ->method('displayJson') + ->willReturnCallback(function ($response) { + $this->assertArrayHasKey('jsonrpc', $response); + $this->assertEquals('2.0', $response['jsonrpc']); + + return response()->json($response); + }); + + $this->controller->post($params); + } + + public function testInvalidMethodString() + { + $params = [ + 'method' => 'invalid.method.string', + 'params' => ['projectId' => 1], + 'id' => 1, + 'jsonrpc' => '2.0', + ]; + + $this->template->expects($this->once()) + ->method('displayJson') + ->willReturnCallback(function ($response) { + $this->assertArrayHasKey('error', $response); + $this->assertEquals(-32602, $response['error']['code']); + + return response()->json($response); + }); + + $this->controller->post($params); + } + + public function testMissingJsonRpcVersion() + { + $params = [ + 'method' => 'leantime.rpc.Comments.pollComments', + 'params' => ['projectId' => 1], + 'id' => 1, + ]; + + $this->template->expects($this->once()) + ->method('displayJson') + ->willReturnCallback(function ($response) { + $this->assertArrayHasKey('error', $response); + $this->assertEquals(-32600, $response['error']['code']); + + return response()->json($response); + }); + + $this->controller->post($params); + } + + public function testBatchRequest() + { + $params = [ + [ + 'method' => 'leantime.rpc.Comments.pollComments', + 'params' => ['projectId' => 1], + 'id' => 1, + 'jsonrpc' => '2.0', + ], + [ + 'method' => 'leantime.rpc.Comments.pollComments', + 'params' => ['projectId' => 2], + 'id' => 2, + 'jsonrpc' => '2.0', + ], + ]; + + //Each batched call will call displayJson once and then the final invokation will combine the results of the + //first 2 invocations to an array of 2 results + $countInvocations = 0; + $this->template->method('displayJson') + ->willReturnCallback(function ($response) use (&$countInvocations) { + $countInvocations++; + $this->assertIsArray($response); + + if ($countInvocations < 3) { + //Each individual invocation will have the regular jsonrpc response which gets mapped to the main + // response + $this->assertCount(3, $response); + } + + if ($countInvocations == 3) { + //The last invocation has 2 array items each being the result set of the jsonrpc call + $this->assertCount(2, $response); + } + + return response()->json($response); + }); + + $this->controller->post($params); + } +} diff --git a/tests/_envs/.env.test b/tests/_envs/.env.test index 7195bd083..835bb1ef9 100644 --- a/tests/_envs/.env.test +++ b/tests/_envs/.env.test @@ -1,7 +1,7 @@ APP_ENV = testing APP_DEBUG = true APP_KEY = base64:testing123testing123testing123testing123= -APP_URL = http://localhost +APP_URL = https://leantime-dev LOG_CHANNEL = stack LOG_DEPRECATIONS_CHANNEL = null @@ -13,3 +13,9 @@ FILESYSTEM_DISK = local QUEUE_CONNECTION = sync SESSION_DRIVER = array SESSION_LIFETIME = 120 + +LEAN_DB_HOST='db' # Database host +LEAN_DB_USER='root' # Database username +LEAN_DB_PASSWORD='test' # Database password +LEAN_DB_DATABASE='leantime_test' # Database name +LEAN_DB_PORT='3306' # Database port diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 7358f65b9..736388711 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,9 +2,9 @@ require_once __DIR__.'/../vendor/autoload.php'; -define('ROOT', realpath(__DIR__.'/..')); -define('APP_ROOT', realpath(__DIR__.'/..')); -define('BASE_URL', 'http://localhost'); +//define('ROOT', realpath(__DIR__.'/..')); +//define('APP_ROOT', realpath(__DIR__.'/..')); +//define('BASE_URL', 'http://localhost'); $app = require __DIR__.'/../bootstrap/app.php'; $app->make(\Leantime\Core\Console\ConsoleKernel::class)->bootstrap();