Merge branch 'master' into feature/rs-oauth
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing

# Conflicts:
#	app/models/user.rb
#	config/routes.rb
#	db/schema.rb
This commit is contained in:
2023-06-20 14:07:46 +02:00
93 changed files with 1625 additions and 504 deletions

View File

@@ -0,0 +1,58 @@
require 'rails_helper'
RSpec.describe 'Account settings', type: :feature do
let(:user) { create :user }
feature "Update email address" do
let(:geraint) { create :user, id: 2, cn: 'geraint', email: "lamagliarosa@example.com" }
before do
login_as user, :scope => :user
geraint.save!
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
.with("invalid password").and_return(false)
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
.with("valid password").and_return(true)
end
scenario 'fails with invalid password' do
visit setting_path(:account)
fill_in 'Address', with: "lamagliarosa@example.com"
fill_in 'Current password', with: "invalid password"
click_button "Update"
expect(current_url).to eq(setting_url(:account))
expect(user.reload.unconfirmed_email).to be_nil
within ".flash-msg" do
expect(page).to have_content("did not match your current password")
end
end
scenario 'fails when new address already taken' do
visit setting_path(:account)
fill_in 'Address', with: "lamagliarosa@example.com"
fill_in 'Current password', with: "valid password"
click_button "Update"
expect(current_url).to eq(setting_url(:update_email))
expect(user.reload.unconfirmed_email).to be_nil
within ".error-msg" do
expect(page).to have_content("has already been taken")
end
end
scenario 'works with valid password and address' do
visit setting_path(:account)
fill_in 'Address', with: "lamagliabianca@example.com"
fill_in 'Current password', with: "valid password"
click_button "Update"
expect(current_url).to eq(setting_url(:account))
expect(user.reload.unconfirmed_email).to eq("lamagliabianca@example.com")
within ".flash-msg" do
expect(page).to have_content("Please confirm your new address")
end
end
end
end

View File

@@ -0,0 +1,40 @@
require 'rails_helper'
RSpec.describe 'Experimental Settings', type: :feature do
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
before do
login_as user, scope: :user
end
describe 'Adding a nostr pubkey' do
scenario 'Without nostr browser extension available' do
visit setting_path(:experiments)
expect(page).to have_content("No browser extension found")
expect(page).to have_css('button[data-settings--nostr-pubkey-target=setPubkey]:disabled')
end
# scenario 'Successfully saving a key' do
# Note: Needs a more complex JS testing setup (maybe poltergeist), not
# worth it for now
# end
end
context "With pubkey configured" do
before do
user.update! nostr_pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
end
scenario 'Remove nostr pubkey from account' do
visit setting_path(:experiments)
expect(page).to have_field("nostr_public_key",
with: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
disabled: true)
click_link "Remove"
expect(page).to_not have_field("nostr_public_key")
expect(page).to have_content("verify your public key")
expect(user.reload.nostr_pubkey).to be_nil
end
end
end

View File

@@ -0,0 +1,45 @@
require 'rails_helper'
RSpec.describe 'Profile settings', type: :feature do
let(:user) { create :user, cn: "mwahlberg" }
before do
login_as user, :scope => :user
end
feature "Update display name" do
before do
allow(user).to receive(:display_name).and_return("Mark")
allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, display_name: "Mark"
})
end
scenario 'fails with validation error' do
visit setting_path(:profile)
fill_in 'Display name', with: "M"
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
expect(page).to have_field('Display name', with: 'M')
within ".error-msg" do
expect(page).to have_content("is too short")
end
end
scenario 'works with valid input' do
expect(LdapManager::UpdateDisplayName).to receive(:call)
.with(user.dn, "Marky Mark").and_return(true)
visit setting_path(:profile)
fill_in 'Display name', with: "Marky Mark"
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
within ".flash-msg" do
expect(page).to have_content("Settings saved")
end
end
end
end

View File

@@ -2,15 +2,18 @@ require 'rails_helper'
require 'webmock/rspec'
RSpec.describe XmppExchangeContactsJob, type: :job do
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
let(:guest) { create :user, cn: "isaacnewton", ou: "kosmos.org",
id: 2, email: "hotapple42@eol.com" }
subject(:job) {
described_class.perform_later(user, 'isaacnewton', 'kosmos.org')
described_class.perform_later(user, guest)
}
before do
stub_request(:post, "http://xmpp.example.com/api/add_rosteritem")
.to_return(status: 200, body: "", headers: {})
allow_any_instance_of(User).to receive(:services_enabled).and_return(["xmpp"])
end
it "posts add_rosteritem commands to the ejabberd API" do

View File

@@ -0,0 +1,34 @@
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe XmppSetDefaultBookmarksJob, type: :job do
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
before do
Setting.xmpp_default_rooms = [
"Welcome <welcome@kosmos.chat>",
"Kosmos Dev <kosmos-dev@kosmos.chat>"
]
end
subject(:job) {
described_class.perform_later(user)
}
before do
stub_request(:post, "http://xmpp.example.com/api/private_set")
.to_return(status: 200, body: "", headers: {})
end
it "posts a private_set command to the ejabberd API" do
perform_enqueued_jobs { job }
expect(WebMock).to have_requested(:post, "http://xmpp.example.com/api/private_set")
.with { |req| req.body == '{"user":"willherschel","host":"kosmos.org","element":"\u003cstorage xmlns=\'storage:bookmarks\'\u003e\u003cconference jid=\'welcome@kosmos.chat\' name=\'Welcome\' autojoin=\'false\'\u003e\u003cnick\u003ewillherschel\u003c/nick\u003e\u003c/conference\u003e\u003cconference jid=\'kosmos-dev@kosmos.chat\' name=\'Kosmos Dev\' autojoin=\'false\'\u003e\u003cnick\u003ewillherschel\u003c/nick\u003e\u003c/conference\u003e\u003c/storage\u003e"}' }
end
after do
clear_enqueued_jobs
clear_performed_jobs
end
end

View File

@@ -101,64 +101,75 @@ RSpec.describe User, type: :model do
end
end
describe "#exchange_xmpp_contact_with_inviter" do
describe "#devise_after_confirmation" do
include ActiveJob::TestHelper
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
let(:guest) { create :user, id: 2, cn: "isaacnewton", ou: "kosmos.org", email: "newt@example.com" }
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org", email: "will@hrsch.el" }
before do
Invitation.create! user: user, invited_user_id: guest.id, used_at: DateTime.now
allow_any_instance_of(User).to receive(:services_enabled).and_return(%w[ xmpp ])
allow(user).to receive(:ldap_entry).and_return({
uid: "willherschel", ou: "kosmos.org", mail: "will@hrsch.el"
})
end
it "enqueues a job to exchange XMPP contacts between inviter and invitee" do
guest.send(:exchange_xmpp_contact_with_inviter)
expect(enqueued_jobs.size).to eq(1)
args = enqueued_jobs.first['arguments']
expect(args[0]['_aj_globalid']).to match('gid://akkounts/User')
expect(args[1]).to eq('isaacnewton')
expect(args[2]).to eq('kosmos.org')
end
after do
clear_enqueued_jobs
end
end
describe "#devise_after_confirmation" do
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
after { clear_enqueued_jobs }
it "enables default services" do
expect(user).to receive(:enable_service).with(%w[ discourse gitea mediawiki xmpp ])
user.send(:devise_after_confirmation)
user.send :devise_after_confirmation
end
it "enqueues a job to set default chatroom bookmarks for XMPP" do
allow(user).to receive(:enable_service).and_return(true)
user.send :devise_after_confirmation
job = enqueued_jobs.select{|j| j['job_class'] == "XmppSetDefaultBookmarksJob"}.first
expect(job['arguments'][0]['_aj_globalid']).to eq('gid://akkounts/User/1')
end
context "for invited user with xmpp enabled" do
let(:guest) { create :user, id: 2, cn: "isaacnewton", ou: "kosmos.org", email: "newt@example.com" }
before do
# TODO remove when defaults are implemented
user.update! preferences: { xmpp_exchange_contacts_with_invitees: true }
Invitation.create! user: user, invited_user_id: guest.id, used_at: DateTime.now
allow_any_instance_of(User).to receive(:enable_service).and_return(true)
allow_any_instance_of(User).to receive(:enable_service)
allow(guest).to receive(:ldap_entry).and_return({
uid: "isaacnewton", ou: "kosmos.org", mail: "newt@example.com"
})
end
it "exchanges XMPP contacts with the inviter" do
expect(guest).to receive(:exchange_xmpp_contact_with_inviter)
guest.send(:devise_after_confirmation)
it "enqueues jobs to exchange XMPP contacts between inviter and invitee" do
guest.send :devise_after_confirmation
job = enqueued_jobs.select{|j| j['job_class'] == "XmppExchangeContactsJob"}.first
expect(job["arguments"][0]['_aj_globalid']).to eq('gid://akkounts/User/1')
expect(job["arguments"][1]['_aj_globalid']).to eq('gid://akkounts/User/2')
end
end
context "for email address update of existing account" do
before do
allow(user).to receive(:ldap_entry)
.and_return({ uid: "willherschel", ou: "kosmos.org", mail: "willyboy@aol.com" })
allow(user).to receive(:dn)
.and_return("cn=willherschel,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow(LdapManager::UpdateEmail).to receive(:call)
end
context "automatic contact exchange disabled" do
before do
user.update! preferences: { xmpp_exchange_contacts_with_invitees: false }
end
it "updates the LDAP 'mail' attribute" do
expect(LdapManager::UpdateEmail).to receive(:call)
.with("cn=willherschel,ou=kosmos.org,cn=users,dc=kosmos,dc=org", "will@hrsch.el")
user.send :devise_after_confirmation
end
it "does not exchange XMPP contacts with the inviter" do
expect(guest).to_not receive(:exchange_xmpp_contact_with_inviter)
guest.send(:devise_after_confirmation)
end
it "does not re-enable default services" do
expect(user).not_to receive(:enable_service)
user.send :devise_after_confirmation
end
it "does not enqueue any delayed jobs" do
user.send :devise_after_confirmation
expect(enqueued_jobs).to be_empty
end
end
end

View File

@@ -0,0 +1,41 @@
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe "Discourse SSO", type: :request do
describe "GET /discourse/connect" do
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
before do
Warden.test_mode!
login_as user, scope: :user
allow(user).to receive(:display_name).and_return('Jimbo')
allow(user).to receive(:is_admin?).and_return(false)
end
after do
Warden.test_reset!
end
context "with invalid SSO credentials" do
it "results in a failed signature check" do
expect {
get discourse_connect_path(
sso: "bm9uY2U9ODk2N2NiMmFlZTdlMjdjNzZiZTNkZWQ5ODIwYzMzN2QmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzZXNzaW9uJTJGc3NvX2xvZ2lu",
sig: "01fc008ff7b51855217e879b6f14aaddefbbd4df2d128951f7bb70cfde834c2a"
)
}.to raise_error(DiscourseApi::SingleSignOn::ParseError)
end
end
context "valid SSO credentials" do
it "redirects to the Discourse SSO endpoint" do
get discourse_connect_path(
sso: "bm9uY2U9YjQwYWZmYzg0YWQ2NWE1ZTk5MjdlZWU1NWEzMjdhMTQmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzZXNzaW9uJTJGc3NvX2xvZ2lu",
sig: "b7905c5db612391293249ad5272dac493681efcd255133f6c2aff91ba654a319"
)
expect(response).to redirect_to('http://discourse.example.com/session/sso_login?sso=YWRtaW49ZmFsc2UmZW1haWw9amltbXklNDBleGFtcGxlLmNvbSZleHRlcm5hbF9pZD0xJm5hbWU9SmltYm8mbm9uY2U9YjQwYWZmYzg0YWQ2NWE1ZTk5MjdlZWU1NWEzMjdhMTQmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzZXNzaW9uJTJGc3NvX2xvZ2luJnVzZXJuYW1lPWppbW15&sig=d5f8b1d6db66569bef789fda4a3216119c2d42b84725d043c9a57dde1e528842')
end
end
end
end

View File

@@ -0,0 +1,104 @@
require 'rails_helper'
RSpec.describe "Settings", type: :request do
let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' }
before do
login_as user, :scope => :user
end
describe "GET /settings/experiments" do
it "works" do
get setting_path(:experiments)
expect(response).to have_http_status(200)
end
end
describe "POST /settings/set_nostr_pubkey" do
before do
session_stub = { shared_secret: "rMjWEmvcvtTlQkMd" }
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
end
context "With valid data" do
before do
post set_nostr_pubkey_settings_path, params: {
signed_event: {
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
created_at: 1678254161,
kind: 1,
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
}
}.to_json, headers: {
"CONTENT_TYPE" => "application/json",
"HTTP_ACCEPT" => "application/json"
}
end
it "returns a success status" do
expect(response).to have_http_status(200)
end
it "saves the pubkey" do
expect(user.nostr_pubkey).to eq("07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3")
end
end
context "With wrong username" do
before do
post set_nostr_pubkey_settings_path, params: {
signed_event: {
id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0",
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
created_at: 1678255391,
kind: 1,
content: "Connect my public key to admin@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
sig: "2ace19c9db892ac6383848721a3e08b13d90d689fdeac60d9633a623d3f08eb7e0d468f1b3e928d1ea979477c2ec46ee6cdb2d053ef2e4ed3c0630a51d249029"
}
}.to_json, headers: {
"CONTENT_TYPE" => "application/json",
"HTTP_ACCEPT" => "application/json"
}
end
it "returns a 422 status" do
expect(response).to have_http_status(422)
end
it "does not save the pubkey" do
expect(user.nostr_pubkey).to be_nil
end
end
context "With wrong shared secret" do
before do
session_stub = { shared_secret: "ho-chi-minh" }
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
post set_nostr_pubkey_settings_path, params: {
signed_event: {
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
created_at: 1678254161,
kind: 1,
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
}
}.to_json, headers: {
"CONTENT_TYPE" => "application/json",
"HTTP_ACCEPT" => "application/json"
}
end
it "returns a 422 status" do
expect(response).to have_http_status(422)
end
it "does not save the pubkey" do
expect(user.nostr_pubkey).to be_nil
end
end
end
end

View File

@@ -0,0 +1,41 @@
require 'rails_helper'
RSpec.describe "Well-known URLs", type: :request do
describe "GET /nostr" do
context "without username param" do
it "returns a 422 status" do
get "/.well-known/nostr.json"
expect(response).to have_http_status(:unprocessable_entity)
end
end
context "non-existent user" do
it "returns a 404 status" do
get "/.well-known/nostr.json?name=bob"
expect(response).to have_http_status(:not_found)
end
end
context "user does not have a nostr pubkey configured" do
let(:user) { create :user, cn: 'spongebob', ou: 'kosmos.org' }
it "returns a 404 status" do
get "/.well-known/nostr.json?name=spongebob"
expect(response).to have_http_status(:not_found)
end
end
context "user with nostr pubkey" do
let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org', nostr_pubkey: '438d35a6750d0dd6b75d032af8a768aad76b62f0c70ecb45f9c4d9e63540f7f4' }
before { user.save! }
it "returns a NIP-05 response" do
get "/.well-known/nostr.json?name=bobdylan"
expect(response).to have_http_status(:ok)
res = JSON.parse(response.body)
expect(res["names"].keys.size).to eq(1)
expect(res["names"]["bobdylan"]).to eq(user.nostr_pubkey)
end
end
end
end