1require File.expand_path('../test_helper', __FILE__)
2require File.expand_path('../../PassengerPref', __FILE__)
3
4def OSX._ignore_ns_override; true; end
5
6class InstallPassengerWarning < OSX::NSView
7  def initWithTextField
8    if init
9      text_field = OSX::NSTextField.alloc.init
10      text_field.stringValue = "blabla http://www.modrails.com blabla"
11      addSubview text_field
12      self
13    end
14  end
15end
16
17module PrefPanePassengerSpecsHelper
18  def set_apps_controller_content(apps)
19    applicationsController.content = apps
20    applicationsController.selectedObjects = apps
21  end
22  
23  def stub_app_controller_with_number_of_apps(number)
24    apps = Array.new(number) do |i|
25      stub("PassengerApplication: #{i}")
26    end
27    set_apps_controller_content(apps)
28    apps
29  end
30  
31  def stub_app_controller_with_a_app
32    stub_app_controller_with_number_of_apps(1).first
33  end
34  
35  def alert_stub
36    window = stub_everything('Window')
37    alert = stub('Alert')
38    alert.stubs(:window).returns(window)
39    alert
40  end
41end
42
43describe "PrefPanePassenger, while initializing" do
44  tests PrefPanePassenger
45  
46  def after_setup
47    ib_outlets :installPassengerWarning => OSX::InstallPassengerWarning.alloc.initWithTextField,
48               :authorizationView => OSX::SFAuthorizationView.alloc.init
49    
50    pref_pane.stubs(:paneWillBecomeActive)
51  end
52  
53  it "should register itself as the sharedInstance" do
54    pref_pane.mainViewDidLoad
55    PrefPanePassenger.sharedInstance.should.be.instance_of PrefPanePassenger
56  end
57  
58  it "should configure the authorization view" do
59    authorizationView.expects(:string=).with(OSX::KAuthorizationRightExecute)
60    pref_pane.mainViewDidLoad
61    authorizationView.delegate.should.be pref_pane
62    assigns(:authorized).should.be false
63  end
64  
65  it "should initialize an empty array which will hold the list of applications" do
66    pref_pane.mainViewDidLoad
67    apps = assigns(:applications)
68    apps.should.be.instance_of OSX::NSCFArray
69    apps.should.be.empty
70  end
71  
72  it "should register itself for notifications for if the System Preferences.app will be activated" do
73    OSX::NSNotificationCenter.defaultCenter.expects(:objc_send).with(
74      :addObserver, pref_pane,
75         :selector, 'paneWillBecomeActive:',
76             :name, OSX::NSApplicationWillBecomeActiveNotification,
77           :object, nil
78    )
79    pref_pane.mainViewDidLoad
80  end
81end
82
83describe "PrefPanePassenger, when about to be (re)displayed" do
84  tests PrefPanePassenger
85  
86  def after_setup
87    ib_outlets :applicationsController => OSX::NSArrayController.alloc.init,
88               :applicationsTableView => OSX::NSTableView.alloc.init,
89               :installPassengerWarning => OSX::InstallPassengerWarning.alloc.initWithTextField
90    
91    pref_pane.stubs(:passenger_installed?).returns(false)
92  end
93  
94  it "should enable the 'install passenger' warning in the UI if the Passenger Apache module isn't loaded" do
95    installPassengerWarning.hidden = true
96    pref_pane.paneWillBecomeActive
97    
98    installPassengerWarning.hidden?.should.be false
99  end
100  
101  it "should disable the 'install passenger' warning in the UI if the Passenger Apache module is loaded" do
102    pref_pane.stubs(:passenger_installed?).returns(true)
103    installPassengerWarning.hidden = false
104    pref_pane.paneWillBecomeActive
105    
106    installPassengerWarning.hidden?.should.be true
107  end
108  
109  it "should add existing applications found in #{PassengerPaneConfig::PASSENGER_APPS_DIR} to the array controller: applicationsController" do
110    blog_app, paste_app = add_applications!
111    pref_pane.paneWillBecomeActive
112    
113    applicationsController.content.should == [blog_app, paste_app]
114    applicationsController.selectedObjects.should == [paste_app]
115  end
116  
117  it "should reload loaded applications from disk" do
118    blog_app, paste_app = add_applications!
119    pref_pane.paneWillBecomeActive
120    
121    blog_app.expects(:reload!)
122    paste_app.expects(:reload!)
123    pref_pane.willSelect
124  end
125  
126  private
127  
128  def add_applications!
129    dir = PassengerPaneConfig::PASSENGER_APPS_DIR
130    ext = PassengerPaneConfig::PASSENGER_APPS_EXTENSION
131    blog, paste = ["#{dir}/blog.#{ext}", "#{dir}/paste.#{ext}"]
132    apps = stub("PassengerApplication: blog"), stub("PassengerApplication: paste")
133    PassengerApplication.stubs(:existingApplications).returns(apps)
134    apps
135  end
136end
137
138describe "PrefPanePassenger, while checking for passenger" do
139  tests PrefPanePassenger
140  
141  it "should return true if the Passenger Apache modules is loaded" do
142    pref_pane.stubs(:`).with('/usr/sbin/httpd -t -D DUMP_MODULES 2>&1').returns(%{
143[Fri Jun 20 12:20:03 2008] [warn] _default_ VirtualHost overlap on port 80, the first has precedence
144[Fri Jun 20 12:20:03 2008] [warn] _default_ VirtualHost overlap on port 80, the first has precedence
145Loaded Modules:
146 core_module (static)
147 mpm_prefork_module (static)
148 http_module (static)
149 passenger_module (shared)
150Syntax OK})
151    
152    pref_pane.send(:passenger_installed?).should.be true
153  end
154  
155  it "should return false if the Passenger Apache modules is not loaded" do
156    pref_pane.stubs(:`).with('/usr/sbin/httpd -t -D DUMP_MODULES 2>&1').returns(%{
157[Fri Jun 20 12:20:03 2008] [warn] _default_ VirtualHost overlap on port 80, the first has precedence
158[Fri Jun 20 12:20:03 2008] [warn] _default_ VirtualHost overlap on port 80, the first has precedence
159Loaded Modules:
160 core_module (static)
161 mpm_prefork_module (static)
162 http_module (static)
163Syntax OK})
164
165    pref_pane.send(:passenger_installed?).should.be false
166  end
167end
168
169describe "PrefPanePassenger, when removing applications" do
170  tests PrefPanePassenger
171  
172  def after_setup
173    ib_outlets :applicationsController => OSX::NSArrayController.alloc.init
174  end
175  
176  it "should remove the selected applications from the applicationsController" do
177    remove_app, stay_app = stub("PassengerApplication: should be removed"), stub("PassengerApplication: should stay")
178    remove_app.stubs(:new_app?).returns(false)
179    stay_app.stubs(:new_app?).returns(false)
180    PassengerApplication.expects(:removeApplications).with([remove_app])
181    
182    applicationsController.content = [remove_app, stay_app]
183    applicationsController.selectedObjects = [remove_app]
184    
185    pref_pane.remove
186    applicationsController.content.should == [stay_app]
187  end
188  
189  it "should not try to delete files when removing a new application" do
190    app = PassengerApplication.alloc.init
191    applicationsController.content = [app]
192    applicationsController.selectedObjects = [app]
193    
194    PassengerApplication.expects(:removeApplications).times(0)
195    pref_pane.remove
196    applicationsController.content.should.be.empty
197  end
198  
199  it "should not open the browse panel after removing applications" do
200    pref_pane.expects(:browse).times(0)
201    pref_pane.setValue_forKey([], 'applications')
202  end
203end
204
205describe "PrefPanePassenger, when adding applications" do
206  tests PrefPanePassenger
207  
208  it "should open the browse panel when a new empty application is added to the applications array" do
209    pref_pane.expects(:browse).times(1)
210    pref_pane.setValue_forKey([PassengerApplication.alloc.init], 'applications')
211    
212    pref_pane.expects(:browse).times(0)
213    pref_pane.setValue_forKey([PassengerApplication.alloc.init, PassengerApplication.alloc.initWithFile(File.expand_path('../fixtures/blog.vhost.conf', __FILE__))], 'applications')
214  end
215end
216
217describe "PrefPanePassenger, when unselecting the pane" do
218  tests PrefPanePassenger
219  
220  include PrefPanePassengerSpecsHelper
221  
222  def after_setup
223    ib_outlets :applicationsController => OSX::NSArrayController.alloc.init
224    
225    mainView = stub('Main View')
226    pref_pane.stubs(:mainView).returns(mainView)
227    window = stub('Main Window')
228    mainView.stubs(:window).returns(window)
229    
230    pref_pane.stubs(:passenger_installed?).returns(true)
231    pref_pane.mainViewDidLoad
232  end
233  
234  it "should show a warning if the current selected application is dirty" do
235    set_apps_controller_content [PassengerApplication.alloc.initWithPath('/previous/path/to/Blog')]
236    
237    OSX::NSAlert.any_instance.expects(:objc_send).with(
238      :beginSheetModalForWindow, pref_pane.mainView.window,
239      :modalDelegate, pref_pane,
240      :didEndSelector, 'unsavedChangesAlertDidEnd:returnCode:contextInfo:',
241      :contextInfo, nil
242    ).times(1)
243    
244    pref_pane.shouldUnselect.should == OSX::NSUnselectLater
245  end
246  
247  it "should not show a warning if there are no dirty applications" do
248    assigns(:dirty_apps, false)
249    OSX::NSAlert.any_instance.expects(:objc_send).times(0)
250    pref_pane.shouldUnselect.should == OSX::NSUnselectNow
251  end
252  
253  it "should not show a warning if there aren't any applications" do
254    assigns(:dirty_apps, true)
255    applicationsController.content = []
256    OSX::NSAlert.any_instance.expects(:objc_send).times(0)
257    pref_pane.shouldUnselect.should == OSX::NSUnselectNow
258  end
259  
260  it "should save the application and then tell the pane to unselect if the user chooses to apply unsaved changes" do
261    pref_pane.expects(:apply)
262    pref_pane.expects(:replyToShouldUnselect).with(true)
263    pref_pane.unsavedChangesAlertDidEnd_returnCode_contextInfo(alert_stub, PrefPanePassenger::APPLY, nil)
264  end
265  
266  it "should tell the pane to not unselect if the user chooses to review unsaved changes" do
267    pref_pane.expects(:apply).times(0)
268    pref_pane.expects(:replyToShouldUnselect).with(false)
269    pref_pane.unsavedChangesAlertDidEnd_returnCode_contextInfo(alert_stub, PrefPanePassenger::CANCEL, nil)
270  end
271  
272  it "should remove new and unsaved apps and revert unsaved existing apps and tell the pane to unselect if the user chooses to not apply unsaved changes" do
273    new_app = PassengerApplication.alloc.init
274    existing_app = PassengerApplication.alloc.initWithFile(File.expand_path('../fixtures/blog.vhost.conf', __FILE__))
275    existing_app.setValue_forKey('foo.local', 'host')
276    
277    set_apps_controller_content([new_app, existing_app])
278    
279    PassengerApplication.expects(:removeApplications).times(0)
280    pref_pane.expects(:replyToShouldUnselect).with(true)
281    pref_pane.unsavedChangesAlertDidEnd_returnCode_contextInfo(alert_stub, PrefPanePassenger::DONT_APPLY, nil)
282    
283    applicationsController.content.should == [existing_app]
284    applicationsController.content.first.host.should == "het-manfreds-blog.local"
285  end
286end
287
288describe "PrefPanePassenger, when applying changes" do
289  tests PrefPanePassenger
290  
291  include PrefPanePassengerSpecsHelper
292  
293  def after_setup
294    ib_outlets :applicationsController => OSX::NSArrayController.alloc.init
295    pref_pane.stubs(:authorize!).returns(true)
296  end
297  
298  it "should show the authorizationView if necessary" do
299    pref_pane.expects(:authorize!).returns(true)
300    pref_pane.apply
301  end
302  
303  it "should send the apply message to all the dirty applications" do
304    apps = stub_app_controller_with_number_of_apps(3)
305    
306    apps.first.stubs(:dirty?).returns(false)
307    apps.first.expects(:apply).times(0)
308    
309    apps[1..2].each do |app|
310      app.stubs(:dirty?).returns(true)
311      app.expects(:apply).times(1)
312    end
313    
314    pref_pane.apply
315  end
316  
317  it "should set @dirty_apps and @revertable_apps to false once all unsaved apps received the apply message" do
318    assigns(:dirty_apps, true)
319    assigns(:revertable_apps, true)
320    pref_pane.apply
321    pref_pane.dirty_apps.should.be false
322    pref_pane.revertable_apps.should.be false
323  end
324end
325
326describe "PrefPanePassenger, when reverting changes" do
327  tests PrefPanePassenger
328  
329  include PrefPanePassengerSpecsHelper
330  
331  def after_setup
332    ib_outlets :applicationsController => OSX::NSArrayController.alloc.init
333  end
334  
335  it "should send the revert message to all revertable applications" do
336    apps = stub_app_controller_with_number_of_apps(3)
337    
338    apps.first.stubs(:revertable?).returns(false)
339    apps.first.expects(:revert).times(0)
340    
341    apps[1..2].each do |app|
342      app.stubs(:revertable?).returns(true)
343      app.expects(:revert).times(1)
344    end
345    
346    pref_pane.revert
347  end
348  
349  it "should set @dirty_apps and @revertable_apps to false once all unsaved apps received the revert message" do
350    assigns(:dirty_apps, true)
351    assigns(:revertable_apps, true)
352    pref_pane.revert
353    pref_pane.dirty_apps.should.be false
354    pref_pane.revertable_apps.should.be false
355  end
356end
357
358describe "PrefPanePassenger, when restarting applications" do
359  tests PrefPanePassenger
360  
361  include PrefPanePassengerSpecsHelper
362  
363  def after_setup
364    ib_outlets :applicationsController => OSX::NSArrayController.alloc.init
365  end
366  
367  it "should send the restart message to all not new applications" do
368    apps = stub_app_controller_with_number_of_apps(3)
369    
370    apps.first.stubs(:new_app?).returns(true)
371    apps.first.expects(:restart).times(0)
372    
373    apps[1..2].each do |app|
374      app.stubs(:new_app?).returns(false)
375      app.expects(:restart).times(1)
376    end
377    
378    pref_pane.restart
379  end
380end
381
382describe "PrefPanePassenger, when using the directory browse panel" do
383  tests PrefPanePassenger
384  
385  def after_setup
386    ib_outlets :applicationsController => OSX::NSArrayController.alloc.init
387    
388    mainView = stub('Main View')
389    pref_pane.stubs(:mainView).returns(mainView)
390    window = stub('Main Window')
391    mainView.stubs(:window).returns(window)
392    
393    PrefPanePassenger.any_instance.stubs(:applicationMarkedDirty)
394  end
395  
396  it "should display the path to the currently selected application" do
397    app = PassengerApplication.alloc.initWithPath('/previous/path/to/Blog')
398    applicationsController.content = [app]
399    applicationsController.selectedObjects = [app]
400    
401    pref_pane.send(:path_for_browser).should == '/previous/path/to/Blog'
402  end
403  
404  it "should display the home directory if no application is selected" do
405    applicationsController.selectedObjects = []
406    pref_pane.send(:path_for_browser).to_s.should == File.expand_path('~')
407  end
408  
409  it "should set the path to the selected directory as the path for the currently selected application" do
410    app = PassengerApplication.alloc.initWithPath('/previous/path/to/Blog')
411    applicationsController.content = [app]
412    applicationsController.selectedObjects = [app]
413    
414    OSX::NSOpenPanel.any_instance.expects(:canChooseDirectories=).with(true)
415    OSX::NSOpenPanel.any_instance.expects(:canChooseFiles=).with(false)
416    OSX::NSOpenPanel.any_instance.expects(:objc_send).with(
417      :beginSheetForDirectory, app.path,
418      :file, nil,
419      :types, nil,
420      :modalForWindow, pref_pane.mainView.window,
421      :modalDelegate, pref_pane,
422      :didEndSelector, 'openPanelDidEnd:returnCode:contextInfo:',
423      :contextInfo, nil
424    )
425    pref_pane.browse
426    
427    panel = stub('NSOpenPanel')
428    panel.stubs(:filename).returns('/some/path/to/Blog')
429    
430    app.expects(:setValue_forKey).with('/some/path/to/Blog', 'path')
431    pref_pane.openPanelDidEnd_returnCode_contextInfo(panel, OSX::NSOKButton, nil)
432  end
433  
434  it "should remove the new application if the user pressed cancel in the browse panel if it's a new not dirty app" do
435    remove_app = PassengerApplication.alloc.init
436    
437    applicationsController.content = [remove_app]
438    applicationsController.selectedObjects = [remove_app]
439    
440    pref_pane.openPanelDidEnd_returnCode_contextInfo(nil, OSX::NSCancelButton, nil)
441    applicationsController.content.should.be.empty
442  end
443  
444  it "should not remove an application when the user presses cancel in the browse panel if the app is dirty" do
445    stay_app = PassengerApplication.alloc.init
446    stay_app.setValue_forKey('foo.local', 'host')
447    
448    applicationsController.content = [stay_app]
449    applicationsController.selectedObjects = [stay_app]
450    
451    pref_pane.openPanelDidEnd_returnCode_contextInfo(nil, OSX::NSCancelButton, nil)
452    applicationsController.content.should == [stay_app]
453  end
454end
455
456describe "PrefPanePassenger, with drag and drop support" do
457  tests PrefPanePassenger
458  
459  def after_setup
460    ib_outlets :applicationsController => OSX::NSArrayController.alloc.init,
461               :applicationsTableView => OSX::NSTableView.alloc.init,
462               :installPassengerWarning => OSX::InstallPassengerWarning.alloc.initWithTextField
463    
464    @tmp = File.expand_path('../tmp')
465    FileUtils.mkdir_p @tmp
466    
467    pref_pane.stubs(:passenger_installed?).returns(true)
468    PassengerApplication.stubs(:existingApplications).returns([])
469    pref_pane.mainViewDidLoad
470  end
471  
472  def after_teardown
473    FileUtils.rm_rf @tmp
474  end
475  
476  it "should configure the table view to accept drag and drop operations" do
477    applicationsTableView.dataSource.should.be pref_pane
478    applicationsTableView.registeredDraggedTypes.should == [OSX::NSFilenamesPboardType]
479  end
480  
481  it "should allow multiple directories to be dropped and always add to the bottom of the list" do
482    assigns(:authorized, true)
483    stub_pb_and_info_with_two_directories
484    
485    applicationsTableView.expects(:setDropRow_dropOperation).with(0, OSX::NSTableViewDropAbove)
486    
487    pref_pane.tableView_validateDrop_proposedRow_proposedDropOperation(nil, @info, nil, nil).should == OSX::NSDragOperationGeneric
488  end
489  
490  it "should not allow files to be dropped" do
491    dir = File.join(@tmp, 'dir')
492    FileUtils.mkdir_p dir
493    file = File.join(@tmp, 'file')
494    `touch #{file}`
495    stub_pb_and_info_with [file, dir]
496    
497    pref_pane.tableView_validateDrop_proposedRow_proposedDropOperation(nil, @info, nil, nil).should == OSX::NSDragOperationNone
498  end
499  
500  it "should add valid applications to the applicationsController" do
501    stub_pb_and_info_with_two_directories
502    
503    PassengerApplication.expects(:startApplications).times(0)
504    pref_pane.tableView_acceptDrop_row_dropOperation(nil, @info, nil, nil)
505    
506    apps = applicationsController.content
507    apps.map { |app| app.path }.should == @dirs
508    apps.map { |app| app.host }.should == %w{ app1.local app2.local }
509    apps.all? { |app| app.valid? }.should.be true
510  end
511  
512  it "should not allow directories to be dropped if not authorized" do
513    assigns(:authorized, false)
514    pref_pane.tableView_validateDrop_proposedRow_proposedDropOperation(nil, nil, nil, nil).should == OSX::NSDragOperationNone
515  end
516  
517  it "should not open the browse panel if directories are dropped" do
518    assigns(:dropping_directories, false)
519    stub_pb_and_info_with_two_directories
520    
521    applicationsController.expects(:addObjects).with do |apps|
522      pref_pane.setValue_forKey([PassengerApplication.alloc.init], 'applications')
523      true
524    end
525    
526    pref_pane.expects(:browse).times(0)
527    pref_pane.tableView_acceptDrop_row_dropOperation(nil, @info, nil, nil)
528    assigns(:dropping_directories).should.be false
529  end
530  
531  it "should allow entries from the table view to be dragged to for instance a text editor" do
532    app1 = PassengerApplication.alloc.init
533    app2 = PassengerApplication.alloc.init
534    app1.host = "app1.local"
535    app2.host = "app2.local"
536    
537    applicationsController.content = [app1, app2]
538    applicationsController.selectedObjects = [app1, app2]
539    
540    pboard = OSX::NSPasteboard.generalPasteboard
541    allowed = pref_pane.tableView_writeRowsWithIndexes_toPasteboard(nil, OSX::NSIndexSet.indexSetWithIndexesInRange(0..1), pboard)
542    allowed.should.be true
543    pboard.propertyListForType(OSX::NSFilenamesPboardType).should == [app1.config_path, app2.config_path]
544  end
545  
546  private
547  
548  def stub_pb_and_info_with_two_directories
549    dir1 = File.join(@tmp, 'app1')
550    dir2 = File.join(@tmp, 'app2')
551    @dirs = [dir1, dir2]
552    @dirs.each { |f| FileUtils.mkdir_p f }
553    stub_pb_and_info_with @dirs
554  end
555  
556  def stub_pb_and_info_with(files)
557    @pb = stub("NSPasteboard")
558    @info = stub("NSDraggingInfo")
559    @info.stubs(:draggingPasteboard).returns(@pb)
560    @pb.stubs(:propertyListForType).with(OSX::NSFilenamesPboardType).returns(files.to_ns)
561  end
562end
563
564describe "PrefPanePassenger, in general" do
565  tests PrefPanePassenger
566  
567  include PrefPanePassengerSpecsHelper
568  
569  def after_setup
570    ib_outlets :applicationsController => OSX::NSArrayController.alloc.init,
571               :authorizationView => OSX::SFAuthorizationView.alloc.init
572    
573    pref_pane.stubs(:passenger_installed?).returns(true)
574    pref_pane.mainViewDidLoad
575  end
576  
577  it "should change the authorized state if a authorization request succeeds" do
578    authorizationView.authorization.expects(:objc_send).with(
579      :permitWithRight, OSX::KAuthorizationRightExecute,
580      :flags, (OSX::KAuthorizationFlagPreAuthorize | OSX::KAuthorizationFlagExtendRights | OSX::KAuthorizationFlagInteractionAllowed)
581    ).returns(0)
582    
583    pref_pane.expects(:authorizationViewDidAuthorize).times(1)
584    pref_pane.send(:authorize!).should.be true
585  end
586  
587  it "should not change the authorized state if a authorization request fails" do
588    authorizationView.authorization.expects(:objc_send).with(
589      :permitWithRight, OSX::KAuthorizationRightExecute,
590      :flags, (OSX::KAuthorizationFlagPreAuthorize | OSX::KAuthorizationFlagExtendRights | OSX::KAuthorizationFlagInteractionAllowed)
591    ).returns(60007)
592    
593    pref_pane.expects(:authorizationViewDidAuthorize).times(0)
594    pref_pane.send(:authorize!).should.be false
595  end
596  
597  it "should forward delegate messages from the authorization view to the security helper" do
598    authorization = stub('Authorization Ref')
599    authorizationView.authorization.stubs(:authorizationRef).returns(authorization)
600    pref_pane.authorizationViewDidAuthorize
601    OSX::SecurityHelper.sharedInstance.should.be.authorized
602    assigns(:authorized).should.be true
603    
604    pref_pane.authorizationViewDidDeauthorize
605    OSX::SecurityHelper.sharedInstance.should.not.be.authorized
606    assigns(:authorized).should.be false
607  end
608  
609  it "should know if there are dirty apps" do
610    app = PassengerApplication.alloc.init
611    set_apps_controller_content([app])
612    
613    app.setValue_forKey('foo.local', 'host')
614    pref_pane.dirty_apps.should.be true
615  end
616end