Calendar: add FT sprint
[tails/test.git] / features / step_definitions / pidgin.rb
blobf9497a20144abe5d10332e796a78679dc0c4b092
1 # Extracts the secrets for the XMPP account `account_name`.
2 def xmpp_account(account_name, required_options = [])
3   begin
4     account = $config['Pidgin']['Accounts']['XMPP'][account_name]
5     check_keys = ['username', 'domain', 'password'] + required_options
6     check_keys.each do |key|
7       assert(account.key?(key))
8       assert_not_nil(account[key])
9       assert(!account[key].empty?)
10     end
11   rescue NoMethodError, Test::Unit::AssertionFailedError
12     raise(
13       "Your Pidgin:Accounts:XMPP:#{account} is incorrect or missing " \
14       "from your local configuration file (#{LOCAL_CONFIG_FILE}). " \
15       'See wiki/src/contribute/release_process/test/usage.mdwn for the format.'
16     )
17   end
18   account
19 end
21 def wait_and_focus(img, window, time = 10)
22   @screen.wait(img, time)
23 rescue FindFailed
24   $vm.focus_window(window)
25   @screen.wait(img, time)
26 end
28 # This method should always fail (except with the option
29 # `return_shellcommand: true`) since we block Pidgin's D-Bus interface
30 # (#14612) ...
31 def pidgin_dbus_call(method, *args, **opts)
32   opts[:user] = LIVE_USER
33   dbus_send(
34     'im.pidgin.purple.PurpleService',
35     '/im/pidgin/purple/PurpleObject',
36     "im.pidgin.purple.PurpleInterface.#{method}",
37     *args, **opts
38   )
39 end
41 # ... unless we re-enable it!
42 def pidgin_force_allowed_dbus_call(method, *args, **opts)
43   opts[:user] = LIVE_USER
44   policy_file = '/etc/dbus-1/session.d/im.pidgin.purple.PurpleService.conf'
45   $vm.execute_successfully("mv #{policy_file} #{policy_file}.disabled")
46   # From dbus-daemon(1): "Policy changes should take effect with SIGHUP"
47   # Note that HUP is asynchronous, so there is no guarantee whatsoever
48   # that the HUP will take effect before we do the dbus call. In
49   # practice, however, the delays imposed by using the remote shell is
50   # (in general) much larger than the processing time needed for
51   # handling signals, so they are in effect synchronous in our
52   # context.
53   $vm.execute_successfully("pkill -HUP -u #{opts[:user]} 'dbus-daemon'")
54   pidgin_dbus_call(method, *args, **opts)
55 ensure
56   $vm.execute_successfully("mv #{policy_file}.disabled #{policy_file}")
57   $vm.execute_successfully("pkill -HUP -u #{opts[:user]} 'dbus-daemon'")
58 end
60 def pidgin_account_connected?(account, prpl_protocol)
61   account_id = pidgin_force_allowed_dbus_call(
62     'PurpleAccountsFind', account, prpl_protocol
63   )
64   pidgin_force_allowed_dbus_call('PurpleAccountIsConnected', account_id) == 1
65 end
67 def click_mid_right_edge(pattern, **opts)
68   m = @screen.find(pattern, **opts)
69   @screen.click(m.x + m.w, m.y + m.h / 2)
70 end
72 When /^I create my XMPP account$/ do
73   account = xmpp_account('Tails_account')
74   @screen.click('PidginAccountManagerAddButton.png')
75   @screen.wait('PidginAddAccountWindow.png', 20)
76   click_mid_right_edge('PidginAddAccountProtocolLabel.png')
77   @screen.click('PidginAddAccountProtocolXMPP.png')
78   # We first wait for some field that is shown for XMPP but not the
79   # default (IRC) since we otherwise may decide where we click before
80   # the GUI has updated after switching protocol.
81   @screen.wait('PidginAddAccountXMPPDomain.png', 5)
82   click_mid_right_edge('PidginAddAccountXMPPUsername.png')
83   @screen.type(account['username'])
84   click_mid_right_edge('PidginAddAccountXMPPDomain.png')
85   @screen.type(account['domain'])
86   click_mid_right_edge('PidginAddAccountXMPPPassword.png')
87   @screen.type(account['password'])
88   @screen.click('PidginAddAccountXMPPRememberPassword.png')
89   if account['connect_server']
90     @screen.click('PidginAddAccountXMPPAdvancedTab.png')
91     click_mid_right_edge('PidginAddAccountXMPPConnectServer.png')
92     @screen.type(account['connect_server'])
93   end
94   @screen.click('PidginAddAccountXMPPAddButton.png')
95 end
97 Then /^Pidgin automatically enables my XMPP account$/ do
98   account = xmpp_account('Tails_account')
99   jid = account['username'] + '@' + account['domain']
100   try_for(3 * 60) do
101     pidgin_account_connected?(jid, 'prpl-jabber')
102   end
103   $vm.focus_window('Buddy List')
104   @screen.wait('PidginAvailableStatus.png', 60 * 3)
107 Given /^my XMPP friend goes online( and joins the multi-user chat)?$/ do |join_chat|
108   account = xmpp_account('Friend_account', ['otr_key'])
109   bot_opts = account.select { |k, _| ['connect_server'].include?(k) }
110   bot_opts['auto_join'] = [@chat_room_jid] if join_chat
111   @friend_name = account['username']
112   @chatbot = ChatBot.new(
113     account['username'] + '@' + account['domain'],
114     account['password'],
115     account['otr_key'],
116     **(bot_opts.transform_keys(&:to_sym))
117   )
118   @chatbot.start
119   add_after_scenario_hook { @chatbot.stop }
120   $vm.focus_window('Buddy List')
121   @screen.wait('PidginFriendOnline.png', 60)
124 When /^I start a conversation with my friend$/ do
125   $vm.focus_window('Buddy List')
126   # Clicking the middle, bottom of this image should query our
127   # friend, given it's the only subscribed user that's online, which
128   # we assume.
129   r = @screen.find('PidginFriendOnline.png')
130   x = r.x + r.w / 2
131   y = r.y + r.h
132   @screen.click(x, y, double: true)
133   # Since Pidgin sets the window name to the contact, we have no good
134   # way to identify the conversation window. Let's just look for the
135   # expected menu bar.
136   @screen.wait('PidginConversationWindowMenuBar.png', 10)
139 And /^I say (.*) to my friend( in the multi-user chat)?$/ do |msg, multi_chat|
140   msg = 'ping' if msg == 'something'
141   if multi_chat
142     $vm.focus_window(@chat_room_jid.split('@').first)
143     msg = @friend_name + ': ' + msg
144   else
145     $vm.focus_window(@friend_name)
146   end
147   @screen.type(msg, ['Return'])
150 Then /^I receive a response from my friend( in the multi-user chat)?$/ do |multi_chat|
151   if multi_chat
152     $vm.focus_window(@chat_room_jid.split('@').first)
153   else
154     $vm.focus_window(@friend_name)
155   end
156   try_for(60) do
157     if @screen.exists('PidginServerMessage.png')
158       @screen.click('PidginDialogCloseButton.png')
159     end
160     @screen.find('PidginFriendExpectedAnswer.png')
161   end
164 When /^I start an OTR session with my friend$/ do
165   $vm.focus_window(@friend_name)
166   @screen.click('PidginConversationOTRMenu.png')
167   @screen.hide_cursor
168   @screen.click('PidginOTRMenuStartSession.png')
171 Then /^Pidgin automatically generates an OTR key$/ do
172   @screen.wait('PidginOTRKeyGenPrompt.png', 30)
173   @screen.wait('PidginOTRKeyGenPromptDoneButton.png', 30).click
176 Then /^an OTR session was successfully started with my friend$/ do
177   $vm.focus_window(@friend_name)
178   @screen.wait('PidginConversationOTRUnverifiedSessionStarted.png', 10)
181 # The reason the chat must be empty is to guarantee that we don't mix
182 # up messages/events from other users with the ones we expect from the
183 # bot.
184 When /^I join some empty multi-user chat$/ do
185   $vm.focus_window('Buddy List')
186   @screen.click('PidginBuddiesMenu.png')
187   @screen.wait('PidginBuddiesMenuJoinChat.png', 10).click
188   @screen.wait('PidginJoinChatWindow.png', 10).click
189   click_mid_right_edge('PidginJoinChatRoomLabel.png')
190   account = xmpp_account('Tails_account')
191   chat_room = if account.key?('chat_room') && \
192                  !account['chat_room'].nil? && \
193                  !account['chat_room'].empty?
194                 account['chat_room']
195               else
196                 random_alnum_string(10, 15)
197               end
198   @screen.type(chat_room)
200   # We will need the conference server later, when starting the bot.
201   click_mid_right_edge('PidginJoinChatServerLabel.png')
202   @screen.press('ctrl', 'a')
203   @screen.press('ctrl', 'c')
204   conference_server =
205     $vm.execute_successfully('xclip -o', user: LIVE_USER).stdout.chomp
206   @chat_room_jid = chat_room + '@' + conference_server
208   @screen.click('PidginJoinChatButton.png')
209   # The following will both make sure that the we joined the chat, and
210   # that it is empty. We'll also deal with the *potential* "Create New
211   # Room" prompt that Pidgin shows for some server configurations.
212   images = ['PidginCreateNewRoomPrompt.png',
213             'PidginChat1UserInRoom.png',]
214   image_found = @screen.wait_any(images, 30)[:found_pattern]
215   if image_found == 'PidginCreateNewRoomPrompt.png'
216     @screen.click('PidginCreateNewRoomAcceptDefaultsButton.png')
217   end
218   $vm.focus_window(@chat_room_jid)
219   @screen.wait('PidginChat1UserInRoom.png', 10)
222 # Since some servers save the scrollback, and sends it when joining,
223 # it's safer to clear it so we do not get false positives from old
224 # messages when looking for a particular response, or similar.
225 When /^I clear the multi-user chat's scrollback$/ do
226   $vm.focus_window(@chat_room_jid)
227   @screen.click('PidginConversationMenu.png')
228   @screen.wait('PidginConversationMenuClearScrollback.png', 10).click
231 Then /^I can see that my friend joined the multi-user chat$/ do
232   $vm.focus_window(@chat_room_jid)
233   @screen.wait('PidginChat2UsersInRoom.png', 60)
236 def configured_pidgin_accounts
237   accounts = {}
238   xml = REXML::Document.new(
239     $vm.file_content("/home/#{LIVE_USER}/.purple/accounts.xml")
240   )
241   xml.elements.each('account/account') do |e|
242     account = e.elements['name'].text
243     account_name, network = account.split('@')
244     protocol = e.elements['protocol'].text
245     port = e.elements["settings/setting[@name='port']"].text
246     username_element = e.elements["settings/setting[@name='username']"]
247     realname_elemenet = e.elements["settings/setting[@name='realname']"]
248     nickname = username_element ? username_element.text : nil
249     real_name = realname_elemenet ? realname_elemenet.text : nil
250     accounts[network] = {
251       'name'      => account_name,
252       'network'   => network,
253       'protocol'  => protocol,
254       'port'      => port,
255       'nickname'  => nickname,
256       'real_name' => real_name,
257     }
258   end
260   accounts
263 def chan_image(account, channel, image)
264   images = {
265     'conference.riseup.net' => {
266       'tails' => {
267         'conversation_tab' => 'PidginTailsConversationTab',
268         'welcome'          => 'PidginTailsChannelWelcome',
269       },
270     },
271   }
272   images[account][channel][image] + '.png'
275 def default_chan(account)
276   chans = {
277     'conference.riseup.net' => 'tails',
278   }
279   chans[account]
282 def pidgin_otr_keys
283   $vm.file_content("/home/#{LIVE_USER}/.purple/otr.private_key")
286 When /^I open Pidgin's account manager window$/ do
287   @screen.wait('PidginMenuAccounts.png', 20).click
288   @screen.wait('PidginMenuManageAccounts.png', 20).click
289   step "I see Pidgin's account manager window"
292 When /^I see Pidgin's account manager window$/ do
293   @screen.wait('PidginAccountWindow.png', 40)
296 When /^I close Pidgin's account manager window$/ do
297   @screen.wait('PidginDialogCloseButton.png', 10).click
300 When /^I close Pidgin$/ do
301   $vm.focus_window('Buddy List')
302   @screen.press('ctrl', 'q')
303   @screen.wait_vanish('PidginAvailableStatus.png', 10)
306 When /^I (de)?activate the "([^"]+)" Pidgin account$/ do |deactivate, account|
307   @screen.click("PidginAccount_#{account}.png")
308   @screen.type(['Left'], ['space'])
309   if deactivate
310     @screen.wait_vanish('PidginAccountEnabledCheckbox.png', 5)
311   else
312     # wait for the Pidgin to be connecting, otherwise sometimes the step
313     # that closes the account management dialog happens before the account
314     # is actually enabled
315     @screen.wait_any(['PidginConnecting.png', 'PidginAvailableStatus.png'], 5)
316   end
319 def deactivate_and_activate_pidgin_account(account)
320   debug_log("Deactivating and reactivating Pidgin account #{account}")
321   step "I open Pidgin's account manager window"
322   step "I deactivate the \"#{account}\" Pidgin account"
323   step "I close Pidgin's account manager window"
324   step "I open Pidgin's account manager window"
325   step "I activate the \"#{account}\" Pidgin account"
326   step "I close Pidgin's account manager window"
329 Then /^Pidgin successfully connects to the "([^"]+)" account$/ do |account|
330   expected_channel_entry = chan_image(account, default_chan(account), 'roster')
331   reconnect_button = 'PidginReconnect.png'
332   recovery_on_failure = proc do
333     if @screen.exists('PidginReconnect.png')
334       @screen.click('PidginReconnect.png')
335     else
336       deactivate_and_activate_pidgin_account(account)
337     end
338   end
339   retry_tor(recovery_on_failure) do
340     begin
341       $vm.focus_window('Buddy List')
342     rescue ExecutionFailedInVM
343       # Sometimes focusing the window with xdotool will fail with the
344       # conversation window right on top of it. We'll try to close the
345       # conversation window. At worst, the test will still fail...
346       close_pidgin_conversation_window(account)
347     end
348     on_screen = @screen.wait_any([expected_channel_entry, reconnect_button],
349                                  60)[:found_pattern]
350     unless on_screen == expected_channel_entry
351       raise "Connecting to account #{account} failed."
352     end
353   end
356 Then /^I can join the "([^"]+)" channel on "([^"]+)"$/ do |channel, account|
357   $vm.focus_window('Buddy List')
358   @screen.wait('PidginBuddiesMenu.png', 20).click
359   @screen.wait('PidginBuddiesMenuJoinChat.png', 10).click
360   @screen.wait('PidginJoinChatWindow.png', 10).click
361   click_mid_right_edge('PidginJoinChatRoomLabel.png')
362   @screen.type(channel)
363   @screen.click('PidginJoinChatButton.png')
364   @chat_room_jid = channel + '@' + account
365   $vm.focus_window(@chat_room_jid)
366   @screen.hide_cursor
367   try_for(60) do
368     begin
369       @screen.wait(chan_image(account, channel, 'conversation_tab'), 5).click
370     rescue FindFailed => e
371       # If the channel tab can't be found it could be because there were
372       # multiple connection attempts and the channel tab we want is off the
373       # screen. We'll try closing tabs until the one we want can be found.
374       @screen.press('ctrl', 'w')
375       raise e
376     end
377   end
378   @screen.hide_cursor
379   @screen.wait(chan_image(account, channel, 'welcome'), 10)
382 Then /^I take note of the configured Pidgin accounts$/ do
383   @persistent_pidgin_accounts = configured_pidgin_accounts
386 Then /^I take note of the OTR key for Pidgin's "(?:[^"]+)" account$/ do
387   @persistent_pidgin_otr_keys = pidgin_otr_keys
390 Then /^Pidgin has the expected persistent accounts configured$/ do
391   current_accounts = configured_pidgin_accounts
392   assert(
393     current_accounts <=> @persistent_pidgin_accounts,
394     "Currently configured Pidgin accounts do not match the persistent ones:\n" \
395     "Current:\n#{current_accounts}\n" \
396     "Persistent:\n#{@persistent_pidgin_accounts}"
397   )
400 Then /^Pidgin has the expected persistent OTR keys$/ do
401   assert_equal(@persistent_pidgin_otr_keys, pidgin_otr_keys)
404 def pidgin_add_certificate_from(cert_file)
405   src = '/usr/share/ca-certificates/mozilla/' \
406         'Staat_der_Nederlanden_EV_Root_CA.crt'
407   # Here, we need a certificate that is not already in the NSS database
408   step "I copy \"#{src}\" to \"#{cert_file}\" as user \"amnesia\""
410   $vm.focus_window('Buddy List')
411   @screen.wait('PidginToolsMenu.png', 10).click
412   @screen.wait('PidginCertificatesMenuItem.png', 10).click
413   @screen.wait('PidginCertificateManagerDialog.png', 10)
414   @screen.wait('PidginCertificateAddButton.png', 10).click
415   begin
416     @screen.wait('GtkFileChooserDesktopButton.png', 10).click
417   rescue FindFailed
418     # The first time we're run, the file chooser opens in the Recent
419     # view, so we have to browse a directory before we can use the
420     # "Type file name" button. But on subsequent runs, the file
421     # chooser is already in the Desktop directory, so we don't need to
422     # do anything. Hence, this noop exception handler.
423   end
424   @screen.wait('GtkFileTypeFileNameButton.png', 10).click
425   @screen.press('alt', 'l') # "Location" field
426   @screen.type(cert_file, ['Return'])
429 Then /^I can add a certificate from the "([^"]+)" directory to Pidgin$/ do |cert_dir|
430   pidgin_add_certificate_from("#{cert_dir}/test.crt")
431   wait_and_focus('PidginCertificateAddHostnameDialog.png',
432                  'Certificate Import', 10)
433   @screen.type('XXX test XXX', ['Return'])
434   wait_and_focus('PidginCertificateTestItem.png', 'Certificate Manager', 10)
437 Then /^I cannot add a certificate from the "([^"]+)" directory to Pidgin$/ do |cert_dir|
438   pidgin_add_certificate_from("#{cert_dir}/test.crt")
439   wait_and_focus('PidginCertificateImportFailed.png', 'Import Error', 10)
442 When /^I close Pidgin's certificate manager$/ do
443   wait_and_focus('PidginCertificateManagerDialog.png', 'Certificate Manager',
444                  10)
445   @screen.press('Escape')
446   # @screen.wait('PidginCertificateManagerClose.png', 10).click
447   @screen.wait_vanish('PidginCertificateManagerDialog.png', 10)
450 When /^I close Pidgin's certificate import failure dialog$/ do
451   @screen.press('Escape')
452   # @screen.wait('PidginCertificateManagerClose.png', 10).click
453   @screen.wait_vanish('PidginCertificateImportFailed.png', 10)
456 When /^I see the Tails GitLab URL$/ do
457   try_for(60) do
458     if @screen.exists('PidginServerMessage.png')
459       @screen.click('PidginDialogCloseButton.png')
460     end
461     begin
462       @screen.find('PidginTailsGitLabUrl.png')
463     rescue FindFailed => e
464       @screen.press('Page_Up')
465       raise e
466     end
467   end
470 When /^I click on the Tails GitLab URL$/ do
471   @screen.click('PidginTailsGitLabUrl.png')
472   try_for(60) { @torbrowser = Dogtail::Application.new('Firefox') }
475 Then /^Pidgin's D-Bus interface is not available$/ do
476   # Pidgin must be running to expose the interface
477   assert($vm.process_running?('pidgin'))
478   # Let's first ensure it would work if not explicitly blocked.
479   # Note: that the method we pick here doesn't really matter
480   # (`PurpleAccountsGetAll` felt like a convenient choice since it
481   # doesn't require any arguments).
482   assert_equal(
483     Array, pidgin_force_allowed_dbus_call('PurpleAccountsGetAll').class
484   )
485   # Finally, let's make sure it is blocked
486   c = pidgin_dbus_call('PurpleAccountsGetAll', return_shellcommand: true)
487   assert(c.failure?)
488   assert_not_nil(c.stderr['Rejected send message'])