Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
742e721281
11
.env.sample
11
.env.sample
@ -184,4 +184,13 @@ CORS_DEBUG=true
|
||||
|
||||
APP_SIGNATURE =
|
||||
TOKENTIMEOUT =
|
||||
JWT_SECRET =
|
||||
JWT_SECRET =
|
||||
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# OTP Configuration
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
DEFAULT_OTP = ''
|
||||
IS_SEND_EMAIL_OTP =
|
||||
IS_SEND_SMS_OTP =
|
||||
@ -1288,6 +1288,8 @@ class EmployeeRestController extends AdminController
|
||||
|
||||
|
||||
// Execute query
|
||||
|
||||
$builder->orderBy('files.id', 'desc');
|
||||
$data = $builder->get()->getResultArray();
|
||||
|
||||
if (!empty($data)) {
|
||||
|
||||
@ -158,10 +158,17 @@ class RestAuthenticationController extends AdminController
|
||||
}
|
||||
|
||||
$otp = random_int(100000, 999999);
|
||||
if($mobile_number == '9442741776')
|
||||
{
|
||||
$default_otp = env('DEFAULT_OTP') ?? '';
|
||||
$is_send_sms_otp = filter_var(env('IS_SEND_SMS_OTP') ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if(!empty($default_otp)){
|
||||
$otp = $default_otp;
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: Sending default otp");
|
||||
}
|
||||
|
||||
if($mobile_number == '9442741776') {
|
||||
$otp = '123456';
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: setting 123456 for IOS APP TESTING = " . json_encode($employeeData));
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: setting 123456 for IOS APP TESTING = " . json_encode($employeeData));
|
||||
}
|
||||
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: Final employee data to verify = " . json_encode($employeeData));
|
||||
@ -211,13 +218,13 @@ class RestAuthenticationController extends AdminController
|
||||
}
|
||||
|
||||
//skip sms for live test no
|
||||
if($mobile_number == '9442741776')
|
||||
{
|
||||
|
||||
if($mobile_number == '9442741776' || $is_send_sms_otp == false) {
|
||||
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: skip SMS sent for IOS APP TESTING = " . ($mobile_number));
|
||||
$result = ['user_verification' => true ,'message' => "Verified Successfully" ];
|
||||
return $this->respond(['status' => 'success','code' => 200,'data' => $result],200);
|
||||
$result = ['user_verification' => true ,'message' => "Verified Successfully" ];
|
||||
return $this->respond(['status' => 'success','code' => 200,'data' => $result, 'is_sms_send' => false],200);
|
||||
}
|
||||
|
||||
//send sms
|
||||
$SMSResult = sendOtpSms($mobile_number, $otp);
|
||||
if ($SMSResult['status'] == 'success')
|
||||
@ -276,6 +283,14 @@ class RestAuthenticationController extends AdminController
|
||||
|
||||
$otp = random_int(100000, 999999);
|
||||
|
||||
$default_otp = env('DEFAULT_OTP') ?? '';
|
||||
$is_send_email_otp = filter_var(env('IS_SEND_EMAIL_OTP') ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if(!empty($default_otp)){
|
||||
$otp = $default_otp;
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: Sending default otp");
|
||||
}
|
||||
|
||||
//only retail policy
|
||||
if(empty($empdata)){
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithEmailId: No employee data found both PRE & POST");
|
||||
@ -387,6 +402,11 @@ class RestAuthenticationController extends AdminController
|
||||
'mail_type' => 'otp_mail',
|
||||
];
|
||||
|
||||
if($is_send_email_otp == false){
|
||||
$result = ['user_verification' => true, 'message' => "Verified Successfully"];
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => $result, 'is_email_send' => false], 200);
|
||||
}
|
||||
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithEmailId: Sending OTP email to " . $employeeData['email_corporate']);
|
||||
$res = $this->sendEmailOtp($employeeData['email_corporate'], $otp, $common);
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithEmailId: Mail response = " . $res);
|
||||
@ -596,6 +616,13 @@ class RestAuthenticationController extends AdminController
|
||||
|
||||
$otp = random_int(100000, 999999);
|
||||
|
||||
$default_otp = env('DEFAULT_OTP') ?? '';
|
||||
$is_send_sms_otp = filter_var(env('IS_SEND_SMS_OTP') ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if(!empty($default_otp)){
|
||||
$otp = $default_otp;
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: Sending default otp");
|
||||
}
|
||||
|
||||
if ($HrData)
|
||||
{
|
||||
@ -617,6 +644,11 @@ class RestAuthenticationController extends AdminController
|
||||
$data->otp = $otp;
|
||||
$this->callThirdPartyAPI($data, 'updateHROTP');
|
||||
|
||||
if($is_send_sms_otp == false){
|
||||
$result = ['user_verification' => true, 'message' => "Verified Successfully"];
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => $result, 'is_sms_send' => false], 200);
|
||||
}
|
||||
|
||||
//send sms
|
||||
$SMSResult = sendOtpSms($mobile_number, $otp);
|
||||
if ($SMSResult['status'] == 'success')
|
||||
@ -653,6 +685,7 @@ class RestAuthenticationController extends AdminController
|
||||
|
||||
$data = $this->request->getJSON();
|
||||
|
||||
|
||||
$email = $data->email;
|
||||
|
||||
$HrData = $this->hrModel->where('email', $email)
|
||||
@ -661,7 +694,16 @@ class RestAuthenticationController extends AdminController
|
||||
->first();
|
||||
|
||||
$otp = random_int(100000, 999999);
|
||||
$data->otp = $otp;
|
||||
|
||||
$default_otp = env('DEFAULT_OTP') ?? '';
|
||||
$is_send_email_otp = filter_var(env('IS_SEND_EMAIL_OTP') ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if(!empty($default_otp)){
|
||||
$otp = $default_otp;
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: Sending default otp");
|
||||
}
|
||||
|
||||
$data->otp = $otp;
|
||||
|
||||
if ($HrData) {
|
||||
|
||||
@ -685,10 +727,16 @@ class RestAuthenticationController extends AdminController
|
||||
$update = $db->query($sql, [$otp, $email]);
|
||||
|
||||
if($update)
|
||||
{
|
||||
{
|
||||
|
||||
$data->otp = $otp;
|
||||
$this->callThirdPartyAPI($data, 'updateHROTP');
|
||||
|
||||
if($is_send_email_otp == false){
|
||||
$result = ['user_verification' => true, 'message' => "Verified Successfully"];
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => $result, 'is_email_send' => false], 200);
|
||||
}
|
||||
|
||||
$common = [
|
||||
'login_type' => 'HR login',
|
||||
'login_email' => $email,
|
||||
@ -1932,7 +1980,7 @@ class RestAuthenticationController extends AdminController
|
||||
}
|
||||
|
||||
$decoded = $result['decoded'];
|
||||
$userId = $decoded['id'] ?? null;
|
||||
$userId = $decoded['pre_hr_id'] ?? null;
|
||||
|
||||
if (!$userId) {
|
||||
return $this->respond([
|
||||
|
||||
@ -115,9 +115,19 @@ class AuthJWT implements FilterInterface
|
||||
}
|
||||
|
||||
// Refresh sliding expiration
|
||||
$model->update($id, [
|
||||
'token_time_out' => time() + getenv('TOKENTIMEOUT')
|
||||
]);
|
||||
$newExpiry = time() + getenv('TOKENTIMEOUT');
|
||||
|
||||
if (!isset($decoded['emp_code'])) {
|
||||
// HR user: refresh token_time_out across all branches (same mobile/email)
|
||||
// so switching branches doesn't cause an unexpected expiry
|
||||
$field = !empty($user['mobile']) ? 'mobile' : 'email';
|
||||
$model->where($field, $user[$field])
|
||||
->where('contact_type', 'client')
|
||||
->where('is_active', 1)
|
||||
->update(null, ['token_time_out' => $newExpiry]);
|
||||
} else {
|
||||
$model->update($id, ['token_time_out' => $newExpiry]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ class JWTToken
|
||||
$token = $jwtParts[0];
|
||||
|
||||
try {
|
||||
$decoded = JWT::decode($token, new Key("secret", 'HS512'));
|
||||
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS512'));
|
||||
return json_encode(['status' => true, 'message' => 'Token is valid', 'data' =>$decoded->data->id ]);
|
||||
} catch (ExpiredException $e) {
|
||||
return json_encode(['status' => false, 'message' => 'Token has expired']);
|
||||
@ -159,7 +159,7 @@ class JWTToken
|
||||
$jwtParts = explode(' ', $jwt);
|
||||
$token = $jwtParts[1];
|
||||
|
||||
$decoded = JWT::decode($token, new Key("secret", 'HS512'));
|
||||
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS512'));
|
||||
return $decoded->id;
|
||||
}
|
||||
|
||||
@ -170,7 +170,7 @@ class JWTToken
|
||||
$jwtParts = explode(' ', $jwt);
|
||||
$token = $jwtParts[1];
|
||||
|
||||
$decoded = JWT::decode($token, new Key("secret", 'HS512'));
|
||||
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS512'));
|
||||
return $decoded->role;
|
||||
}
|
||||
|
||||
@ -182,7 +182,7 @@ class JWTToken
|
||||
$jwtParts = explode(' ', $jwt);
|
||||
$token = $jwtParts[1];
|
||||
|
||||
$decoded = JWT::decode($token, new Key("secret", 'HS512'));
|
||||
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS512'));
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
|
||||
@ -238,6 +238,9 @@ input:checked + .slider:before {
|
||||
var message = (PrimaryKey === '') ? 'Client General Info Created successfully' : 'Client General Info Updated successfully';
|
||||
toastr.success(message, 'Success');
|
||||
$submitButton.prop('disabled', false);
|
||||
let client_name = res.data.client_name ? (' - ' + res.data.client_name) : '';
|
||||
$('#client_heading').text(client_name);
|
||||
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ body {
|
||||
<div class="card-body">
|
||||
<div class="row" style="padding-bottom: 10px;">
|
||||
<div class="col-6" style="align-self: center;">
|
||||
<h4 style="position: relative;">Client Onboarding <?php if(isset($client)) {echo ' - ' . $client['client_name']; } ?></h4>
|
||||
<h4 style="position: relative;">Client Onboarding <span id="client_heading"><?php if (isset($client)) { echo ' - ' . $client['client_name']; } ?></span> </h4>
|
||||
</div>
|
||||
<div class="col-6" style="text-align: right;">
|
||||
<a href="<?= base_url("client/list"); ?>"><i class="fas fa-arrow-left" style="font-size: 17px;"></i></a>
|
||||
|
||||
607
tests/unit/HRAuthenticationTest.php
Normal file
607
tests/unit/HRAuthenticationTest.php
Normal file
@ -0,0 +1,607 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Helpers\JWTToken;
|
||||
use App\Filters\AuthJWT;
|
||||
use Firebase\JWT\JWT;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
|
||||
class HRAuthenticationTest extends CIUnitTestCase
|
||||
{
|
||||
private string $jwtSecret = 'test-secret-key-for-unit-tests';
|
||||
private int $tokenTimeout = 1800; // 30 minutes
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
putenv("JWT_SECRET={$this->jwtSecret}");
|
||||
putenv("TOKENTIMEOUT={$this->tokenTimeout}");
|
||||
|
||||
$_ENV['JWT_SECRET'] = $this->jwtSecret;
|
||||
$_SERVER['JWT_SECRET'] = $this->jwtSecret;
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
putenv('JWT_SECRET');
|
||||
putenv('TOKENTIMEOUT');
|
||||
unset($_ENV['JWT_SECRET'], $_SERVER['JWT_SECRET']);
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private function createToken(array $payload, string $algo = 'HS512'): string
|
||||
{
|
||||
return JWT::encode($payload, $this->jwtSecret, $algo);
|
||||
}
|
||||
|
||||
private function bearerHeader(array $payload): string
|
||||
{
|
||||
return 'Bearer ' . $this->createToken($payload);
|
||||
}
|
||||
|
||||
private function createMockRequest(string $authHeader): IncomingRequest
|
||||
{
|
||||
$request = $this->createMock(IncomingRequest::class);
|
||||
$request->method('getHeaderLine')
|
||||
->with('Authorization')
|
||||
->willReturn($authHeader);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 1. validateJWT — Header Format Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public function testValidateJWT_RejectsEmptyHeader()
|
||||
{
|
||||
$result = JWTToken::validateJWT('');
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Invalid Authorization header', $result['message']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_RejectsMissingBearerPrefix()
|
||||
{
|
||||
$token = $this->createToken(['id' => 1]);
|
||||
|
||||
$result = JWTToken::validateJWT($token);
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Invalid Authorization header', $result['message']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_RejectsBasicAuthScheme()
|
||||
{
|
||||
$result = JWTToken::validateJWT('Basic dXNlcjpwYXNz');
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Invalid Authorization header', $result['message']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_RejectsBearerWithExtraSpaces()
|
||||
{
|
||||
$token = $this->createToken(['id' => 1]);
|
||||
|
||||
$result = JWTToken::validateJWT("Bearer $token");
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Invalid Authorization header', $result['message']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_BearerIsCaseSensitive()
|
||||
{
|
||||
$token = $this->createToken(['id' => 1]);
|
||||
|
||||
$result = JWTToken::validateJWT("bearer $token");
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Invalid Authorization header', $result['message']);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 2. validateJWT — JWT Structure Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public function testValidateJWT_RejectsMalformedJWT_TwoParts()
|
||||
{
|
||||
$result = JWTToken::validateJWT('Bearer header.payload');
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Malformed JWT', $result['message']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_RejectsMalformedJWT_FourParts()
|
||||
{
|
||||
$result = JWTToken::validateJWT('Bearer a.b.c.d');
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Malformed JWT', $result['message']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_RejectsMalformedJWT_SinglePart()
|
||||
{
|
||||
$result = JWTToken::validateJWT('Bearer notajwt');
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Malformed JWT', $result['message']);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 3. validateJWT — Algorithm Security
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public function testValidateJWT_RejectsNoneAlgorithm()
|
||||
{
|
||||
$header = base64_encode(json_encode(['alg' => 'none', 'typ' => 'JWT']));
|
||||
$payload = base64_encode(json_encode(['id' => 1]));
|
||||
$fakeToken = "$header.$payload.";
|
||||
|
||||
$result = JWTToken::validateJWT("Bearer $fakeToken");
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Invalid or unsupported JWT algorithm', $result['message']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_RejectsMissingAlgorithm()
|
||||
{
|
||||
$header = base64_encode(json_encode(['typ' => 'JWT']));
|
||||
$payload = base64_encode(json_encode(['id' => 1]));
|
||||
$fakeToken = "$header.$payload.fakesig";
|
||||
|
||||
$result = JWTToken::validateJWT("Bearer $fakeToken");
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Invalid or unsupported JWT algorithm', $result['message']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_RejectsHS256Algorithm()
|
||||
{
|
||||
$token = JWT::encode(['id' => 1], $this->jwtSecret, 'HS256');
|
||||
|
||||
$result = JWTToken::validateJWT("Bearer $token");
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Invalid or unsupported JWT algorithm', $result['message']);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 4. validateJWT — Signature Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public function testValidateJWT_RejectsTokenSignedWithWrongSecret()
|
||||
{
|
||||
$token = JWT::encode(['id' => 1, 'pre_hr_id' => 5], 'attacker-secret', 'HS512');
|
||||
|
||||
$result = JWTToken::validateJWT("Bearer $token");
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
$this->assertEquals('Invalid token signature', $result['message']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_RejectsTamperedPayload()
|
||||
{
|
||||
$token = $this->createToken(['id' => 1, 'pre_hr_id' => 5]);
|
||||
$parts = explode('.', $token);
|
||||
|
||||
// Attacker tampers payload to escalate to a different branch
|
||||
$parts[1] = rtrim(base64_encode(json_encode(['id' => 1, 'pre_hr_id' => 999])), '=');
|
||||
$tampered = implode('.', $parts);
|
||||
|
||||
$result = JWTToken::validateJWT("Bearer $tampered");
|
||||
|
||||
$this->assertFalse($result['status']);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 5. validateJWT — Valid Token Decoding
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public function testValidateJWT_AcceptsValidHRToken()
|
||||
{
|
||||
$payload = [
|
||||
'id' => 10,
|
||||
'pre_hr_id' => 5,
|
||||
'pre_client_id' => md5('1'),
|
||||
'pre_branch_id' => 1,
|
||||
'post_client_id' => md5('2'),
|
||||
'allowed_modules' => '["dashboard","reports"]',
|
||||
];
|
||||
|
||||
$result = JWTToken::validateJWT($this->bearerHeader($payload));
|
||||
|
||||
$this->assertTrue($result['status']);
|
||||
$this->assertArrayHasKey('decoded', $result);
|
||||
$this->assertEquals(5, $result['decoded']['pre_hr_id']);
|
||||
$this->assertEquals(10, $result['decoded']['id']);
|
||||
$this->assertEquals(1, $result['decoded']['pre_branch_id']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_AcceptsValidEmployeeToken()
|
||||
{
|
||||
$payload = [
|
||||
'id' => 1,
|
||||
'emp_code' => 'EMP001',
|
||||
'token_type' => 'pre',
|
||||
];
|
||||
|
||||
$result = JWTToken::validateJWT($this->bearerHeader($payload));
|
||||
|
||||
$this->assertTrue($result['status']);
|
||||
$this->assertEquals('EMP001', $result['decoded']['emp_code']);
|
||||
}
|
||||
|
||||
public function testValidateJWT_PreservesAllPayloadFields()
|
||||
{
|
||||
$payload = [
|
||||
'id' => 10,
|
||||
'pre_hr_id' => 5,
|
||||
'pre_client_id' => 'hashed_client',
|
||||
'pre_branch_id' => 3,
|
||||
'post_client_id' => 'hashed_post',
|
||||
'allowed_modules' => '["m1"]',
|
||||
];
|
||||
|
||||
$result = JWTToken::validateJWT($this->bearerHeader($payload));
|
||||
$decoded = $result['decoded'];
|
||||
|
||||
$this->assertEquals($payload['pre_hr_id'], $decoded['pre_hr_id']);
|
||||
$this->assertEquals($payload['pre_branch_id'], $decoded['pre_branch_id']);
|
||||
$this->assertEquals($payload['pre_client_id'], $decoded['pre_client_id']);
|
||||
$this->assertEquals($payload['post_client_id'], $decoded['post_client_id']);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 6. Token Differentiation (HR vs Employee)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public function testHRToken_DoesNotContainEmpCode()
|
||||
{
|
||||
$hrPayload = ['id' => 10, 'pre_hr_id' => 5, 'pre_branch_id' => 1];
|
||||
|
||||
$result = JWTToken::validateJWT($this->bearerHeader($hrPayload));
|
||||
|
||||
$this->assertTrue($result['status']);
|
||||
$this->assertArrayNotHasKey('emp_code', $result['decoded']);
|
||||
}
|
||||
|
||||
public function testEmployeeToken_ContainsEmpCode()
|
||||
{
|
||||
$empPayload = ['id' => 1, 'emp_code' => 'EMP001'];
|
||||
|
||||
$result = JWTToken::validateJWT($this->bearerHeader($empPayload));
|
||||
|
||||
$this->assertTrue($result['status']);
|
||||
$this->assertArrayHasKey('emp_code', $result['decoded']);
|
||||
}
|
||||
|
||||
public function testFilterRoutesHRToLevelContactModel()
|
||||
{
|
||||
$decoded = ['id' => 10, 'pre_hr_id' => 5, 'pre_branch_id' => 1];
|
||||
|
||||
$this->assertFalse(isset($decoded['emp_code']), 'HR token should NOT have emp_code');
|
||||
$id = $decoded['pre_hr_id'] ?? null;
|
||||
$this->assertEquals(5, $id, 'HR lookup should use pre_hr_id');
|
||||
}
|
||||
|
||||
public function testFilterRoutesEmployeeToEmployeeModel()
|
||||
{
|
||||
$decoded = ['id' => 1, 'emp_code' => 'EMP001'];
|
||||
|
||||
$this->assertTrue(isset($decoded['emp_code']), 'Employee token should have emp_code');
|
||||
$id = $decoded['id'] ?? null;
|
||||
$this->assertEquals(1, $id, 'Employee lookup should use id');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 7. Multi-Branch Token Generation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public function testEachBranchGetsUniqueToken()
|
||||
{
|
||||
$clientId = md5('42');
|
||||
$branches = [
|
||||
['id' => 10, 'pre_hr_id' => 5, 'pre_client_id' => $clientId, 'pre_branch_id' => 1],
|
||||
['id' => 11, 'pre_hr_id' => 6, 'pre_client_id' => $clientId, 'pre_branch_id' => 2],
|
||||
['id' => 12, 'pre_hr_id' => 7, 'pre_client_id' => $clientId, 'pre_branch_id' => 3],
|
||||
];
|
||||
|
||||
$tokens = [];
|
||||
foreach ($branches as $branch) {
|
||||
$token = $this->createToken($branch);
|
||||
$result = JWTToken::validateJWT("Bearer $token");
|
||||
|
||||
$this->assertTrue($result['status']);
|
||||
$this->assertEquals($clientId, $result['decoded']['pre_client_id']);
|
||||
$tokens[] = $token;
|
||||
}
|
||||
|
||||
$this->assertCount(3, array_unique($tokens), 'Each branch must produce a distinct token');
|
||||
}
|
||||
|
||||
public function testBranchTokensDecodeToCorrectBranchId()
|
||||
{
|
||||
$branchIds = [1, 2, 3];
|
||||
|
||||
foreach ($branchIds as $i => $branchId) {
|
||||
$payload = [
|
||||
'id' => 10 + $i,
|
||||
'pre_hr_id' => 5 + $i,
|
||||
'pre_branch_id' => $branchId,
|
||||
];
|
||||
|
||||
$result = JWTToken::validateJWT($this->bearerHeader($payload));
|
||||
|
||||
$this->assertTrue($result['status']);
|
||||
$this->assertEquals($branchId, $result['decoded']['pre_branch_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 8. Sliding Expiration Logic
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public function testTokenTimeoutEnvIsReadable()
|
||||
{
|
||||
$this->assertEquals($this->tokenTimeout, (int)getenv('TOKENTIMEOUT'));
|
||||
}
|
||||
|
||||
public function testNewExpiryIsInTheFuture()
|
||||
{
|
||||
$before = time();
|
||||
$newExpiry = time() + (int)getenv('TOKENTIMEOUT');
|
||||
|
||||
$this->assertGreaterThanOrEqual($before + $this->tokenTimeout, $newExpiry);
|
||||
}
|
||||
|
||||
public function testActiveTokenPassesExpiryCheck()
|
||||
{
|
||||
$tokenTimeOut = time() + 600;
|
||||
|
||||
$this->assertGreaterThan(time(), $tokenTimeOut, 'Active token_time_out must be in the future');
|
||||
}
|
||||
|
||||
public function testExpiredTokenFailsExpiryCheck()
|
||||
{
|
||||
$tokenTimeOut = time() - 1;
|
||||
|
||||
$this->assertLessThanOrEqual(time(), $tokenTimeOut, 'Expired token_time_out must be <= current time');
|
||||
}
|
||||
|
||||
public function testNullExpiryFailsExpiryCheck()
|
||||
{
|
||||
$tokenTimeOut = null;
|
||||
|
||||
$this->assertTrue($tokenTimeOut <= time(), 'null token_time_out should fail the expiry check');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 9. Multi-Branch Refresh Logic (the fix)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public function testBranchRefresh_UsesMobileWhenAvailable()
|
||||
{
|
||||
$user = ['mobile' => '9876543210', 'email' => 'hr@test.com'];
|
||||
|
||||
$field = !empty($user['mobile']) ? 'mobile' : 'email';
|
||||
|
||||
$this->assertEquals('mobile', $field);
|
||||
$this->assertEquals('9876543210', $user[$field]);
|
||||
}
|
||||
|
||||
public function testBranchRefresh_FallsBackToEmailWhenMobileEmpty()
|
||||
{
|
||||
$user = ['mobile' => '', 'email' => 'hr@test.com'];
|
||||
|
||||
$field = !empty($user['mobile']) ? 'mobile' : 'email';
|
||||
|
||||
$this->assertEquals('email', $field);
|
||||
$this->assertEquals('hr@test.com', $user[$field]);
|
||||
}
|
||||
|
||||
public function testBranchRefresh_FallsBackToEmailWhenMobileNull()
|
||||
{
|
||||
$user = ['mobile' => null, 'email' => 'hr@test.com'];
|
||||
|
||||
$field = !empty($user['mobile']) ? 'mobile' : 'email';
|
||||
|
||||
$this->assertEquals('email', $field);
|
||||
}
|
||||
|
||||
public function testBranchRefresh_HRUserTakesMultiBranchPath()
|
||||
{
|
||||
$decoded = ['id' => 10, 'pre_hr_id' => 5, 'pre_branch_id' => 1];
|
||||
|
||||
$isMultiBranch = !isset($decoded['emp_code']);
|
||||
|
||||
$this->assertTrue($isMultiBranch, 'HR user (no emp_code) should take the multi-branch refresh path');
|
||||
}
|
||||
|
||||
public function testBranchRefresh_EmployeeTakesSingleRowPath()
|
||||
{
|
||||
$decoded = ['id' => 1, 'emp_code' => 'EMP001'];
|
||||
|
||||
$isSingleRow = isset($decoded['emp_code']);
|
||||
|
||||
$this->assertTrue($isSingleRow, 'Employee (with emp_code) should take the single-row refresh path');
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates the branch-switching scenario to verify the refresh logic
|
||||
* targets all branches, not just the active one.
|
||||
*/
|
||||
public function testBranchRefresh_SimulateMultiBranchExpiryUpdate()
|
||||
{
|
||||
$sameMobile = '9876543210';
|
||||
|
||||
// Three level_contacts rows for the same HR person across branches
|
||||
$branchRows = [
|
||||
['id' => 5, 'mobile' => $sameMobile, 'email' => 'hr@test.com', 'contact_type' => 'client', 'is_active' => 1, 'token_time_out' => time() + 1800],
|
||||
['id' => 6, 'mobile' => $sameMobile, 'email' => 'hr@test.com', 'contact_type' => 'client', 'is_active' => 1, 'token_time_out' => time() + 1800],
|
||||
['id' => 7, 'mobile' => $sameMobile, 'email' => 'hr@test.com', 'contact_type' => 'client', 'is_active' => 1, 'token_time_out' => time() + 1800],
|
||||
];
|
||||
|
||||
// User is on Branch A (id=5). AuthJWT decoded token says pre_hr_id = 5.
|
||||
$currentUser = $branchRows[0];
|
||||
$decoded = ['id' => 10, 'pre_hr_id' => 5, 'pre_branch_id' => 1];
|
||||
|
||||
// The filter's multi-branch refresh logic:
|
||||
$field = !empty($currentUser['mobile']) ? 'mobile' : 'email';
|
||||
$newExpiry = time() + $this->tokenTimeout;
|
||||
|
||||
// Simulate: update all rows where $field matches (same as the WHERE clause in AuthJWT)
|
||||
$updatedRows = array_filter($branchRows, function ($row) use ($field, $currentUser) {
|
||||
return $row[$field] === $currentUser[$field]
|
||||
&& $row['contact_type'] === 'client'
|
||||
&& $row['is_active'] === 1;
|
||||
});
|
||||
|
||||
foreach ($updatedRows as &$row) {
|
||||
$row['token_time_out'] = $newExpiry;
|
||||
}
|
||||
unset($row);
|
||||
|
||||
// ALL three branches must have the new expiry
|
||||
$this->assertCount(3, $updatedRows, 'All three branch rows should be updated');
|
||||
foreach ($updatedRows as $row) {
|
||||
$this->assertEquals($newExpiry, $row['token_time_out']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contrast test: the old single-row logic would only update one branch.
|
||||
*/
|
||||
public function testBranchRefresh_OldLogicWouldLeaveOtherBranchesStale()
|
||||
{
|
||||
$loginTime = time();
|
||||
$timeout = $this->tokenTimeout;
|
||||
$sameMobile = '9876543210';
|
||||
|
||||
$branchRows = [
|
||||
['id' => 5, 'mobile' => $sameMobile, 'token_time_out' => $loginTime + $timeout],
|
||||
['id' => 6, 'mobile' => $sameMobile, 'token_time_out' => $loginTime + $timeout],
|
||||
['id' => 7, 'mobile' => $sameMobile, 'token_time_out' => $loginTime + $timeout],
|
||||
];
|
||||
|
||||
// Simulate 20 minutes of activity on Branch A only (old single-row logic)
|
||||
$simulatedNow = $loginTime + 1200; // 20 min later
|
||||
$branchRows[0]['token_time_out'] = $simulatedNow + $timeout; // only branch A refreshed
|
||||
|
||||
// Branch B and C still have the original login-time expiry
|
||||
$this->assertEquals($loginTime + $timeout, $branchRows[1]['token_time_out']);
|
||||
$this->assertEquals($loginTime + $timeout, $branchRows[2]['token_time_out']);
|
||||
|
||||
// After 31 minutes, Branch B and C would be expired
|
||||
$switchTime = $loginTime + 1860; // 31 min later
|
||||
$this->assertLessThanOrEqual($switchTime, $branchRows[1]['token_time_out'],
|
||||
'OLD logic: Branch B would have expired by the time user switches');
|
||||
$this->assertLessThanOrEqual($switchTime, $branchRows[2]['token_time_out'],
|
||||
'OLD logic: Branch C would have expired by the time user switches');
|
||||
|
||||
// But Branch A is still alive
|
||||
$this->assertGreaterThan($switchTime, $branchRows[0]['token_time_out'],
|
||||
'Branch A stays alive because it was actively refreshed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Proves the new logic keeps all branches alive.
|
||||
*/
|
||||
public function testBranchRefresh_NewLogicKeepsAllBranchesAlive()
|
||||
{
|
||||
$loginTime = time();
|
||||
$timeout = $this->tokenTimeout;
|
||||
$sameMobile = '9876543210';
|
||||
|
||||
$branchRows = [
|
||||
['id' => 5, 'mobile' => $sameMobile, 'token_time_out' => $loginTime + $timeout],
|
||||
['id' => 6, 'mobile' => $sameMobile, 'token_time_out' => $loginTime + $timeout],
|
||||
['id' => 7, 'mobile' => $sameMobile, 'token_time_out' => $loginTime + $timeout],
|
||||
];
|
||||
|
||||
// Simulate 20 minutes of activity on Branch A (new multi-branch logic)
|
||||
$simulatedNow = $loginTime + 1200;
|
||||
$newExpiry = $simulatedNow + $timeout;
|
||||
|
||||
// New logic refreshes ALL rows with same mobile
|
||||
foreach ($branchRows as &$row) {
|
||||
if ($row['mobile'] === $sameMobile) {
|
||||
$row['token_time_out'] = $newExpiry;
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
// After 31 minutes from login, ALL branches are still alive
|
||||
$switchTime = $loginTime + 1860;
|
||||
|
||||
foreach ($branchRows as $i => $row) {
|
||||
$this->assertGreaterThan($switchTime, $row['token_time_out'],
|
||||
"NEW logic: Branch id={$row['id']} should still be alive after switching");
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 10. AuthJWT Filter — Rejection Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private function createFilter(): AuthJWT
|
||||
{
|
||||
// AuthJWT has require_once('../vendor/autoload.php') which resolves
|
||||
// relative to CWD. In Apache CWD is public/, in PHPUnit it's the
|
||||
// project root. Temporarily switch CWD so the require resolves.
|
||||
$cwd = getcwd();
|
||||
chdir(PUBLICPATH);
|
||||
try {
|
||||
return new AuthJWT();
|
||||
} finally {
|
||||
chdir($cwd);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFilter_RejectsMissingAuthHeader()
|
||||
{
|
||||
$filter = $this->createFilter();
|
||||
$request = $this->createMockRequest('');
|
||||
|
||||
$result = $filter->before($request);
|
||||
|
||||
$this->assertNotTrue($result, 'Filter must reject requests without Authorization header');
|
||||
}
|
||||
|
||||
public function testFilter_RejectsInvalidJWT()
|
||||
{
|
||||
$filter = $this->createFilter();
|
||||
$request = $this->createMockRequest('Bearer not.a.valid-jwt');
|
||||
|
||||
$result = $filter->before($request);
|
||||
|
||||
$this->assertNotTrue($result, 'Filter must reject requests with an invalid JWT');
|
||||
}
|
||||
|
||||
public function testFilter_RejectsTokenWithoutId()
|
||||
{
|
||||
$token = $this->createToken(['name' => 'no-id-field']);
|
||||
$filter = $this->createFilter();
|
||||
$request = $this->createMockRequest("Bearer $token");
|
||||
|
||||
$result = $filter->before($request);
|
||||
|
||||
$this->assertNotTrue($result, 'Filter must reject tokens without an id field');
|
||||
}
|
||||
|
||||
public function testFilter_RejectsWrongAlgorithmToken()
|
||||
{
|
||||
$token = JWT::encode(['id' => 1], $this->jwtSecret, 'HS256');
|
||||
$filter = $this->createFilter();
|
||||
$request = $this->createMockRequest("Bearer $token");
|
||||
|
||||
$result = $filter->before($request);
|
||||
|
||||
$this->assertNotTrue($result, 'Filter must reject tokens using non-HS512 algorithms');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user