add SessionTimeout logic
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\BaseApiController;
|
||||||
|
use App\Http\Requests\Auth\SessionTimeoutCheckRequest;
|
||||||
|
use App\Http\Requests\Auth\SessionTimeoutConfigRequest;
|
||||||
|
use App\Http\Requests\Auth\SessionTimeoutPingRequest;
|
||||||
|
use App\Http\Resources\Auth\SessionTimeoutConfigResource;
|
||||||
|
use App\Http\Resources\Auth\SessionTimeoutStatusResource;
|
||||||
|
use App\Services\Auth\SessionTimeoutConfigService;
|
||||||
|
use App\Services\Auth\SessionTimeoutService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class SessionTimeoutController extends BaseApiController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private SessionTimeoutConfigService $configService,
|
||||||
|
private SessionTimeoutService $timeoutService
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function config(SessionTimeoutConfigRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$config = $this->configService->config();
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'config' => new SessionTimeoutConfigResource($config),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check(SessionTimeoutCheckRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$result = $this->timeoutService->checkTimeout();
|
||||||
|
|
||||||
|
if (($result['status'] ?? '') === 'expired') {
|
||||||
|
return $this->error(
|
||||||
|
$result['message'] ?? 'Session expired.',
|
||||||
|
Response::HTTP_UNAUTHORIZED,
|
||||||
|
(new SessionTimeoutStatusResource($result))->toArray($request)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'session' => new SessionTimeoutStatusResource($result),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ping(SessionTimeoutPingRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$result = $this->timeoutService->pingActivity();
|
||||||
|
|
||||||
|
if (($result['status'] ?? '') === 'expired') {
|
||||||
|
return $this->error(
|
||||||
|
$result['message'] ?? 'Session expired.',
|
||||||
|
Response::HTTP_UNAUTHORIZED,
|
||||||
|
(new SessionTimeoutStatusResource($result))->toArray($request)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'session' => new SessionTimeoutStatusResource($result),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Auth;
|
||||||
|
|
||||||
|
use App\Http\Requests\ApiFormRequest;
|
||||||
|
|
||||||
|
class SessionTimeoutCheckRequest extends ApiFormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->check();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Auth;
|
||||||
|
|
||||||
|
use App\Http\Requests\ApiFormRequest;
|
||||||
|
|
||||||
|
class SessionTimeoutConfigRequest extends ApiFormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->check();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Auth;
|
||||||
|
|
||||||
|
use App\Http\Requests\ApiFormRequest;
|
||||||
|
|
||||||
|
class SessionTimeoutPingRequest extends ApiFormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->check();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class SessionTimeoutConfigResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray($request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'timeout' => (int) ($this['timeout'] ?? 0),
|
||||||
|
'warning_time' => (int) ($this['warning_time'] ?? 0),
|
||||||
|
'check_interval' => (int) ($this['check_interval'] ?? 0),
|
||||||
|
'logout_url' => (string) ($this['logout_url'] ?? ''),
|
||||||
|
'keep_alive_url' => (string) ($this['keep_alive_url'] ?? ''),
|
||||||
|
'check_url' => (string) ($this['check_url'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class SessionTimeoutStatusResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray($request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'status' => (string) ($this['status'] ?? ''),
|
||||||
|
'time_remaining' => (int) ($this['time_remaining'] ?? 0),
|
||||||
|
'redirect' => $this['redirect'] ?? null,
|
||||||
|
'message' => $this['message'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Session\Store;
|
||||||
|
|
||||||
|
class SessionActivityService
|
||||||
|
{
|
||||||
|
public function __construct(private Store $session)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasLastActivity(): bool
|
||||||
|
{
|
||||||
|
return $this->session->has('last_activity');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastActivity(): ?int
|
||||||
|
{
|
||||||
|
$value = $this->session->get('last_activity');
|
||||||
|
return $value !== null ? (int) $value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLastActivity(int $timestamp): void
|
||||||
|
{
|
||||||
|
$this->session->put('last_activity', $timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearLastActivity(): void
|
||||||
|
{
|
||||||
|
$this->session->forget('last_activity');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invalidate(): void
|
||||||
|
{
|
||||||
|
$this->session->invalidate();
|
||||||
|
$this->session->regenerateToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Auth;
|
||||||
|
|
||||||
|
use App\Config\SessionTimeout;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
|
||||||
|
class SessionTimeoutConfigService
|
||||||
|
{
|
||||||
|
public function config(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'timeout' => SessionTimeout::TIMEOUT_DURATION,
|
||||||
|
'warning_time' => SessionTimeout::WARNING_THRESHOLD,
|
||||||
|
'check_interval' => SessionTimeout::CHECK_INTERVAL,
|
||||||
|
'logout_url' => URL::to('/auth/logout'),
|
||||||
|
'keep_alive_url' => route('session.timeout.ping'),
|
||||||
|
'check_url' => route('session.timeout.check'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Auth;
|
||||||
|
|
||||||
|
use App\Config\SessionTimeout;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
|
||||||
|
class SessionTimeoutService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private SessionActivityService $activityService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkTimeout(): array
|
||||||
|
{
|
||||||
|
if (!$this->activityService->hasLastActivity()) {
|
||||||
|
return $this->expireSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastActivity = (int) $this->activityService->getLastActivity();
|
||||||
|
$elapsed = time() - $lastActivity;
|
||||||
|
|
||||||
|
if ($elapsed > SessionTimeout::TIMEOUT_DURATION) {
|
||||||
|
return $this->expireSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($elapsed > SessionTimeout::WARNING_THRESHOLD) {
|
||||||
|
return [
|
||||||
|
'status' => 'warning',
|
||||||
|
'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'active',
|
||||||
|
'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pingActivity(): array
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!$this->activityService->hasLastActivity()
|
||||||
|
|| (time() - (int) $this->activityService->getLastActivity() > SessionTimeout::TIMEOUT_DURATION)
|
||||||
|
) {
|
||||||
|
return $this->expireSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->activityService->setLastActivity(time());
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'active',
|
||||||
|
'time_remaining' => SessionTimeout::TIMEOUT_DURATION,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function expireSession(): array
|
||||||
|
{
|
||||||
|
Log::info('Session expired due to inactivity.');
|
||||||
|
$this->activityService->clearLastActivity();
|
||||||
|
$this->activityService->invalidate();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'expired',
|
||||||
|
'redirect' => URL::to('/login'),
|
||||||
|
'message' => 'Your session has expired due to inactivity.',
|
||||||
|
'time_remaining' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -182,6 +182,11 @@ Route::prefix('v1')->group(function () {
|
|||||||
|
|
||||||
Route::middleware('auth:sanctum')->group(function () {
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
Route::get('stats', [StatsController::class, 'index']);
|
Route::get('stats', [StatsController::class, 'index']);
|
||||||
|
Route::prefix('session')->group(function () {
|
||||||
|
Route::get('timeout-config', [SessionTimeoutController::class, 'config'])->name('session.timeout.config');
|
||||||
|
Route::get('check-timeout', [SessionTimeoutController::class, 'check'])->name('session.timeout.check');
|
||||||
|
Route::post('ping', [SessionTimeoutController::class, 'ping'])->name('session.timeout.ping');
|
||||||
|
});
|
||||||
|
|
||||||
Route::prefix('whatsapp')->group(function () {
|
Route::prefix('whatsapp')->group(function () {
|
||||||
Route::get('links', [WhatsappController::class, 'index']);
|
Route::get('links', [WhatsappController::class, 'index']);
|
||||||
|
|||||||
-292
@@ -1,292 +0,0 @@
|
|||||||
Xdebug: [Step Debug] Could not connect to debugging client. Tried: 127.0.0.1:9003 (through xdebug.client_host/xdebug.client_port).
|
|
||||||
|
|
||||||
Xdebug: [Step Debug] Could not connect to debugging client. Tried: 127.0.0.1:9003 (through xdebug.client_host/xdebug.client_port).
|
|
||||||
|
|
||||||
PASS Tests\Unit\Models\WhatsappGroupLinkTest
|
|
||||||
✓ model metadata is converted 0.01s
|
|
||||||
✓ model class loads
|
|
||||||
|
|
||||||
PASS Tests\Unit\Models\WhatsappGroupMembershipTest
|
|
||||||
✓ model metadata is converted 0.01s
|
|
||||||
✓ model class loads
|
|
||||||
|
|
||||||
PASS Tests\Unit\Models\WhatsappInviteLogTest
|
|
||||||
✓ model metadata is converted
|
|
||||||
✓ model class loads
|
|
||||||
|
|
||||||
|
|
||||||
FAIL Tests\Unit\Services\Whatsapp\WhatsappContactServiceTest
|
|
||||||
⨯ list parent contacts returns primary and second 0.40s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PASS Tests\Unit\Services\Whatsapp\WhatsappContextServiceTest
|
|
||||||
✓ school year aliases generate expected values 0.28s
|
|
||||||
✓ has roster for year 0.28s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FAIL Tests\Unit\Services\Whatsapp\WhatsappInviteBundleServiceTest
|
|
||||||
⨯ bundle for parent returns links and emails 0.28s
|
|
||||||
✓ consolidate bundles merges by parent 0.28s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PASS Tests\Unit\Services\Whatsapp\WhatsappInviteNotificationServiceTest
|
|
||||||
✓ dispatch returns error when no listeners 0.29s
|
|
||||||
✓ dispatch triggers event when listener registered 0.28s
|
|
||||||
|
|
||||||
|
|
||||||
FAIL Tests\Unit\Services\Whatsapp\WhatsappInviteServiceTest
|
|
||||||
⨯ send returns success for parent mode 0.28s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PASS Tests\Unit\Services\Whatsapp\WhatsappLinkServiceTest
|
|
||||||
✓ upsert creates link 0.28s
|
|
||||||
✓ paginate filters by active 0.28s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PASS Tests\Unit\Services\Whatsapp\WhatsappMembershipServiceTest
|
|
||||||
✓ update membership saves primary 0.29s
|
|
||||||
✓ update membership returns error when table missing 0.29s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FAIL Tests\Feature\Api\V1\Whatsapp\WhatsappInviteControllerTest
|
|
||||||
⨯ parent contacts endpoint returns contacts 0.31s
|
|
||||||
⨯ parent contacts by class endpoint returns sections 0.29s
|
|
||||||
⨯ send invites returns success when listener registered 0.29s
|
|
||||||
⨯ send invites returns error when no listener 0.29s
|
|
||||||
⨯ membership update saves record 0.29s
|
|
||||||
✓ send invites validation rejects payload 0.31s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FAIL Tests\Feature\Api\V1\Whatsapp\WhatsappLinkControllerTest
|
|
||||||
✓ index returns paginated links 0.30s
|
|
||||||
✓ show returns single link 0.30s
|
|
||||||
⨯ store creates link 0.29s
|
|
||||||
⨯ update updates link 0.29s
|
|
||||||
✓ destroy deletes link 0.31s
|
|
||||||
✓ store validation rejects invalid payload 0.30s
|
|
||||||
✓ requires authentication 0.29s
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Unit\Services\Whatsapp\WhatsappContactServi… QueryException
|
|
||||||
SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: parents.secondparent_id (Connection: sqlite, Database: :memory:, SQL: insert into "parents" ("secondparent_firstname", "secondparent_lastname", "secondparent_email", "secondparent_phone", "firstparent_id", "school_year", "semester", "created_at", "updated_at") values (Parent, Two, parent2@example.com, 5555555555, 10, 2025-2026, Fall, 2026-03-10 20:29:52, 2026-03-10 20:29:52))
|
|
||||||
|
|
||||||
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
|
|
||||||
834▕ $exceptionType = $this->isUniqueConstraintError($e)
|
|
||||||
835▕ ? UniqueConstraintViolationException::class
|
|
||||||
836▕ : QueryException::class;
|
|
||||||
837▕
|
|
||||||
➜ 838▕ throw new $exceptionType(
|
|
||||||
839▕ $this->getNameWithReadWriteType(),
|
|
||||||
840▕ $query,
|
|
||||||
841▕ $this->prepareBindings($bindings),
|
|
||||||
842▕ $e,
|
|
||||||
|
|
||||||
[2m+8 vendor frames [22m
|
|
||||||
9 tests/Unit/Services/Whatsapp/WhatsappContactServiceTest.php:38
|
|
||||||
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Unit\Services\Whatsapp\WhatsappInviteBundle… QueryException
|
|
||||||
SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: parents.secondparent_id (Connection: sqlite, Database: :memory:, SQL: insert into "parents" ("secondparent_firstname", "secondparent_lastname", "secondparent_email", "secondparent_phone", "firstparent_id", "school_year", "semester", "created_at", "updated_at") values (Parent, Two, parent2@example.com, 5555555555, 10, 2025-2026, Fall, 2026-03-10 20:29:53, 2026-03-10 20:29:53))
|
|
||||||
|
|
||||||
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
|
|
||||||
834▕ $exceptionType = $this->isUniqueConstraintError($e)
|
|
||||||
835▕ ? UniqueConstraintViolationException::class
|
|
||||||
836▕ : QueryException::class;
|
|
||||||
837▕
|
|
||||||
➜ 838▕ throw new $exceptionType(
|
|
||||||
839▕ $this->getNameWithReadWriteType(),
|
|
||||||
840▕ $query,
|
|
||||||
841▕ $this->prepareBindings($bindings),
|
|
||||||
842▕ $e,
|
|
||||||
|
|
||||||
[2m+8 vendor frames [22m
|
|
||||||
9 tests/Unit/Services/Whatsapp/WhatsappInviteBundleServiceTest.php:37
|
|
||||||
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Unit\Services\Whatsapp\WhatsappInviteServic… QueryException
|
|
||||||
SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: parents.secondparent_id (Connection: sqlite, Database: :memory:, SQL: insert into "parents" ("secondparent_firstname", "secondparent_lastname", "secondparent_email", "secondparent_phone", "firstparent_id", "school_year", "semester", "created_at", "updated_at") values (Parent, Two, parent2@example.com, 5555555555, 10, 2025-2026, Fall, 2026-03-10 20:29:54, 2026-03-10 20:29:54))
|
|
||||||
|
|
||||||
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
|
|
||||||
834▕ $exceptionType = $this->isUniqueConstraintError($e)
|
|
||||||
835▕ ? UniqueConstraintViolationException::class
|
|
||||||
836▕ : QueryException::class;
|
|
||||||
837▕
|
|
||||||
➜ 838▕ throw new $exceptionType(
|
|
||||||
839▕ $this->getNameWithReadWriteType(),
|
|
||||||
840▕ $query,
|
|
||||||
841▕ $this->prepareBindings($bindings),
|
|
||||||
842▕ $e,
|
|
||||||
|
|
||||||
[2m+8 vendor frames [22m
|
|
||||||
9 tests/Unit/Services/Whatsapp/WhatsappInviteServiceTest.php:65
|
|
||||||
10 tests/Unit/Services/Whatsapp/WhatsappInviteServiceTest.php:21
|
|
||||||
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Feature\Api\V1\Whatsapp\WhatsappInviteContr… QueryException
|
|
||||||
SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: parents.secondparent_id (Connection: sqlite, Database: :memory:, SQL: insert into "parents" ("secondparent_firstname", "secondparent_lastname", "secondparent_email", "secondparent_phone", "firstparent_id", "school_year", "semester", "created_at", "updated_at") values (Parent, Two, parent2@example.com, 5555555555, 10, 2025-2026, Fall, 2026-03-10 20:29:56, 2026-03-10 20:29:56))
|
|
||||||
|
|
||||||
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
|
|
||||||
834▕ $exceptionType = $this->isUniqueConstraintError($e)
|
|
||||||
835▕ ? UniqueConstraintViolationException::class
|
|
||||||
836▕ : QueryException::class;
|
|
||||||
837▕
|
|
||||||
➜ 838▕ throw new $exceptionType(
|
|
||||||
839▕ $this->getNameWithReadWriteType(),
|
|
||||||
840▕ $query,
|
|
||||||
841▕ $this->prepareBindings($bindings),
|
|
||||||
842▕ $e,
|
|
||||||
|
|
||||||
[2m+8 vendor frames [22m
|
|
||||||
9 tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php:22
|
|
||||||
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Feature\Api\V1\Whatsapp\WhatsappInviteContr… QueryException
|
|
||||||
SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: classSection.semester (Connection: sqlite, Database: :memory:, SQL: insert into "classSection" ("class_section_id", "class_section_name", "class_id", "created_at", "updated_at") values (200, Grade 1, 1, 2026-03-10 20:29:56, 2026-03-10 20:29:56))
|
|
||||||
|
|
||||||
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
|
|
||||||
834▕ $exceptionType = $this->isUniqueConstraintError($e)
|
|
||||||
835▕ ? UniqueConstraintViolationException::class
|
|
||||||
836▕ : QueryException::class;
|
|
||||||
837▕
|
|
||||||
➜ 838▕ throw new $exceptionType(
|
|
||||||
839▕ $this->getNameWithReadWriteType(),
|
|
||||||
840▕ $query,
|
|
||||||
841▕ $this->prepareBindings($bindings),
|
|
||||||
842▕ $e,
|
|
||||||
|
|
||||||
[2m+8 vendor frames [22m
|
|
||||||
9 tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php:134
|
|
||||||
10 tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php:46
|
|
||||||
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Feature\Api\V1\Whatsapp\WhatsappInviteContr… QueryException
|
|
||||||
SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: classSection.semester (Connection: sqlite, Database: :memory:, SQL: insert into "classSection" ("class_section_id", "class_section_name", "class_id", "created_at", "updated_at") values (200, Grade 1, 1, 2026-03-10 20:29:56, 2026-03-10 20:29:56))
|
|
||||||
|
|
||||||
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
|
|
||||||
834▕ $exceptionType = $this->isUniqueConstraintError($e)
|
|
||||||
835▕ ? UniqueConstraintViolationException::class
|
|
||||||
836▕ : QueryException::class;
|
|
||||||
837▕
|
|
||||||
➜ 838▕ throw new $exceptionType(
|
|
||||||
839▕ $this->getNameWithReadWriteType(),
|
|
||||||
840▕ $query,
|
|
||||||
841▕ $this->prepareBindings($bindings),
|
|
||||||
842▕ $e,
|
|
||||||
|
|
||||||
[2m+8 vendor frames [22m
|
|
||||||
9 tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php:134
|
|
||||||
10 tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php:60
|
|
||||||
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Feature\Api\V1\Whatsapp\WhatsappInviteContr… QueryException
|
|
||||||
SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: classSection.semester (Connection: sqlite, Database: :memory:, SQL: insert into "classSection" ("class_section_id", "class_section_name", "class_id", "created_at", "updated_at") values (200, Grade 1, 1, 2026-03-10 20:29:56, 2026-03-10 20:29:56))
|
|
||||||
|
|
||||||
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
|
|
||||||
834▕ $exceptionType = $this->isUniqueConstraintError($e)
|
|
||||||
835▕ ? UniqueConstraintViolationException::class
|
|
||||||
836▕ : QueryException::class;
|
|
||||||
837▕
|
|
||||||
➜ 838▕ throw new $exceptionType(
|
|
||||||
839▕ $this->getNameWithReadWriteType(),
|
|
||||||
840▕ $query,
|
|
||||||
841▕ $this->prepareBindings($bindings),
|
|
||||||
842▕ $e,
|
|
||||||
|
|
||||||
[2m+8 vendor frames [22m
|
|
||||||
9 tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php:134
|
|
||||||
10 tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php:81
|
|
||||||
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Feature\Api\V1\Whatsapp\WhatsappInviteContr… QueryException
|
|
||||||
SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: classSection.semester (Connection: sqlite, Database: :memory:, SQL: insert into "classSection" ("class_section_id", "class_section_name", "class_id", "created_at", "updated_at") values (200, Grade 1, 1, 2026-03-10 20:29:57, 2026-03-10 20:29:57))
|
|
||||||
|
|
||||||
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
|
|
||||||
834▕ $exceptionType = $this->isUniqueConstraintError($e)
|
|
||||||
835▕ ? UniqueConstraintViolationException::class
|
|
||||||
836▕ : QueryException::class;
|
|
||||||
837▕
|
|
||||||
➜ 838▕ throw new $exceptionType(
|
|
||||||
839▕ $this->getNameWithReadWriteType(),
|
|
||||||
840▕ $query,
|
|
||||||
841▕ $this->prepareBindings($bindings),
|
|
||||||
842▕ $e,
|
|
||||||
|
|
||||||
[2m+8 vendor frames [22m
|
|
||||||
9 tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php:134
|
|
||||||
10 tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php:99
|
|
||||||
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Feature\Api\V1\Whatsapp\WhatsappLinkControllerTest > store…
|
|
||||||
Expected response status code [201] but received 422.
|
|
||||||
Failed asserting that 422 is identical to 201.
|
|
||||||
|
|
||||||
The following errors occurred during the last request:
|
|
||||||
|
|
||||||
{
|
|
||||||
"message": "Validation failed.",
|
|
||||||
"errors": {
|
|
||||||
"class_section_id": [
|
|
||||||
"The class section id field is required."
|
|
||||||
],
|
|
||||||
"invite_link": [
|
|
||||||
"The invite link field is required."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
at tests/Feature/Api/V1/Whatsapp/WhatsappLinkControllerTest.php:78
|
|
||||||
74▕ ];
|
|
||||||
75▕
|
|
||||||
76▕ $response = $this->postJson('/api/v1/whatsapp/links', $payload);
|
|
||||||
77▕
|
|
||||||
➜ 78▕ $response->assertStatus(201);
|
|
||||||
79▕ $response->assertJsonPath('status', true);
|
|
||||||
80▕ $this->assertDatabaseHas('whatsapp_group_links', [
|
|
||||||
81▕ 'class_section_id' => 202,
|
|
||||||
82▕ 'invite_link' => 'https://chat.whatsapp.com/new',
|
|
||||||
|
|
||||||
────────────────────────────────────────────────────────────────────────────
|
|
||||||
FAILED Tests\Feature\Api\V1\Whatsapp\WhatsappLinkControllerTest > update…
|
|
||||||
Failed asserting that a row in the table [whatsapp_group_links] matches the attributes {
|
|
||||||
"id": 1,
|
|
||||||
"invite_link": "https:\/\/chat.whatsapp.com\/updated",
|
|
||||||
"active": 0
|
|
||||||
}.
|
|
||||||
|
|
||||||
Found similar results: [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"invite_link": "https:\/\/chat.whatsapp.com\/old",
|
|
||||||
"active": 1
|
|
||||||
}
|
|
||||||
].
|
|
||||||
|
|
||||||
at tests/Feature/Api/V1/Whatsapp/WhatsappLinkControllerTest.php:109
|
|
||||||
105▕ ]);
|
|
||||||
106▕
|
|
||||||
107▕ $response->assertOk();
|
|
||||||
108▕ $response->assertJsonPath('status', true);
|
|
||||||
➜ 109▕ $this->assertDatabaseHas('whatsapp_group_links', [
|
|
||||||
110▕ 'id' => $linkId,
|
|
||||||
111▕ 'invite_link' => 'https://chat.whatsapp.com/updated',
|
|
||||||
112▕ 'active' => 0,
|
|
||||||
113▕ ]);
|
|
||||||
|
|
||||||
|
|
||||||
Tests: 10 failed, 21 passed (52 assertions)
|
|
||||||
Duration: 7.96s
|
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Api\V1\Auth;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SessionTimeoutControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_config_returns_timeout_values(): void
|
||||||
|
{
|
||||||
|
$user = $this->seedUser();
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/session/timeout-config');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonPath('status', true);
|
||||||
|
$this->assertArrayHasKey('timeout', $response->json('data.config'));
|
||||||
|
$this->assertArrayHasKey('keep_alive_url', $response->json('data.config'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_check_timeout_returns_active_status(): void
|
||||||
|
{
|
||||||
|
$user = $this->seedUser();
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
$response = $this
|
||||||
|
->withSession(['last_activity' => time() - 10])
|
||||||
|
->getJson('/api/v1/session/check-timeout');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonPath('status', true);
|
||||||
|
$response->assertJsonPath('data.session.status', 'active');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_check_timeout_returns_warning_status(): void
|
||||||
|
{
|
||||||
|
$user = $this->seedUser();
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
$response = $this
|
||||||
|
->withSession(['last_activity' => time() - 1000])
|
||||||
|
->getJson('/api/v1/session/check-timeout');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonPath('status', true);
|
||||||
|
$response->assertJsonPath('data.session.status', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_check_timeout_returns_expired_status(): void
|
||||||
|
{
|
||||||
|
$user = $this->seedUser();
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
$response = $this
|
||||||
|
->withSession(['last_activity' => time() - 5000])
|
||||||
|
->getJson('/api/v1/session/check-timeout');
|
||||||
|
|
||||||
|
$response->assertStatus(401);
|
||||||
|
$response->assertJsonPath('status', false);
|
||||||
|
$response->assertJsonPath('errors.status', 'expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_ping_updates_activity(): void
|
||||||
|
{
|
||||||
|
$user = $this->seedUser();
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
$response = $this
|
||||||
|
->withSession(['last_activity' => time() - 10])
|
||||||
|
->postJson('/api/v1/session/ping');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonPath('status', true);
|
||||||
|
$response->assertJsonPath('data.session.status', 'active');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_ping_returns_expired_when_missing_activity(): void
|
||||||
|
{
|
||||||
|
$user = $this->seedUser();
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/session/ping');
|
||||||
|
|
||||||
|
$response->assertStatus(401);
|
||||||
|
$response->assertJsonPath('status', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_requires_authentication(): void
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/api/v1/session/timeout-config');
|
||||||
|
|
||||||
|
$response->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedUser(): User
|
||||||
|
{
|
||||||
|
$userId = DB::table('users')->insertGetId([
|
||||||
|
'firstname' => 'Admin',
|
||||||
|
'lastname' => 'User',
|
||||||
|
'cellphone' => '9999999999',
|
||||||
|
'email' => 'session@example.com',
|
||||||
|
'address_street' => '123 Street',
|
||||||
|
'city' => 'City',
|
||||||
|
'state' => 'ST',
|
||||||
|
'zip' => '12345',
|
||||||
|
'accept_school_policy' => 1,
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'user_type' => 'primary',
|
||||||
|
'semester' => 'Fall',
|
||||||
|
'school_year' => '2025-2026',
|
||||||
|
'status' => 'Active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return User::query()->findOrFail($userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services\Auth;
|
||||||
|
|
||||||
|
use App\Services\Auth\SessionActivityService;
|
||||||
|
use Illuminate\Session\ArraySessionHandler;
|
||||||
|
use Illuminate\Session\Store;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SessionActivityServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_set_and_get_last_activity(): void
|
||||||
|
{
|
||||||
|
$store = $this->makeSessionStore();
|
||||||
|
$service = new SessionActivityService($store);
|
||||||
|
|
||||||
|
$service->setLastActivity(12345);
|
||||||
|
|
||||||
|
$this->assertTrue($service->hasLastActivity());
|
||||||
|
$this->assertSame(12345, $service->getLastActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_clear_last_activity(): void
|
||||||
|
{
|
||||||
|
$store = $this->makeSessionStore();
|
||||||
|
$service = new SessionActivityService($store);
|
||||||
|
|
||||||
|
$service->setLastActivity(12345);
|
||||||
|
$service->clearLastActivity();
|
||||||
|
|
||||||
|
$this->assertFalse($service->hasLastActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeSessionStore(): Store
|
||||||
|
{
|
||||||
|
$handler = new ArraySessionHandler(10);
|
||||||
|
$store = new Store('test', $handler);
|
||||||
|
$store->start();
|
||||||
|
|
||||||
|
return $store;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services\Auth;
|
||||||
|
|
||||||
|
use App\Services\Auth\SessionTimeoutConfigService;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SessionTimeoutConfigServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_config_returns_expected_keys(): void
|
||||||
|
{
|
||||||
|
$service = new SessionTimeoutConfigService();
|
||||||
|
$config = $service->config();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('timeout', $config);
|
||||||
|
$this->assertArrayHasKey('warning_time', $config);
|
||||||
|
$this->assertArrayHasKey('check_interval', $config);
|
||||||
|
$this->assertArrayHasKey('logout_url', $config);
|
||||||
|
$this->assertArrayHasKey('keep_alive_url', $config);
|
||||||
|
$this->assertArrayHasKey('check_url', $config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services\Auth;
|
||||||
|
|
||||||
|
use App\Config\SessionTimeout;
|
||||||
|
use App\Services\Auth\SessionActivityService;
|
||||||
|
use App\Services\Auth\SessionTimeoutService;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SessionTimeoutServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_check_timeout_returns_expired_when_missing_activity(): void
|
||||||
|
{
|
||||||
|
$activity = Mockery::mock(SessionActivityService::class);
|
||||||
|
$activity->shouldReceive('hasLastActivity')->andReturn(false);
|
||||||
|
$activity->shouldReceive('clearLastActivity')->once();
|
||||||
|
$activity->shouldReceive('invalidate')->once();
|
||||||
|
|
||||||
|
$service = new SessionTimeoutService($activity);
|
||||||
|
$result = $service->checkTimeout();
|
||||||
|
|
||||||
|
$this->assertSame('expired', $result['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_check_timeout_returns_warning(): void
|
||||||
|
{
|
||||||
|
$activity = Mockery::mock(SessionActivityService::class);
|
||||||
|
$activity->shouldReceive('hasLastActivity')->andReturn(true);
|
||||||
|
$activity->shouldReceive('getLastActivity')->andReturn(time() - (SessionTimeout::WARNING_THRESHOLD + 10));
|
||||||
|
|
||||||
|
$service = new SessionTimeoutService($activity);
|
||||||
|
$result = $service->checkTimeout();
|
||||||
|
|
||||||
|
$this->assertSame('warning', $result['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_ping_returns_expired_when_timed_out(): void
|
||||||
|
{
|
||||||
|
$activity = Mockery::mock(SessionActivityService::class);
|
||||||
|
$activity->shouldReceive('hasLastActivity')->andReturn(true);
|
||||||
|
$activity->shouldReceive('getLastActivity')->andReturn(time() - (SessionTimeout::TIMEOUT_DURATION + 10));
|
||||||
|
$activity->shouldReceive('clearLastActivity')->once();
|
||||||
|
$activity->shouldReceive('invalidate')->once();
|
||||||
|
|
||||||
|
$service = new SessionTimeoutService($activity);
|
||||||
|
$result = $service->pingActivity();
|
||||||
|
|
||||||
|
$this->assertSame('expired', $result['status']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user