# frozen_string_literal: true RSpec.describe DiscourseAi::AiBot::ArtifactKeyValuesController do fab!(:user) fab!(:admin) fab!(:other_user) { Fabricate(:user) } fab!(:private_message_topic) { Fabricate(:private_message_topic, user: user) } fab!(:private_message_post) { Fabricate(:post, topic: private_message_topic, user: user) } fab!(:artifact) do Fabricate(:ai_artifact, post: private_message_post, metadata: { public: true }) end fab!(:private_artifact) { Fabricate(:ai_artifact, post: private_message_post) } before do SiteSetting.discourse_ai_enabled = true SiteSetting.ai_bot_enabled = true end describe "#index" do fab!(:public_key_value) do Fabricate( :ai_artifact_key_value, ai_artifact: artifact, user: user, key: "test_key", value: "test_value", public: true, ) end fab!(:private_key_value) do Fabricate( :ai_artifact_key_value, ai_artifact: artifact, user: user, key: "private_key", value: "private_value", public: false, ) end fab!(:other_user_key_value) do Fabricate( :ai_artifact_key_value, ai_artifact: artifact, user: other_user, key: "other_key", value: "other_value", public: true, ) end fab!(:admin_key_value) do Fabricate( :ai_artifact_key_value, ai_artifact: artifact, user: admin, key: "admin_key", value: "admin_value", public: false, ) end context "when not logged in" do it "returns only public key values" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json", params: { all_users: true, } expect(response.status).to eq(200) json = response.parsed_body expect(json["key_values"].length).to eq(2) # public_key_value and other_user_key_value expect(json["key_values"].map { |kv| kv["key"] }).to contain_exactly( "test_key", "other_key", ) expect(json["has_more"]).to eq(false) expect(json["total_count"]).to eq(2) expect(json["key_values"].map { |kv| kv["user_id"] }).to contain_exactly( user.id, other_user.id, ) expect(json["users"].map { |u| u["id"] }).to contain_exactly(user.id, other_user.id) end it "returns 404 for private artifact" do get "/discourse-ai/ai-bot/artifact-key-values/#{private_artifact.id}.json" expect(response.status).to eq(404) end it "returns 404 for non-existent artifact" do get "/discourse-ai/ai-bot/artifact-key-values/999999.json" expect(response.status).to eq(404) end end context "when logged in as regular user" do before { sign_in(user) } it "returns public key values and own private key values" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json", params: { all_users: true, } expect(response.status).to eq(200) json = response.parsed_body expect(json["key_values"].length).to eq(3) # all key values expect(json["key_values"].map { |kv| kv["key"] }).to contain_exactly( "test_key", "private_key", "other_key", ) end it "filters by current user when all_users is not true" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json" expect(response.status).to eq(200) json = response.parsed_body # Should only show user's own key values when all_users is not explicitly true user_key_values = json["key_values"].select { |kv| kv["user_id"] == user.id } expect(user_key_values.length).to be > 0 end it "shows all users' key values when all_users=true" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json?all_users=true" expect(response.status).to eq(200) json = response.parsed_body expect(json["key_values"].length).to eq(3) end it "filters by key when specified" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json?key=test_key" expect(response.status).to eq(200) json = response.parsed_body expect(json["key_values"].length).to eq(1) expect(json["key_values"].first["key"]).to eq("test_key") end it "returns keys only when keys_only=true" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json?keys_only=true" expect(response.status).to eq(200) json = response.parsed_body expect(json["key_values"]).to be_present # The serializer should handle keys_only option end end context "when logged in as admin" do before { sign_in(admin) } it "returns only my own keys by default" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json" expect(response.status).to eq(200) json = response.parsed_body expect(json["key_values"].length).to eq(1) expect(json["key_values"].map { |kv| kv["key"] }).to contain_exactly("admin_key") end it "can access private artifacts" do get "/discourse-ai/ai-bot/artifact-key-values/#{private_artifact.id}.json" expect(response.status).to eq(200) end end context "when paginating" do before do sign_in(user) # Create more key values to test pagination 15.times do |i| Fabricate( :ai_artifact_key_value, ai_artifact: artifact, user: user, key: "key_#{i}", value: "value_#{i}", public: true, ) end end it "paginates results correctly" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json?per_page=5&page=1" expect(response.status).to eq(200) json = response.parsed_body expect(json["key_values"].length).to eq(5) expect(json["has_more"]).to eq(true) end it "respects per_page limit" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json?per_page=200" expect(response.status).to eq(200) json = response.parsed_body # Should be limited to PER_PAGE_MAX (100) expect(json["key_values"].length).to be <= 100 end it "defaults to page 1 for invalid page numbers" do get "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json?page=0" expect(response.status).to eq(200) json = response.parsed_body expect(json["key_values"]).to be_present end end end describe "#destroy" do fab!(:key_value_to_delete) do Fabricate( :ai_artifact_key_value, ai_artifact: artifact, user: user, key: "delete_me", value: "delete_value", public: false, ) end fab!(:other_user_key_value_to_delete) do Fabricate( :ai_artifact_key_value, ai_artifact: artifact, user: other_user, key: "other_delete_me", value: "other_delete_value", public: false, ) end context "when not logged in" do it "returns 403 forbidden" do delete "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}/delete_me.json" expect(response.status).to eq(403) end end context "when logged in" do before { sign_in(user) } it "deletes own key value successfully" do expect { delete "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}/delete_me.json" expect(response.status).to eq(200) }.to change { artifact.key_values.count }.by(-1) expect(artifact.key_values.find_by(key: "delete_me")).to be_nil end it "returns 404 for non-existent key" do delete "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}/non_existent_key.json" expect(response.status).to eq(404) json = response.parsed_body expect(json["error"]).to eq("Key not found") end it "returns 404 when trying to delete another user's key" do delete "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}/other_delete_me.json" expect(response.status).to eq(404) json = response.parsed_body expect(json["error"]).to eq("Key not found") # Verify the key still exists expect(artifact.key_values.find_by(key: "other_delete_me")).to be_present end it "returns 404 for non-existent artifact" do delete "/discourse-ai/ai-bot/artifact-key-values/999999/delete_me.json" expect(response.status).to eq(404) end it "returns 404 for private artifact user cannot see" do topic = Fabricate(:private_message_topic, user: other_user) private_post = Fabricate(:post, topic: topic) private_artifact = Fabricate(:ai_artifact, post: private_post) Fabricate( :ai_artifact_key_value, ai_artifact: private_artifact, user: other_user, key: "private_key", ) delete "/discourse-ai/ai-bot/artifact-key-values/#{private_artifact.id}/private_key.json" expect(response.status).to eq(404) end context "with URL encoding" do fab!(:special_key_value) do Fabricate( :ai_artifact_key_value, ai_artifact: artifact, user: user, key: "key with spaces & symbols!", value: "special_value", ) end it "handles URL encoded keys correctly" do expect { delete "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json", params: { key: "key with spaces & symbols!", } expect(response.status).to eq(200) }.to change { artifact.key_values.count }.by(-1) expect(artifact.key_values.find_by(key: "key with spaces & symbols!")).to be_nil end end end context "when logged in as admin" do before { sign_in(admin) } it "can only delete own keys, not other users' keys" do # Even admins should only be able to delete their own keys in this context delete "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}/other_delete_me.json" expect(response.status).to eq(404) json = response.parsed_body expect(json["error"]).to eq("Key not found") # Verify the key still exists expect(artifact.key_values.find_by(key: "other_delete_me")).to be_present end it "can delete own keys" do _admin_key = Fabricate( :ai_artifact_key_value, ai_artifact: artifact, user: admin, key: "admin_key", value: "admin_value", ) expect { delete "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}/admin_key.json" expect(response.status).to eq(200) }.to change { artifact.key_values.count }.by(-1) end it "can access private artifacts" do _admin_key = Fabricate( :ai_artifact_key_value, ai_artifact: private_artifact, user: admin, key: "private_admin_key", value: "private_admin_value", ) expect { delete "/discourse-ai/ai-bot/artifact-key-values/#{private_artifact.id}/private_admin_key.json" expect(response.status).to eq(200) }.to change { private_artifact.key_values.count }.by(-1) end end end describe "#set" do let(:valid_params) do { artifact_id: artifact.id, key: "new_key", value: "new_value", public: true } end context "when not logged in" do it "returns 403 forbidden" do post "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json", params: valid_params expect(response.status).to eq(403) end end context "when logged in" do before { sign_in(user) } it "creates a new key value successfully" do expect { post "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json", params: valid_params expect(response.status).to eq(200) }.to change { artifact.key_values.count }.by(1) json = response.parsed_body key_value = json["ai_artifact_key_value"] expect(key_value["key"]).to eq("new_key") expect(key_value["value"]).to eq("new_value") key_value = artifact.key_values.last expect(key_value.user).to eq(user) end it "returns validation errors for invalid data" do post "/discourse-ai/ai-bot/artifact-key-values/#{artifact.id}.json", params: { artifact_id: artifact.id, key: "", # invalid empty key value: "value", } expect(response.status).to eq(422) json = response.parsed_body expect(json["errors"]).to be_present end it "returns 404 for non-existent artifact" do post "/discourse-ai/ai-bot/artifact-key-values/999999.json", params: valid_params expect(response.status).to eq(404) end it "returns 404 for private artifact user cannot see" do topic = Fabricate(:private_message_topic, user: other_user) private_post = Fabricate(:post, topic: topic) private_artifact = Fabricate(:ai_artifact, post: private_post) post "/discourse-ai/ai-bot/artifact-key-values/#{private_artifact.id}.json", params: valid_params expect(response.status).to eq(404) end end end end