'student@example.com', 'id' => 11, 'identity_number' => '123456', 'name' => 'Alma', 'role' => 'student', 'status' => 'active', ], JSON_THROW_ON_ERROR); } test('mahasiswa routes redirect guests to login', function () { $this->get(route('mahasiswa')) ->assertRedirect(route('login')); }); test('mahasiswa dashboard page can be rendered through laravel proxy', function () { config(['services.rag.base_url' => 'https://rag.test/api/v1']); Http::fake([ 'https://rag.test/api/v1/courses*' => Http::response([ 'data' => [ [ 'description' => 'Materi struktur data', 'id' => 101, 'title' => 'Struktur Data', ], ], 'limit' => 100, 'page' => 1, 'total' => 1, ]), 'https://rag.test/api/v1/chats/sessions*' => Http::response([ 'data' => [ [ 'course_id' => '101', 'created_at' => '2026-05-01T10:00:00Z', 'message_count' => 2, 'title' => 'Apa itu stack?', 'updated_at' => '2026-05-01T10:00:00Z', 'user_id' => 'student-1', 'uuid_id' => 'session-1', ], ], 'pagination' => [ 'limit' => 20, 'page' => 1, 'total' => 1, 'total_pages' => 1, ], ]), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->get(route('mahasiswa')) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('mahasiswa') ->where('backendMessage', null) ->has('courses', 1) ->where('courses.0.title', 'Struktur Data') ->has('chatSessions', 1) ->where('chatSessions.0.uuid_id', 'session-1') ->where('selectedCourseId', '101')); Http::assertSent(fn (HttpRequest $request): bool => $request->method() === 'GET' && str_starts_with($request->url(), 'https://rag.test/api/v1/chats/sessions?') && str_contains($request->url(), 'page=1') && str_contains($request->url(), 'limit=20') && $request->hasHeader('Authorization', 'Bearer token')); }); test('mahasiswa chat session can be created and initial message sent through laravel proxy', function () { config([ 'services.rag.base_url' => 'https://rag.test/api/v1', 'services.rag.chat_mode' => 'rest', ]); Http::fake([ 'https://rag.test/api/v1/chats/sessions/session-1/messages' => Http::response([ 'content' => 'Stack adalah struktur data LIFO.', 'created_at' => '2026-05-01T10:01:00Z', 'role' => 'assistant', 'sources' => [], 'uuid_id' => 'message-1', ]), 'https://rag.test/api/v1/chats/sessions' => Http::response([ 'course_id' => '101', 'created_at' => '2026-05-01T10:00:00Z', 'message_count' => 0, 'title' => 'Apa itu stack?', 'updated_at' => '2026-05-01T10:00:00Z', 'user_id' => 'student-1', 'uuid_id' => 'session-1', ], 201), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->from(route('mahasiswa')) ->post(route('mahasiswa.sessions.store'), [ 'course_id' => '101', 'title' => 'Apa itu stack?', ]) ->assertRedirect(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->assertSessionMissing('sevima_raghub_pending_initial_chat.session-1') ->assertSessionHasNoErrors(); Http::assertSent(fn (HttpRequest $request): bool => $request->method() === 'POST' && $request->url() === 'https://rag.test/api/v1/chats/sessions' && $request->hasHeader('Authorization', 'Bearer token') && $request['course_id'] === '101' && $request['title'] === 'Apa itu stack?'); Http::assertSent(fn (HttpRequest $request): bool => $request->method() === 'POST' && $request->url() === 'https://rag.test/api/v1/chats/sessions/session-1/messages' && $request->hasHeader('Authorization', 'Bearer token') && $request['content'] === 'Apa itu stack?'); }); test('mahasiswa chat detail page renders session and history from api', function () { config([ 'services.rag.base_url' => 'https://rag.test/api/v1', 'services.rag.chat_mode' => 'rest', ]); Http::fake([ 'https://rag.test/api/v1/courses*' => Http::response([ 'data' => [ [ 'id' => 101, 'title' => 'Struktur Data', ], ], 'limit' => 100, 'page' => 1, 'total' => 1, ]), 'https://rag.test/api/v1/chats/sessions/session-1/messages' => Http::response([ 'data' => [ [ 'content' => 'Apa itu stack?', 'created_at' => '2026-05-01T10:00:00Z', 'role' => 'user', 'sources' => [], 'uuid_id' => 'user-message-1', ], [ 'content' => 'Stack adalah struktur data LIFO.', 'created_at' => '2026-05-01T10:01:00Z', 'role' => 'assistant', 'sources' => [], 'uuid_id' => 'message-1', ], ], ]), 'https://rag.test/api/v1/chats/sessions/session-1' => Http::response([ 'course_id' => '101', 'created_at' => '2026-05-01T10:00:00Z', 'message_count' => 2, 'title' => 'Apa itu stack?', 'updated_at' => '2026-05-01T10:01:00Z', 'user_id' => 'student-1', 'uuid_id' => 'session-1', ]), 'https://rag.test/api/v1/chats/sessions*' => Http::response([ 'data' => [], 'pagination' => [ 'limit' => 20, 'page' => 1, 'total' => 0, 'total_pages' => 0, ], ]), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->get(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('mahasiswa-chat') ->where('sessionId', 'session-1') ->where('currentSession.uuid_id', 'session-1') ->has('messages', 2) ->where('messages.0.role', 'user') ->where('messages.1.role', 'assistant') ->where('selectedCourseId', '101')); Http::assertSent(fn (HttpRequest $request): bool => $request->url() === 'https://rag.test/api/v1/chats/sessions/session-1'); Http::assertSent(fn (HttpRequest $request): bool => $request->url() === 'https://rag.test/api/v1/chats/sessions/session-1/messages'); }); test('mahasiswa chat detail page can be rendered with direct session messages', function () { config([ 'services.rag.base_url' => 'https://rag.test/api/v1', 'services.rag.chat_mode' => 'direct', ]); Http::fake([ 'https://rag.test/api/v1/courses*' => Http::response([ 'data' => [ [ 'id' => 101, 'title' => 'Struktur Data', ], ], 'limit' => 100, 'page' => 1, 'total' => 1, ]), 'https://rag.test/api/v1/chats/sessions/session-1/messages' => Http::response([ 'data' => [], ]), 'https://rag.test/api/v1/chats/sessions/session-1' => Http::response([ 'course_id' => '101', 'created_at' => '2026-05-01T10:00:00Z', 'message_count' => 0, 'title' => 'Apa itu stack?', 'updated_at' => '2026-05-01T10:00:00Z', 'user_id' => 'student-1', 'uuid_id' => 'session-1', ]), 'https://rag.test/api/v1/chats/sessions*' => Http::response([ 'data' => [], 'pagination' => [ 'limit' => 20, 'page' => 1, 'total' => 0, 'total_pages' => 0, ], ]), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->withSession([ 'sevima_raghub_direct_messages.session-1' => [ [ 'content' => 'Jawaban bot', 'created_at' => '2026-05-01T10:01:00Z', 'role' => 'assistant', 'sources' => [], 'uuid_id' => 'message-1', ], ], ]) ->get(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('mahasiswa-chat') ->where('backendMessage', null) ->where('sessionId', 'session-1') ->where('currentSession.uuid_id', 'session-1') ->has('messages', 1) ->where('messages.0.content', 'Jawaban bot') ->where('selectedCourseId', '101')); }); test('mahasiswa direct chat message can be sent through laravel proxy', function () { config([ 'services.rag.base_url' => 'https://rag.test/api/v1', 'services.rag.chat_mode' => 'direct', ]); Http::fake([ 'https://rag.test/api/v1/ai/query' => Http::response([ 'content' => 'Stack adalah struktur data LIFO.', 'created_at' => '2026-05-01T10:01:00Z', 'message_id' => 'message-1', 'role' => 'assistant', 'sources' => [], ]), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->from(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->post(route('mahasiswa.messages.store', ['sessionId' => 'session-1']), [ 'content' => 'Apa itu stack?', 'course_id' => '101', ]) ->assertRedirect(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->assertSessionHasNoErrors(); $directMessages = session('sevima_raghub_direct_messages.session-1'); expect($directMessages)->toHaveCount(2) ->and($directMessages[0]['content'])->toBe('Apa itu stack?') ->and($directMessages[1]['content'])->toBe('Stack adalah struktur data LIFO.'); Http::assertSent(fn (HttpRequest $request): bool => $request->method() === 'POST' && $request->url() === 'https://rag.test/api/v1/ai/query' && $request->hasHeader('Authorization', 'Bearer token') && $request['course_id'] === '101' && $request['prompt'] === 'Apa itu stack?'); }); test('mahasiswa rest chat message can be sent through laravel proxy', function () { config([ 'services.rag.base_url' => 'https://rag.test/api/v1', 'services.rag.chat_mode' => 'rest', ]); Http::fake([ 'https://rag.test/api/v1/chats/sessions/session-1/messages' => Http::response([ 'content' => 'Stack adalah struktur data LIFO.', 'created_at' => '2026-05-01T10:01:00Z', 'role' => 'assistant', 'sources' => [], 'uuid_id' => 'message-1', ]), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->from(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->post(route('mahasiswa.messages.store', ['sessionId' => 'session-1']), [ 'content' => 'Apa itu stack?', 'course_id' => '101', ]) ->assertRedirect(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->assertSessionHasNoErrors(); Http::assertSent(fn (HttpRequest $request): bool => $request->method() === 'POST' && $request->url() === 'https://rag.test/api/v1/chats/sessions/session-1/messages' && $request->hasHeader('Authorization', 'Bearer token') && $request['content'] === 'Apa itu stack?'); }); test('mahasiswa stream mode json chat message uses rest endpoint', function () { config([ 'services.rag.base_url' => 'https://rag.test/api/v1', 'services.rag.chat_mode' => 'stream', ]); Http::fake([ 'https://rag.test/api/v1/chats/sessions/session-1/messages' => Http::response([ 'content' => 'Stack adalah struktur data LIFO.', 'created_at' => '2026-05-01T10:01:00Z', 'role' => 'assistant', 'sources' => [], 'uuid_id' => 'message-1', ]), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->withHeaders(['Accept' => 'application/json']) ->postJson(route('mahasiswa.messages.store', ['sessionId' => 'session-1']), [ 'content' => 'Apa itu stack?', 'course_id' => '101', ]) ->assertSuccessful() ->assertJsonPath('assistantMessage.content', 'Stack adalah struktur data LIFO.'); Http::assertSent(fn (HttpRequest $request): bool => $request->method() === 'POST' && $request->url() === 'https://rag.test/api/v1/chats/sessions/session-1/messages' && $request->hasHeader('Authorization', 'Bearer token') && $request['content'] === 'Apa itu stack?'); }); test('mahasiswa stream chat message forwards backend server sent events', function () { config([ 'services.rag.base_url' => 'https://rag.test/api/v1', 'services.rag.chat_mode' => 'stream', ]); $streamBody = "data: {\"content\":\"Stack adalah struktur data LIFO.\"}\n\n"; Http::fake([ 'https://rag.test/api/v1/chats/sessions/session-1/stream' => Http::response( $streamBody, 200, ['Content-Type' => 'text/event-stream'], ), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->withHeaders(['Accept' => 'text/event-stream']) ->post(route('mahasiswa.messages.store', ['sessionId' => 'session-1']), [ 'content' => 'Apa itu stack?', 'course_id' => '101', ]) ->assertSuccessful() ->assertStreamed() ->assertHeader('Content-Type', 'text/event-stream; charset=utf-8') ->assertStreamedContent($streamBody); Http::assertSent(fn (HttpRequest $request): bool => $request->method() === 'POST' && $request->url() === 'https://rag.test/api/v1/chats/sessions/session-1/stream' && $request->hasHeader('Authorization', 'Bearer token') && $request->hasHeader('Accept', 'text/event-stream') && $request['content'] === 'Apa itu stack?'); }); test('mahasiswa chat message uses backend error message', function () { config([ 'services.rag.base_url' => 'https://rag.test/api/v1', 'services.rag.chat_mode' => 'direct', ]); Http::fake([ 'https://rag.test/api/v1/ai/query' => Http::response([ 'message' => 'I do not know based on available materials.', ], 422), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->from(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->post(route('mahasiswa.messages.store', ['sessionId' => 'session-1']), [ 'content' => 'Apa itu stack?', 'course_id' => '101', ]) ->assertRedirect(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->assertSessionHasErrors(['message' => 'I do not know based on available materials.']); }); test('mahasiswa chat session can be deleted through laravel proxy', function () { config(['services.rag.base_url' => 'https://rag.test/api/v1']); Http::fake([ 'https://rag.test/api/v1/chats/sessions/session-1' => Http::response([]), ]); $this->withCookie('sevima_raghub_auth_token', 'token') ->withCookie('sevima_raghub_auth_user', studentAuthUserCookie()) ->withSession([ 'sevima_raghub_direct_messages.session-1' => [ [ 'content' => 'Jawaban bot', 'created_at' => '2026-05-01T10:01:00Z', 'role' => 'assistant', 'sources' => [], 'uuid_id' => 'message-1', ], ], ]) ->from(route('mahasiswa.show', ['sessionId' => 'session-1'])) ->delete(route('mahasiswa.destroy', ['sessionId' => 'session-1']), [ 'active_session_id' => 'session-1', ]) ->assertRedirect(route('mahasiswa')) ->assertSessionMissing('sevima_raghub_direct_messages.session-1') ->assertSessionHasNoErrors(); Http::assertSent(fn (HttpRequest $request): bool => $request->method() === 'DELETE' && $request->url() === 'https://rag.test/api/v1/chats/sessions/session-1' && $request->hasHeader('Authorization', 'Bearer token')); });