diff --git a/app/views/api/v1/chats/_chat.json.jbuilder b/app/views/api/v1/chats/_chat.json.jbuilder index c2d16d9d..a1b520eb 100644 --- a/app/views/api/v1/chats/_chat.json.jbuilder +++ b/app/views/api/v1/chats/_chat.json.jbuilder @@ -4,4 +4,4 @@ json.id chat.id json.title chat.title json.error chat.error.present? ? chat.error : nil json.created_at chat.created_at.iso8601 -json.updated_at chat.updated_at.iso8601 \ No newline at end of file +json.updated_at chat.updated_at.iso8601 diff --git a/app/views/api/v1/chats/index.json.jbuilder b/app/views/api/v1/chats/index.json.jbuilder index 762b054f..c251b416 100644 --- a/app/views/api/v1/chats/index.json.jbuilder +++ b/app/views/api/v1/chats/index.json.jbuilder @@ -15,4 +15,4 @@ json.pagination do json.per_page @pagy.vars[:items] json.total_count @pagy.count json.total_pages @pagy.pages -end \ No newline at end of file +end diff --git a/app/views/api/v1/chats/show.json.jbuilder b/app/views/api/v1/chats/show.json.jbuilder index 25c64c9a..d179da26 100644 --- a/app/views/api/v1/chats/show.json.jbuilder +++ b/app/views/api/v1/chats/show.json.jbuilder @@ -10,7 +10,7 @@ json.messages @messages do |message| json.model message.ai_model if message.type == "AssistantMessage" json.created_at message.created_at.iso8601 json.updated_at message.updated_at.iso8601 - + # Include tool calls for assistant messages if message.type == "AssistantMessage" && message.tool_calls.any? json.tool_calls message.tool_calls do |tool_call| @@ -30,4 +30,4 @@ if @pagy json.total_count @pagy.count json.total_pages @pagy.pages end -end \ No newline at end of file +end diff --git a/app/views/api/v1/messages/show.json.jbuilder b/app/views/api/v1/messages/show.json.jbuilder index 7fcef1f2..4f580f62 100644 --- a/app/views/api/v1/messages/show.json.jbuilder +++ b/app/views/api/v1/messages/show.json.jbuilder @@ -13,4 +13,4 @@ json.updated_at @message.updated_at.iso8601 if @message.type == "UserMessage" json.ai_response_status "pending" json.ai_response_message "AI response is being generated" -end \ No newline at end of file +end diff --git a/config/routes.rb b/config/routes.rb index eb5f7329..d352a5a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -189,7 +189,7 @@ Rails.application.routes.draw do resources :accounts, only: [ :index ] resources :transactions, only: [ :index, :show, :create, :update, :destroy ] resource :usage, only: [ :show ], controller: "usage" - + resources :chats, only: [ :index, :show, :create, :update, :destroy ] do resources :messages, only: [ :create ] do post :retry, on: :collection diff --git a/test/controllers/api/v1/chats_controller_test.rb b/test/controllers/api/v1/chats_controller_test.rb index db0e49d6..f56ed353 100644 --- a/test/controllers/api/v1/chats_controller_test.rb +++ b/test/controllers/api/v1/chats_controller_test.rb @@ -6,25 +6,25 @@ class Api::V1::ChatsControllerTest < ActionDispatch::IntegrationTest setup do @user = users(:family_admin) @user.update!(ai_enabled: true) - + @oauth_app = Doorkeeper::Application.create!( name: "Test API App", redirect_uri: "https://example.com/callback", scopes: "read write read_write" ) - + @read_token = Doorkeeper::AccessToken.create!( application: @oauth_app, resource_owner_id: @user.id, scopes: "read" ) - + @write_token = Doorkeeper::AccessToken.create!( application: @oauth_app, resource_owner_id: @user.id, scopes: "read_write" ) - + @chat = chats(:one) end @@ -32,13 +32,13 @@ class Api::V1::ChatsControllerTest < ActionDispatch::IntegrationTest get "/api/v1/chats" assert_response :unauthorized end - + test "should require AI to be enabled" do @user.update!(ai_enabled: false) - + get "/api/v1/chats", headers: bearer_auth_header(@read_token) assert_response :forbidden - + response_body = JSON.parse(response.body) assert_equal "feature_disabled", response_body["error"] end @@ -46,88 +46,88 @@ class Api::V1::ChatsControllerTest < ActionDispatch::IntegrationTest test "should list chats with read scope" do get "/api/v1/chats", headers: bearer_auth_header(@read_token) assert_response :success - + response_body = JSON.parse(response.body) assert response_body["chats"].is_a?(Array) assert response_body["pagination"].present? end - + test "should show chat with messages" do get "/api/v1/chats/#{@chat.id}", headers: bearer_auth_header(@read_token) assert_response :success - + response_body = JSON.parse(response.body) assert_equal @chat.id, response_body["id"] assert response_body["messages"].is_a?(Array) end - + test "should create chat with write scope" do assert_difference "Chat.count" do - post "/api/v1/chats", + post "/api/v1/chats", params: { title: "New chat", message: "Hello AI" }, headers: bearer_auth_header(@write_token) end - + assert_response :created response_body = JSON.parse(response.body) assert_equal "New chat", response_body["title"] end - + test "should not create chat with read scope" do - post "/api/v1/chats", + post "/api/v1/chats", params: { title: "New chat" }, headers: bearer_auth_header(@read_token) - + assert_response :forbidden end - + test "should update chat" do patch "/api/v1/chats/#{@chat.id}", params: { title: "Updated title" }, headers: bearer_auth_header(@write_token) - + assert_response :success response_body = JSON.parse(response.body) assert_equal "Updated title", response_body["title"] end - + test "should delete chat" do assert_difference "Chat.count", -1 do delete "/api/v1/chats/#{@chat.id}", headers: bearer_auth_header(@write_token) end - + assert_response :no_content end - + test "should not access other user's chat" do other_user = users(:family_member) other_user.update!(family: families(:empty)) other_chat = chats(:two) other_chat.update!(user: other_user) - + get "/api/v1/chats/#{other_chat.id}", headers: bearer_auth_header(@read_token) assert_response :not_found end - + test "should support API key authentication" do # Remove any existing API keys for this user @user.api_keys.destroy_all - + plain_key = ApiKey.generate_secure_key api_key = @user.api_keys.build( name: "Test API Key", - scopes: ["read_write"] + scopes: [ "read_write" ] ) api_key.key = plain_key api_key.save! - + get "/api/v1/chats", headers: { "X-Api-Key" => plain_key } assert_response :success end - + private - + def bearer_auth_header(token) { "Authorization" => "Bearer #{token.token}" } end -end \ No newline at end of file +end diff --git a/test/controllers/api/v1/messages_controller_test.rb b/test/controllers/api/v1/messages_controller_test.rb index 5932d967..da0ff01f 100644 --- a/test/controllers/api/v1/messages_controller_test.rb +++ b/test/controllers/api/v1/messages_controller_test.rb @@ -6,19 +6,19 @@ class Api::V1::MessagesControllerTest < ActionDispatch::IntegrationTest setup do @user = users(:family_admin) @user.update!(ai_enabled: true) - + @oauth_app = Doorkeeper::Application.create!( name: "Test API App", redirect_uri: "https://example.com/callback", scopes: "read write read_write" ) - + @write_token = Doorkeeper::AccessToken.create!( application: @oauth_app, resource_owner_id: @user.id, scopes: "read_write" ) - + @chat = chats(:one) end @@ -26,30 +26,30 @@ class Api::V1::MessagesControllerTest < ActionDispatch::IntegrationTest post "/api/v1/chats/#{@chat.id}/messages" assert_response :unauthorized end - + test "should require AI to be enabled" do @user.update!(ai_enabled: false) - + post "/api/v1/chats/#{@chat.id}/messages", params: { content: "Hello" }, headers: bearer_auth_header(@write_token) assert_response :forbidden end - + test "should create message with write scope" do assert_difference "Message.count" do post "/api/v1/chats/#{@chat.id}/messages", params: { content: "Test message", model: "gpt-4" }, headers: bearer_auth_header(@write_token) end - + assert_response :created response_body = JSON.parse(response.body) assert_equal "Test message", response_body["content"] assert_equal "user_message", response_body["type"] assert_equal "pending", response_body["ai_response_status"] end - + test "should enqueue assistant response job" do assert_enqueued_with(job: AssistantResponseJob) do post "/api/v1/chats/#{@chat.id}/messages", @@ -57,55 +57,55 @@ class Api::V1::MessagesControllerTest < ActionDispatch::IntegrationTest headers: bearer_auth_header(@write_token) end end - + test "should retry last assistant message" do skip "Retry functionality needs debugging" - + # Create an assistant message to retry assistant_message = @chat.messages.create!( type: "AssistantMessage", content: "Previous response", ai_model: "gpt-4" ) - + assert_enqueued_with(job: AssistantResponseJob) do post "/api/v1/chats/#{@chat.id}/messages/retry", headers: bearer_auth_header(@write_token) end - + assert_response :accepted response_body = JSON.parse(response.body) assert response_body["message_id"].present? end - + test "should not retry if no assistant message exists" do # Remove all assistant messages @chat.messages.where(type: "AssistantMessage").destroy_all - + post "/api/v1/chats/#{@chat.id}/messages/retry.json", headers: bearer_auth_header(@write_token) - + assert_response :unprocessable_entity response_body = JSON.parse(response.body) assert_equal "No assistant message to retry", response_body["error"] end - + test "should not access messages in other user's chat" do other_user = users(:family_member) other_user.update!(family: families(:empty)) other_chat = chats(:two) other_chat.update!(user: other_user) - + post "/api/v1/chats/#{other_chat.id}/messages", params: { content: "Test" }, headers: bearer_auth_header(@write_token) - + assert_response :not_found end - + private - + def bearer_auth_header(token) { "Authorization" => "Bearer #{token.token}" } end -end \ No newline at end of file +end